@blotch/dolan 22.2.4 → 22.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/dist/commands/add.test.js +2 -0
  2. package/dist/commands/add.test.js.map +1 -1
  3. package/dist/commands/doctor.js +78 -21
  4. package/dist/commands/doctor.js.map +1 -1
  5. package/dist/commands/doctor.test.js +167 -26
  6. package/dist/commands/doctor.test.js.map +1 -1
  7. package/dist/commands/init.test.js +4 -7
  8. package/dist/commands/init.test.js.map +1 -1
  9. package/dist/commands/update.d.ts +3 -1
  10. package/dist/commands/update.js +78 -15
  11. package/dist/commands/update.js.map +1 -1
  12. package/dist/commands/update.test.js +257 -2
  13. package/dist/commands/update.test.js.map +1 -1
  14. package/dist/lockfile.d.ts +10 -0
  15. package/dist/lockfile.js +20 -1
  16. package/dist/lockfile.js.map +1 -1
  17. package/dist/lockfile.test.js +43 -3
  18. package/dist/lockfile.test.js.map +1 -1
  19. package/dist/registry.js +73 -11
  20. package/dist/registry.js.map +1 -1
  21. package/dist/registry.test.js +16 -3
  22. package/dist/registry.test.js.map +1 -1
  23. package/dist/test-utils.d.ts +24 -2
  24. package/dist/test-utils.js +41 -2
  25. package/dist/test-utils.js.map +1 -1
  26. package/dist/ui/boolean-inputs.test.ts +76 -0
  27. package/dist/ui/components/action-sheet/action-sheet.test.ts +370 -1
  28. package/dist/ui/components/action-sheet/action-sheet.ts +306 -28
  29. package/dist/ui/components/action-sheet/index.ts +1 -0
  30. package/dist/ui/components/alert-dialog/alert-dialog.test.ts +552 -8
  31. package/dist/ui/components/alert-dialog/alert-dialog.ts +644 -36
  32. package/dist/ui/components/alert-dialog/index.ts +5 -0
  33. package/dist/ui/components/area-chart/area-chart.ts +3 -2
  34. package/dist/ui/components/badge/badge.ts +2 -1
  35. package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +968 -3
  36. package/dist/ui/components/bottom-sheet/bottom-sheet.ts +1021 -95
  37. package/dist/ui/components/bottom-sheet/index.ts +7 -1
  38. package/dist/ui/components/button/button.test.ts +36 -1
  39. package/dist/ui/components/button/button.ts +17 -7
  40. package/dist/ui/components/button-group/button-group.test.ts +53 -9
  41. package/dist/ui/components/button-group/button-group.ts +67 -21
  42. package/dist/ui/components/card/card.test.ts +16 -0
  43. package/dist/ui/components/card/card.ts +2 -1
  44. package/dist/ui/components/chart/chart-presets.ts +6 -6
  45. package/dist/ui/components/chart/chart.test.ts +14 -0
  46. package/dist/ui/components/chart/chart.ts +10 -3
  47. package/dist/ui/components/checkbox/checkbox.ts +2 -1
  48. package/dist/ui/components/collapsible/collapsible.ts +2 -1
  49. package/dist/ui/components/dialog/dialog.ts +6 -6
  50. package/dist/ui/components/dock/dock-scroll.ts +9 -0
  51. package/dist/ui/components/dock/dock-search.test.ts +594 -0
  52. package/dist/ui/components/dock/dock.test.ts +4 -1
  53. package/dist/ui/components/dock/dock.ts +529 -51
  54. package/dist/ui/components/dynamic-color/dynamic-color.test.ts +144 -0
  55. package/dist/ui/components/dynamic-color/dynamic-color.ts +146 -0
  56. package/dist/ui/components/dynamic-color/index.ts +5 -0
  57. package/dist/ui/components/glass/glass.test.ts +39 -1
  58. package/dist/ui/components/glass/glass.ts +87 -17
  59. package/dist/ui/components/glass/index.ts +1 -0
  60. package/dist/ui/components/icon/icon-theme.test.ts +28 -0
  61. package/dist/ui/components/icon/icon-theme.ts +38 -7
  62. package/dist/ui/components/input/input.ts +2 -1
  63. package/dist/ui/components/label/label.ts +8 -2
  64. package/dist/ui/components/line-chart/line-chart.ts +3 -2
  65. package/dist/ui/components/list/list.ts +5 -4
  66. package/dist/ui/components/menu/menu-popup.test.ts +18 -14
  67. package/dist/ui/components/menu/menu-popup.ts +24 -3
  68. package/dist/ui/components/menu/menu.test.ts +99 -5
  69. package/dist/ui/components/menu/menu.ts +74 -34
  70. package/dist/ui/components/nav-drawer/nav-drawer.ts +3 -2
  71. package/dist/ui/components/navigation-bar/index.ts +1 -0
  72. package/dist/ui/components/navigation-bar/navigation-bar.test.ts +425 -0
  73. package/dist/ui/components/navigation-bar/navigation-bar.ts +411 -0
  74. package/dist/ui/components/popover/index.ts +9 -0
  75. package/dist/ui/components/popover/popover-panel.test.ts +735 -0
  76. package/dist/ui/components/popover/popover-panel.ts +603 -0
  77. package/dist/ui/components/popover/popover.test.ts +286 -0
  78. package/dist/ui/components/popover/popover.ts +207 -0
  79. package/dist/ui/components/progress/progress.ts +2 -1
  80. package/dist/ui/components/radio-group/radio-group.ts +2 -1
  81. package/dist/ui/components/scroll-area/scroll-area.ts +4 -3
  82. package/dist/ui/components/search-field/index.ts +15 -0
  83. package/dist/ui/components/search-field/keyboard.test.ts +147 -0
  84. package/dist/ui/components/search-field/keyboard.ts +123 -0
  85. package/dist/ui/components/search-field/search-field.test.ts +549 -0
  86. package/dist/ui/components/search-field/search-field.ts +628 -0
  87. package/dist/ui/components/segmented-control/index.ts +8 -0
  88. package/dist/ui/components/segmented-control/segmented-control.test.ts +861 -0
  89. package/dist/ui/components/segmented-control/segmented-control.ts +884 -0
  90. package/dist/ui/components/select/select.ts +2 -1
  91. package/dist/ui/components/sheet/sheet.test.ts +90 -3
  92. package/dist/ui/components/sheet/sheet.ts +59 -4
  93. package/dist/ui/components/slider/index.ts +1 -0
  94. package/dist/ui/components/slider/slider.test.ts +695 -0
  95. package/dist/ui/components/slider/slider.ts +901 -0
  96. package/dist/ui/components/switch/index.ts +1 -1
  97. package/dist/ui/components/switch/switch.test.ts +464 -48
  98. package/dist/ui/components/switch/switch.ts +613 -43
  99. package/dist/ui/components/tabs/index.ts +7 -1
  100. package/dist/ui/components/tabs/tabs.test.ts +278 -2
  101. package/dist/ui/components/tabs/tabs.ts +233 -30
  102. package/dist/ui/components/textarea/textarea.ts +2 -1
  103. package/dist/ui/components/toggle/index.ts +1 -0
  104. package/dist/ui/components/toggle/toggle.test.ts +310 -4
  105. package/dist/ui/components/toggle/toggle.ts +241 -26
  106. package/dist/ui/components/toolbar/bar-scroll.test.ts +310 -0
  107. package/dist/ui/components/toolbar/bar-scroll.ts +372 -0
  108. package/dist/ui/components/toolbar/index.ts +12 -0
  109. package/dist/ui/components/toolbar/toolbar.test.ts +438 -0
  110. package/dist/ui/components/toolbar/toolbar.ts +535 -0
  111. package/dist/utils/analyze.d.ts +10 -5
  112. package/dist/utils/analyze.js +3 -2
  113. package/dist/utils/analyze.js.map +1 -1
  114. package/dist/utils/installed.d.ts +13 -1
  115. package/dist/utils/installed.js +17 -0
  116. package/dist/utils/installed.js.map +1 -1
  117. package/dist/utils/installed.test.js +39 -1
  118. package/dist/utils/installed.test.js.map +1 -1
  119. package/dist/utils/tailwind.d.ts +25 -5
  120. package/dist/utils/tailwind.js +53 -14
  121. package/dist/utils/tailwind.js.map +1 -1
  122. package/dist/utils/tailwind.test.js +85 -22
  123. package/dist/utils/tailwind.test.js.map +1 -1
  124. package/package.json +1 -1
