Skip to content

Commit 5df5b7c

Browse files
committed
feat(web): cleanup sync ui
1 parent 88ca50f commit 5df5b7c

13 files changed

Lines changed: 80 additions & 157 deletions

File tree

‎packages/translations/source/en.xlf‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@
55
<trans-unit id="dialogs.feedback.thanks">
66
<source>Thanks for your feedback.</source>
77
</trans-unit>
8-
<trans-unit id="dialogs.feedback.unsynced">
9-
<source>Not sent yet. It sends when you're online.</source>
10-
</trans-unit>
118
<trans-unit id="dialogs.feedback.content-rating">
129
<source>Content quality:</source>
1310
</trans-unit>
@@ -518,9 +515,6 @@
518515
<trans-unit id="shell.sync.offline-many">
519516
<source>Offline · <x id="0" equiv-text="${pending}"/> changes to sync</source>
520517
</trans-unit>
521-
<trans-unit id="shell.sync.unsynced">
522-
<source>Saved on this device. Syncs when you're online.</source>
523-
</trans-unit>
524518
<trans-unit id="store.offline.needs-network">
525519
<source>Connect to the internet to send this.</source>
526520
</trans-unit>

‎packages/web/src/components/dialogs/feedback-block.test.ts‎

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -92,24 +92,6 @@ describe('feedback-block', () => {
9292
'aria-hidden',
9393
'true',
9494
);
95-
expect(shadowRoot.querySelector('.thanks .unsynced')).toBeNull();
96-
});
97-
98-
it('says when saved feedback has not been sent yet', async () => {
99-
const { element, shadowRoot } = await fixture<FeedbackBlock>(
100-
html`<feedback-block></feedback-block>`,
101-
);
102-
element.sessionId = 'session-id';
103-
setStoreState({
104-
user: new Initialized(),
105-
feedback: { data: new Success([feedback]), subscription: new Initialized() },
106-
sync: { online: false, pending: { feedback: ['session-id'] } },
107-
} as unknown as Partial<RootState>);
108-
await element.updateComplete;
109-
110-
expect(shadowRoot.querySelector('.thanks .unsynced')).toHaveTextContent(
111-
"Not sent yet. It sends when you're online.",
112-
);
11395
});
11496

11597
it('hides the delete button when there is no saved feedback', async () => {

‎packages/web/src/components/dialogs/feedback-block.ts‎

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
setFeedback,
1515
} from '../../store/feedback';
1616
import { queueComplexSnackbar, queueSnackbar } from '../../store/snackbars';
17-
import { selectPending } from '../../store/sync';
1817
import type { UserState } from '../../store/user';
1918
import '../shared/star-rating';
2019
import { type StarRatingChangeDetail } from '../shared/star-rating';
@@ -49,12 +48,6 @@ export class FeedbackBlock extends ThemedElement {
4948
font-weight: 600;
5049
}
5150
52-
.thanks .unsynced {
53-
flex-basis: 100%;
54-
color: var(--hb-color-on-surface-variant);
55-
font-weight: 400;
56-
}
57-
5851
#commentInput {
5952
width: 100%;
6053
}
@@ -94,8 +87,6 @@ export class FeedbackBlock extends ThemedElement {
9487
private accessor user!: UserState;
9588
@fromStore((state, host: FeedbackBlock) => selectFeedbackById(state, host.sessionId))
9689
private accessor feedback!: RemoteData<Error, Feedback | false>;
97-
@fromStore((state) => selectPending(state, 'feedback'))
98-
private accessor unsyncedFeedback!: string[];
9990

10091
override willUpdate(changedProperties: PropertyValues) {
10192
if (changedProperties.has('feedback')) {
@@ -111,15 +102,6 @@ export class FeedbackBlock extends ThemedElement {
111102
? html`<div class="thanks">
112103
${illustration(thankYou)}
113104
<p>${msg('Thanks for your feedback.', { id: 'dialogs.feedback.thanks' })}</p>
114-
${
115-
this.sessionId && this.unsyncedFeedback.includes(this.sessionId)
116-
? html`<p class="unsynced">
117-
${msg("Not sent yet. It sends when you're online.", {
118-
id: 'dialogs.feedback.unsynced',
119-
})}
120-
</p>`
121-
: nothing
122-
}
123105
</div>`
124106
: nothing
125107
}

‎packages/web/src/components/schedule/session-element.test.ts‎

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -114,21 +114,6 @@ describe('session-element', () => {
114114
expect(bookmark).toHaveAttribute('label', 'Bookmark Example Session');
115115
expect(bookmark.pressed).toBe(true);
116116
expect(bookmark.querySelector('hoverboard-icon')).toHaveAttribute('name', 'bookmark-check');
117-
expect(shadowRoot.querySelector('.unsynced')).toBeNull();
118-
});
119-
120-
it('marks a bookmark that has not synced yet', async () => {
121-
const { shadowRoot } = await render({
122-
featuredSessions: new Success({ 'session-1': true }),
123-
unsyncedBookmarks: ['session-1'],
124-
} as never);
125-
const bookmark = shadowRoot.querySelector('hb-icon-button')!;
126-
127-
expect(bookmark).toHaveAttribute(
128-
'label',
129-
"Bookmark Example Session. Saved on this device. Syncs when you're online.",
130-
);
131-
expect(bookmark.querySelector('.unsynced')).toHaveAttribute('name', 'cloud-upload');
132117
});
133118

134119
it('asks to sign in before bookmarking', async () => {

‎packages/web/src/components/schedule/session-element.ts‎

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
setUserFeaturedSessions,
1414
} from '../../store/featured-sessions';
1515
import { queueComplexSnackbar } from '../../store/snackbars';
16-
import { selectPending, unsyncedMessage } from '../../store/sync';
1716
import type { UserState } from '../../store/user';
1817
import { acceptingFeedback } from '../../utils/feedback';
1918
import { confetti } from '../../utils/confetti';
@@ -157,16 +156,6 @@ export class SessionElement extends ThemedElement {
157156
inset-inline-end: var(--hb-space-1);
158157
}
159158
160-
.unsynced {
161-
position: absolute;
162-
inset-block-end: 4px;
163-
inset-inline-end: 4px;
164-
inline-size: 14px;
165-
block-size: 14px;
166-
border-radius: 50%;
167-
background-color: var(--hb-panel-background);
168-
}
169-
170159
@media (prefers-reduced-motion: reduce) {
171160
.session:hover {
172161
translate: none;
@@ -187,8 +176,6 @@ export class SessionElement extends ThemedElement {
187176
accessor session: BuiltSession | undefined;
188177
@fromStore((state) => selectFeaturedSessionsState(state))
189178
accessor featuredSessions!: FeaturedSessionsState;
190-
@fromStore((state) => selectPending(state, 'featuredSessions'))
191-
private accessor unsyncedBookmarks!: string[];
192179

193180
// Depends on the time, so it is only set in the browser.
194181
@state()
@@ -270,23 +257,16 @@ export class SessionElement extends ThemedElement {
270257
}
271258
if (!__HB_FEATURES__.mySchedule) return nothing;
272259
const bookmarked = this.isBookmarked;
273-
const unsynced = bookmarked && this.unsyncedBookmarks.includes(session.id);
274-
const label = msg(str`Bookmark ${session.title}`, { id: 'schedule.session.bookmark' });
275260
return html`
276261
<hb-icon-button
277262
class="action bookmark"
278-
label="${unsynced ? `${label}. ${unsyncedMessage()}` : label}"
263+
label="${msg(str`Bookmark ${session.title}`, { id: 'schedule.session.bookmark' })}"
279264
.pressed="${bookmarked}"
280265
@click="${this.toggleBookmark}"
281266
>
282267
<hoverboard-icon
283268
name="${bookmarked ? 'bookmark-check' : 'bookmark-plus'}"
284269
></hoverboard-icon>
285-
${
286-
unsynced
287-
? html`<hoverboard-icon class="unsynced" name="cloud-upload"></hoverboard-icon>`
288-
: nothing
289-
}
290270
</hb-icon-button>
291271
`;
292272
}

‎packages/web/src/components/shared/hoverboard-icon.test.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ describe('hoverboard-icon', () => {
5858
'density-small',
5959
'density-medium',
6060
'density-large',
61+
'cloud-off',
62+
'cloud-upload',
6163
])('renders the %s icon', async (name) => {
6264
const { shadowRoot } = await fixture(html`<hoverboard-icon name="${name}"></hoverboard-icon>`);
6365

@@ -92,6 +94,15 @@ describe('hoverboard-icon', () => {
9294
expect(missing).toEqual([]);
9395
});
9496

97+
it('bundles the icons that show offline, which cannot load a chunk then', async () => {
98+
const source = (await import('./hoverboard-icon.ts?raw')).default;
99+
100+
for (const name of ['cloud-off', 'cloud-upload']) {
101+
expect(source).toMatch(new RegExp(`^import \\w+ from './icons/${name}';$`, 'm'));
102+
expect(source).not.toContain(`import('./icons/${name}')`);
103+
}
104+
});
105+
95106
it('renders nothing for an unknown icon name', async () => {
96107
const element = document.createElement('hoverboard-icon') as HoverboardIcon;
97108
element.name = 'not-real';

‎packages/web/src/components/shared/hoverboard-icon.ts‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { css, html, isServer, type PropertyValues, type SVGTemplateResult } from 'lit';
22
import { customElement, property, state } from 'lit/decorators.js';
33
import { ThemedElement } from '../themed-element';
4+
import cloudOff from './icons/cloud-off';
5+
import cloudUpload from './icons/cloud-upload';
46

57
// Lit replacement for the retired `<iron-icon icon="hoverboard:name">` Polymer iconset.
68
// Each icon is its own module under ./icons so only the icons a page uses are loaded.
@@ -48,8 +50,6 @@ const ICONS: Record<string, () => Promise<{ default: SVGTemplateResult }>> = {
4850
people: () => import('./icons/people'),
4951
registration: () => import('./icons/registration'),
5052
'bell-off': () => import('./icons/bell-off'),
51-
'cloud-off': () => import('./icons/cloud-off'),
52-
'cloud-upload': () => import('./icons/cloud-upload'),
5353
'bell-outline': () => import('./icons/bell-outline'),
5454
monitor: () => import('./icons/monitor'),
5555
sun: () => import('./icons/sun'),
@@ -59,7 +59,11 @@ const ICONS: Record<string, () => Promise<{ default: SVGTemplateResult }>> = {
5959
'density-large': () => import('./icons/density-large'),
6060
};
6161

62-
const loaded = new Map<string, SVGTemplateResult>();
62+
// The icons that show offline are bundled, since their chunks might not be cached.
63+
const loaded = new Map<string, SVGTemplateResult>([
64+
['cloud-off', cloudOff],
65+
['cloud-upload', cloudUpload],
66+
]);
6367

6468
@customElement('hoverboard-icon')
6569
export class HoverboardIcon extends ThemedElement {
@@ -104,12 +108,12 @@ export class HoverboardIcon extends ThemedElement {
104108
}
105109

106110
private loadIcon(name: string) {
111+
const cached = loaded.get(name);
107112
const load = ICONS[name];
108-
if (!load) {
113+
if (!cached && !load) {
109114
return;
110115
}
111-
const cached = loaded.get(name);
112-
const loading = (cached ? Promise.resolve({ default: cached }) : load())
116+
const loading = (cached ? Promise.resolve({ default: cached }) : load!())
113117
.then((module) => {
114118
loaded.set(name, module.default);
115119
if (this.name === name) {

‎packages/web/src/components/shell/sync-status.test.ts‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,28 @@ describe('sync-status', () => {
1414
expect(element.shadowRoot?.querySelector('details')).toBeNull();
1515
});
1616

17-
it('says when the site is offline, with the changes to sync and what works offline', async () => {
17+
it('shows only an icon while offline with nothing to sync', async () => {
18+
setStoreState({ sync: { online: false, pending: {} } });
19+
const { shadowRoot, shadowRootForWithin } = await fixture(html`<sync-status></sync-status>`);
20+
const status = within(shadowRootForWithin).getByRole('status');
21+
22+
expect(status).toHaveTextContent(/^Offline$/);
23+
expect(status.querySelector('[aria-hidden]')).toBeNull();
24+
expect(shadowRoot.querySelector('hoverboard-icon')).toHaveAttribute('name', 'cloud-off');
25+
});
26+
27+
it('shows the number of changes to sync, with what works offline', async () => {
1828
setStoreState({ sync: { online: false, pending: { featuredSessions: ['a', 'b'] } } });
19-
const { shadowRootForWithin } = await fixture(html`<sync-status></sync-status>`);
29+
const { shadowRoot, shadowRootForWithin } = await fixture(html`<sync-status></sync-status>`);
2030
const view = within(shadowRootForWithin);
31+
const status = view.getByRole('status');
2132

22-
expect(view.getByRole('status')).toHaveTextContent('Offline · 2 changes to sync');
33+
expect(status.querySelector('[aria-hidden="true"]')).toHaveTextContent(/^2$/);
34+
expect(shadowRoot.querySelector('hoverboard-icon')).toHaveAttribute('name', 'cloud-upload');
35+
expect(status.querySelector('.visually-hidden')).toHaveTextContent(
36+
'Offline · 2 changes to sync',
37+
);
38+
expect(status).toHaveAttribute('title', 'Offline · 2 changes to sync');
2339
expect(
2440
view.getByText(/saved on this device, and sync when you are online/),
2541
).toBeInTheDocument();
@@ -28,7 +44,9 @@ describe('sync-status', () => {
2844
it('says when it syncs changes made offline', async () => {
2945
setStoreState({ sync: { online: true, pending: { feedback: ['a'] } } });
3046
const { shadowRootForWithin } = await fixture(html`<sync-status></sync-status>`);
47+
const status = within(shadowRootForWithin).getByRole('status');
3148

32-
expect(within(shadowRootForWithin).getByRole('status')).toHaveTextContent('Syncing…');
49+
expect(status.querySelector('[aria-hidden="true"]')).toHaveTextContent(/^1$/);
50+
expect(status.querySelector('.visually-hidden')).toHaveTextContent('Syncing…');
3351
});
3452
});

‎packages/web/src/components/shell/sync-status.ts‎

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,17 @@ export class SyncStatus extends ThemedElement {
2121
summary {
2222
display: inline-flex;
2323
align-items: center;
24+
justify-content: center;
2425
gap: var(--hb-space-1);
25-
min-block-size: 32px;
26-
padding: var(--hb-space-1) var(--hb-space-3);
26+
min-inline-size: 40px;
27+
block-size: 40px;
28+
box-sizing: border-box;
29+
padding: 0 var(--hb-space-2);
2730
border-radius: var(--hb-radius-full);
2831
background-color: var(--hb-color-accent-2-container);
2932
color: var(--hb-color-on-accent-2-container);
30-
font: 500 var(--hb-text-sm) / 1.2 var(--hb-font-mono);
33+
font: 600 var(--hb-text-sm) / 1 var(--hb-font-mono);
34+
white-space: nowrap;
3135
list-style: none;
3236
cursor: pointer;
3337
}
@@ -42,8 +46,18 @@ export class SyncStatus extends ThemedElement {
4246
}
4347
4448
hoverboard-icon {
45-
inline-size: 18px;
46-
block-size: 18px;
49+
inline-size: 20px;
50+
block-size: 20px;
51+
}
52+
53+
/* Still read by screen readers. */
54+
.visually-hidden {
55+
position: absolute;
56+
inline-size: 1px;
57+
block-size: 1px;
58+
overflow: hidden;
59+
clip-path: inset(50%);
60+
white-space: nowrap;
4761
}
4862
4963
.help {
@@ -79,9 +93,10 @@ export class SyncStatus extends ThemedElement {
7993
if (!label) return nothing;
8094
return html`
8195
<details>
82-
<summary role="status">
83-
<hoverboard-icon name="${this.online ? 'cloud-upload' : 'cloud-off'}"></hoverboard-icon>
84-
${label}
96+
<summary role="status" title="${label}">
97+
${this.pending ? html`<span aria-hidden="true">${this.pending}</span>` : nothing}
98+
<hoverboard-icon name="${this.pending ? 'cloud-upload' : 'cloud-off'}"></hoverboard-icon>
99+
<span class="visually-hidden">${label}</span>
85100
</summary>
86101
<p class="help">
87102
${msg(

‎packages/web/src/store/sync/index.test.ts‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import reducer, {
33
canWriteNow,
44
resetPending,
55
selectOnline,
6-
selectPending,
76
selectPendingCount,
87
setPendingIds,
98
syncLabel,
@@ -44,7 +43,6 @@ describe('sync', () => {
4443
setPendingIds('featuredSessions', ['session-1', 'session-2']);
4544
setPendingIds('feedback', ['session-3']);
4645
expect(selectPendingCount(store.getState())).toBe(3);
47-
expect(selectPending(store.getState(), 'feedback')).toEqual(['session-3']);
4846

4947
go(true);
5048
expect(lastSnackbar()).not.toBe('Back online. Your changes are saved.');

0 commit comments

Comments
 (0)