@@ -0,0 +1,438 @@
1
+ import { Component, type Provider, signal } from '@angular/core';
2
+ import {
3
+ fireEvent,
4
+ render,
5
+ waitForUpdate,
6
+ } from '@blotch/angular-lynx-testing-library';
7
+ import { LYNX_ELEMENTS, LynxAccessibilitySettings } from '@blotch/angular-lynx';
8
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
9
+ import { fireLynxEvent } from '../../../test-utils/fire-lynx-event';
10
+ import { provideGlass } from '../glass/glass-design';
11
+ import { type IconName } from '../icon/icons';
12
+ import { UiBarState } from './bar-scroll';
13
+ import {
14
+ UiToolbar,
15
+ UiToolbarGroup,
16
+ UiToolbarItem,
17
+ UiToolbarSpacer,
18
+ } from './toolbar';
19
+
20
+ @Component({
21
+ imports: [
22
+ LYNX_ELEMENTS,
23
+ UiToolbar,
24
+ UiToolbarGroup,
25
+ UiToolbarItem,
26
+ UiToolbarSpacer,
27
+ ],
28
+ template: `
29
+ <ui-toolbar id="toolbar" [class]="toolbarClass()">
30
+ <ui-toolbar-group id="group" [class]="groupClass()">
31
+ <ui-toolbar-item
32
+ icon="list-checks"
33
+ label="Checklist"
34
+ (pressed)="did('Checklist')"
35
+ />
36
+ <ui-toolbar-item label="Select" prominent (pressed)="did('Select')" />
37
+ </ui-toolbar-group>
38
+ <ui-toolbar-spacer />
39
+ <ui-toolbar-item
40
+ label="Compose"
41
+ [icon]="icon()"
42
+ [prominent]="prominent()"
43
+ [back]="back()"
44
+ [disabled]="disabled()"
45
+ (pressed)="did('Compose')"
46
+ />
47
+ </ui-toolbar>
48
+ `,
49
+ })
50
+ class ToolbarHost {
51
+ readonly toolbarClass = signal('');
52
+ readonly groupClass = signal('');
53
+ readonly icon = signal<IconName | undefined>('pencil');
54
+ readonly prominent = signal(false);
55
+ readonly back = signal(false);
56
+ readonly disabled = signal(false);
57
+ readonly actions = signal<string[]>([]);
58
+
59
+ did(action: string): void {
60
+ this.actions.update((actions) => [...actions, action]);
61
+ }
62
+ }
63
+
64
+ /** Accessibility settings a test can change, as the host app would. */
65
+ const settings = {
66
+ reduceMotion: signal(false),
67
+ reduceTransparency: signal(false),
68
+ increaseContrast: signal(false),
69
+ };
70
+
71
+ const IOS_26 = { platform: 'iOS', osVersion: '26.0' };
72
+ const IOS_18 = { platform: 'iOS', osVersion: '18.5' };
73
+ const ANDROID = { platform: 'Android', osVersion: '15' };
74
+ const WEB = { platform: 'web', osVersion: '' };
75
+
76
+ /** The theme's light colors, as icon-theme.ts resolves them for an SVG. */
77
+ const PRIMARY = 'rgba(24, 24, 27, 1)';
78
+ const PRIMARY_FOREGROUND = 'rgba(250, 250, 250, 1)';
79
+ const FOREGROUND = 'rgba(9, 9, 11, 1)';
80
+ const MUTED_FOREGROUND = 'rgba(113, 113, 122, 1)';
81
+
82
+ const renderToolbar = async (
83
+ platform: { platform: string; osVersion: string },
84
+ providers: Provider[] = [],
85
+ ) => {
86
+ vi.stubGlobal('SystemInfo', platform);
87
+ const result = await render(ToolbarHost, {
88
+ providers: [
89
+ { provide: LynxAccessibilitySettings, useValue: settings },
90
+ ...providers,
91
+ ],
92
+ });
93
+ const host = result.componentRef.instance as ToolbarHost;
94
+ const state = result.componentRef.injector.get(UiBarState);
95
+ const { container } = result;
96
+ const toolbar = () => container.querySelector<HTMLElement>('#toolbar')!;
97
+ const group = () => container.querySelector<HTMLElement>('#group')!;
98
+ const item = (name: string) => result.getByRole('button', { name });
99
+ /**
100
+ * The item's button: the content its glass, or the item, holds.
101
+ */
102
+ const button = (name: string) =>
103
+ item(name).querySelector<HTMLElement>('.relative')!;
104
+ const ownGlass = (name: string) =>
105
+ item(name).querySelector<HTMLElement>('[ui-glass]');
106
+ const iconOf = (name: string) =>
107
+ item(name).querySelector('svg')?.getAttribute('content') ?? '';
108
+ const labelOf = (name: string) =>
109
+ item(name).querySelector('text') as HTMLElement | null;
110
+ const update = async (change: () => void) => {
111
+ change();
112
+ await waitForUpdate();
113
+ };
114
+ const press = async (name: string) => {
115
+ fireEvent.touchstart(item(name));
116
+ await waitForUpdate();
117
+ };
118
+ const release = async (name: string) => {
119
+ fireEvent.touchend(item(name));
120
+ await waitForUpdate();
121
+ };
122
+ return {
123
+ ...result,
124
+ host,
125
+ state,
126
+ toolbar,
127
+ group,
128
+ item,
129
+ button,
130
+ ownGlass,
131
+ iconOf,
132
+ labelOf,
133
+ update,
134
+ press,
135
+ release,
136
+ };
137
+ };
138
+
139
+ /**
140
+ * The highlight under a pressed item's content, if it shows one.
141
+ */
142
+ const highlightOf = (button: HTMLElement) =>
143
+ Array.from(button.children).find((child) =>
144
+ child.className.includes('absolute'),
145
+ ) as HTMLElement | undefined;
146
+
147
+ // The components' selectors aren't Lynx elements, so the renderer falls back
148
+ // to a <view> for each and warns.
149
+ beforeEach(() => {
150
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
151
+ });
152
+
153
+ afterEach(() => {
154
+ vi.restoreAllMocks();
155
+ vi.unstubAllGlobals();
156
+ settings.reduceMotion.set(false);
157
+ });
158
+
159
+ describe('UiToolbar on iOS 26 and later', () => {
160
+ it('floats its items over the content above the home indicator, with no bar behind them', async () => {
161
+ const { toolbar, state } = await renderToolbar(IOS_26);
162
+ const classes = toolbar().classList;
163
+ expect(classes).toContain('absolute');
164
+ expect(classes).toContain('bottom-0');
165
+ expect(classes).toContain('pb-safe-[8px]');
166
+ expect(classes).not.toContain('bg-secondary');
167
+ expect(toolbar().querySelector('[ui-glass][shape="rounded"]')).toBeNull();
168
+ // It tells the scroll view under it how much room it takes.
169
+ expect(state.bottomBar()?.height()).toBe(52);
170
+ });
171
+
172
+ it('puts a group of items on one glass capsule, plain inside it', async () => {
173
+ const { group, item, ownGlass } = await renderToolbar(IOS_26);
174
+ const glass = group().querySelector<HTMLElement>('[ui-glass]')!;
175
+ expect(glass.getAttribute('blur-effect')).toBe('glass');
176
+ expect(glass.getAttribute('glass-interactive')).toBe('true');
177
+ expect(glass.contains(item('Checklist'))).toBe(true);
178
+ expect(ownGlass('Checklist')).toBeNull();
179
+ });
180
+
181
+ it('gives an item on its own a glass circle for an icon, and a capsule for a label', async () => {
182
+ const { host, ownGlass, update, iconOf, labelOf, button } =
183
+ await renderToolbar(IOS_26);
184
+ expect(ownGlass('Compose')?.getAttribute('blur-effect')).toBe('glass');
185
+ expect(ownGlass('Compose')?.classList).toContain('rounded-full');
186
+ expect(button('Compose').className).toContain('w-[44px]');
187
+ expect(iconOf('Compose')).toContain(FOREGROUND);
188
+ expect(labelOf('Compose')).toBeNull();
189
+
190
+ await update(() => host.icon.set(undefined));
191
+ expect(button('Compose').className).toContain('px-[16px]');
192
+ expect(labelOf('Compose')?.textContent).toBe('Compose');
193
+ expect(labelOf('Compose')?.className).toContain('text-glass-foreground');
194
+ });
195
+
196
+ it('tints a prominent item on its own, and leaves one in a group plain', async () => {
197
+ const { host, ownGlass, update, labelOf, iconOf } =
198
+ await renderToolbar(IOS_26);
199
+ await update(() => host.prominent.set(true));
200
+ expect(ownGlass('Compose')?.getAttribute('glass-tint-color')).toBe(PRIMARY);
201
+ expect(iconOf('Compose')).toContain(PRIMARY_FOREGROUND);
202
+
203
+ await update(() => host.icon.set(undefined));
204
+ expect(labelOf('Compose')?.className).toContain(
205
+ 'text-glass-tint-foreground',
206
+ );
207
+ // In a group, prominent is ignored: a key action stands on its own.
208
+ expect(labelOf('Select')?.className).toContain('text-glass-foreground');
209
+ });
210
+
211
+ it('draws a Back item as a glass circle with a chevron, its label for screen readers', async () => {
212
+ const { host, ownGlass, update, labelOf, iconOf, item, button } =
213
+ await renderToolbar(IOS_26);
214
+ await update(() => host.back.set(true));
215
+ expect(ownGlass('Compose')).not.toBeNull();
216
+ expect(button('Compose').className).toContain('w-[44px]');
217
+ expect(iconOf('Compose')).toContain('m15 18-6-6 6-6');
218
+ expect(labelOf('Compose')).toBeNull();
219
+ expect(item('Compose').getAttribute('accessibility-label')).toBe('Compose');
220
+ });
221
+
222
+ it('leaves a press to the glass, its own or the group’s capsule', async () => {
223
+ const { press, button } = await renderToolbar(IOS_26);
224
+ await press('Compose');
225
+ expect(highlightOf(button('Compose'))).toBeUndefined();
226
+
227
+ // The group's interactive glass swells under the finger, as iOS 26's
228
+ // capsule does, with no gray shade.
229
+ await press('Checklist');
230
+ expect(highlightOf(button('Checklist'))).toBeUndefined();
231
+ });
232
+
233
+ it('emits pressed on a tap, and dims and ignores a disabled item', async () => {
234
+ const { host, item, update, press, button, ownGlass } =
235
+ await renderToolbar(IOS_26);
236
+ fireEvent.tap(item('Checklist'));
237
+ fireEvent.tap(item('Compose'));
238
+ expect(host.actions()).toEqual(['Checklist', 'Compose']);
239
+
240
+ await update(() => host.disabled.set(true));
241
+ expect(item('Compose').getAttribute('accessibility-traits')).toBe(
242
+ 'button,disabled',
243
+ );
244
+ // The content dims, not the glass, which iOS would wash out.
245
+ expect(button('Compose').className).toContain('opacity-50');
246
+ expect(ownGlass('Compose')?.getAttribute('glass-interactive')).toBeNull();
247
+ await press('Compose');
248
+ expect(highlightOf(button('Compose'))).toBeUndefined();
249
+ fireEvent.tap(item('Compose'));
250
+ expect(host.actions()).toEqual(['Checklist', 'Compose']);
251
+
252
+ await update(() => host.disabled.set(false));
253
+ expect(item('Compose').getAttribute('accessibility-traits')).toBe('button');
254
+ });
255
+
256
+ it('takes extra classes on the toolbar and the group', async () => {
257
+ const { host, toolbar, group, update } = await renderToolbar(IOS_26);
258
+ await update(() => {
259
+ host.toolbarClass.set('mb-4');
260
+ host.groupClass.set('ml-2');
261
+ });
262
+ expect(toolbar().classList).toContain('mb-4');
263
+ expect(group().classList).toContain('ml-2');
264
+ });
265
+ });
266
+
267
+ describe('UiToolbar before iOS 26', () => {
268
+ it("is the toolbar of that time: edge to edge, on the bars' chrome material with a hairline while content continues beneath it", async () => {
269
+ const { toolbar, state, update } = await renderToolbar(IOS_18);
270
+ expect(toolbar().classList).toContain('pb-safe');
271
+ expect(state.bottomBar()?.height()).toBe(44);
272
+ const material = () =>
273
+ toolbar().querySelector<HTMLElement>('[ui-glass][shape="rounded"]');
274
+ expect(material()).toBeNull();
275
+
276
+ await update(() => state.scrolledUnderBottom.set(true));
277
+ expect(material()?.getAttribute('blur-material')).toBe('chrome');
278
+ expect(material()?.getAttribute('blur-effect')).toBe('extra-light');
279
+ expect(toolbar().querySelector('.h-\\[0\\.5px\\]')).not.toBeNull();
280
+ });
281
+
282
+ it('draws plain bar buttons in the tint color, which fade while pressed', async () => {
283
+ const { group, iconOf, labelOf, press, release, button, host, update } =
284
+ await renderToolbar(IOS_18);
285
+ expect(group().querySelector('[ui-glass]')).toBeNull();
286
+ expect(iconOf('Compose')).toContain(PRIMARY);
287
+ expect(labelOf('Select')?.className).toContain('text-primary');
288
+ // A prominent label is bold, in the group too: before iOS 26, a group
289
+ // only keeps items together.
290
+ await update(() => {
291
+ host.icon.set(undefined);
292
+ host.prominent.set(true);
293
+ });
294
+ expect(labelOf('Compose')?.className).toContain('font-semibold');
295
+
296
+ await press('Compose');
297
+ expect(button('Compose').className).toContain('opacity-30');
298
+ expect(highlightOf(button('Compose'))).toBeUndefined();
299
+ await release('Compose');
300
+ expect(button('Compose').className).not.toContain('opacity-30');
301
+ });
302
+
303
+ it('shows a Back item as a chevron and the previous screen’s title', async () => {
304
+ const { host, update, iconOf, labelOf } = await renderToolbar(IOS_18);
305
+ await update(() => host.back.set(true));
306
+ expect(iconOf('Compose')).toContain('m15 18-6-6 6-6');
307
+ expect(labelOf('Compose')?.textContent).toBe('Compose');
308
+ });
309
+ });
310
+
311
+ describe('UiToolbar on Android', () => {
312
+ it('is a Material 3 docked toolbar on the surface container', async () => {
313
+ const { toolbar, state } = await renderToolbar(ANDROID);
314
+ expect(toolbar().classList).toContain('bg-secondary');
315
+ expect(state.bottomBar()?.height()).toBe(64);
316
+ expect(toolbar().querySelector('[ui-glass]')).toBeNull();
317
+ });
318
+
319
+ it('draws icon buttons with a round state layer while pressed', async () => {
320
+ const { iconOf, press, release, button } = await renderToolbar(ANDROID);
321
+ expect(iconOf('Compose')).toContain(MUTED_FOREGROUND);
322
+ expect(button('Compose').className).toContain('h-[48px]');
323
+
324
+ await press('Compose');
325
+ const layer = highlightOf(button('Compose'))!;
326
+ expect(layer.className).toContain('rounded-full');
327
+ expect(layer.className).toContain('bg-foreground');
328
+ await release('Compose');
329
+ expect(highlightOf(button('Compose'))).toBeUndefined();
330
+ });
331
+
332
+ it('draws labels as text buttons, and prominent ones filled', async () => {
333
+ const { host, update, labelOf, button, press, iconOf } =
334
+ await renderToolbar(ANDROID);
335
+ await update(() => host.icon.set(undefined));
336
+ expect(labelOf('Compose')?.className).toContain('text-primary');
337
+ await press('Compose');
338
+ expect(highlightOf(button('Compose'))?.className).toContain('bg-primary');
339
+
340
+ await update(() => host.prominent.set(true));
341
+ expect(button('Compose').className).toContain('bg-primary');
342
+ expect(labelOf('Compose')?.className).toContain('text-primary-foreground');
343
+ expect(highlightOf(button('Compose'))?.className).toContain(
344
+ 'bg-primary-foreground',
345
+ );
346
+
347
+ // A prominent icon is a filled icon button.
348
+ await update(() => host.icon.set('plus'));
349
+ expect(button('Compose').className).toContain('h-[40px]');
350
+ expect(iconOf('Compose')).toContain(PRIMARY_FOREGROUND);
351
+ });
352
+
353
+ it('draws a Back item as the navigation icon, an arrow in the surface’s foreground', async () => {
354
+ const { host, update, iconOf } = await renderToolbar(ANDROID);
355
+ await update(() => host.back.set(true));
356
+ expect(iconOf('Compose')).toContain('M19 12H5');
357
+ expect(iconOf('Compose')).toContain(FOREGROUND);
358
+ });
359
+ });
360
+
361
+ describe('UiToolbar without SystemInfo', () => {
362
+ it('draws the Material design, as where there is no platform to read', async () => {
363
+ vi.stubGlobal('SystemInfo', undefined);
364
+ const result = await render(ToolbarHost, {
365
+ providers: [{ provide: LynxAccessibilitySettings, useValue: settings }],
366
+ });
367
+ const toolbar = result.container.querySelector('#toolbar')!;
368
+ expect(toolbar.classList).toContain('bg-secondary');
369
+ expect(toolbar.querySelector('[ui-glass]')).toBeNull();
370
+ });
371
+ });
372
+
373
+ describe('UiToolbar with glass on Android and the web', () => {
374
+ it('fades the content beneath it itself on Android, whose scroll view takes no mask', async () => {
375
+ const { toolbar } = await renderToolbar(ANDROID, [
376
+ provideGlass({ android: 'glass' }),
377
+ ]);
378
+ const fade = toolbar().style.getPropertyValue('background-image');
379
+ expect(fade).toContain('linear-gradient(to top');
380
+ expect(fade).toContain('var(--background-rgb)');
381
+ });
382
+
383
+ it('highlights an item on Android, whose glass doesn’t react to touch', async () => {
384
+ const { ownGlass, press, release, button } = await renderToolbar(ANDROID, [
385
+ provideGlass({ android: 'glass' }),
386
+ ]);
387
+ expect(ownGlass('Compose')?.getAttribute('blur-effect')).toBe('glass');
388
+ await press('Compose');
389
+ expect(highlightOf(button('Compose'))?.className).toContain(
390
+ 'bg-glass-foreground',
391
+ );
392
+
393
+ // A grouped item too: the group's capsule doesn't react either.
394
+ await press('Checklist');
395
+ expect(highlightOf(button('Checklist'))?.className).toContain(
396
+ 'bg-glass-foreground',
397
+ );
398
+ await release('Checklist');
399
+ expect(highlightOf(button('Checklist'))).toBeUndefined();
400
+ });
401
+
402
+ it('stills the web’s glass under Reduce Motion, and highlights the item instead', async () => {
403
+ const { group, ownGlass, press, button, update } = await renderToolbar(WEB);
404
+ const groupGlass = () => group().querySelector('[ui-glass]')!;
405
+ expect(ownGlass('Compose')?.getAttribute('glass-interactive')).toBe('true');
406
+ expect(groupGlass().getAttribute('glass-interactive')).toBe('true');
407
+
408
+ await press('Checklist');
409
+ expect(highlightOf(button('Checklist'))).toBeUndefined();
410
+
411
+ await update(() => settings.reduceMotion.set(true));
412
+ expect(ownGlass('Compose')?.getAttribute('glass-interactive')).toBeNull();
413
+ expect(groupGlass().getAttribute('glass-interactive')).toBeNull();
414
+ await press('Compose');
415
+ expect(highlightOf(button('Compose'))).toBeDefined();
416
+ await press('Checklist');
417
+ expect(highlightOf(button('Checklist'))).toBeDefined();
418
+ });
419
+
420
+ it('releases a press the scroll took, or the mouse left', async () => {
421
+ const { press, item, button } = await renderToolbar(ANDROID);
422
+ await press('Compose');
423
+ fireEvent.touchcancel(item('Compose'));
424
+ await waitForUpdate();
425
+ expect(highlightOf(button('Compose'))).toBeUndefined();
426
+
427
+ fireLynxEvent(item('Compose'), 'mousedown');
428
+ await waitForUpdate();
429
+ expect(highlightOf(button('Compose'))).toBeDefined();
430
+ fireLynxEvent(item('Compose'), 'mouseleave');
431
+ await waitForUpdate();
432
+ expect(highlightOf(button('Compose'))).toBeUndefined();
433
+ fireLynxEvent(item('Compose'), 'mousedown');
434
+ fireLynxEvent(item('Compose'), 'mouseup');
435
+ await waitForUpdate();
436
+ expect(highlightOf(button('Compose'))).toBeUndefined();
437
+ });
438
+ });