@blotch/dolan 22.2.3 → 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.
- package/dist/commands/add.js +9 -1
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/add.test.js +38 -3
- package/dist/commands/add.test.js.map +1 -1
- package/dist/commands/doctor.js +258 -21
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +425 -11
- package/dist/commands/doctor.test.js.map +1 -1
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.js +38 -18
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/init.test.js +105 -2
- package/dist/commands/init.test.js.map +1 -1
- package/dist/commands/theme.js +1 -1
- package/dist/commands/theme.js.map +1 -1
- package/dist/commands/update.d.ts +7 -0
- package/dist/commands/update.js +194 -50
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/update.test.js +570 -3
- package/dist/commands/update.test.js.map +1 -1
- package/dist/lockfile.d.ts +17 -0
- package/dist/lockfile.js +56 -1
- package/dist/lockfile.js.map +1 -1
- package/dist/lockfile.test.d.ts +1 -0
- package/dist/lockfile.test.js +65 -0
- package/dist/lockfile.test.js.map +1 -0
- package/dist/registry.d.ts +6 -0
- package/dist/registry.js +119 -11
- package/dist/registry.js.map +1 -1
- package/dist/registry.test.js +61 -6
- package/dist/registry.test.js.map +1 -1
- package/dist/test-utils.d.ts +27 -0
- package/dist/test-utils.js +45 -1
- package/dist/test-utils.js.map +1 -1
- package/dist/testing/fire-lynx-event.d.ts +14 -0
- package/dist/testing/fire-lynx-event.js +19 -0
- package/dist/testing/fire-lynx-event.js.map +1 -0
- package/dist/testing/fire-lynx-event.test.d.ts +1 -0
- package/dist/testing/fire-lynx-event.test.js +44 -0
- package/dist/testing/fire-lynx-event.test.js.map +1 -0
- package/dist/testing/lynx-main-thread.d.ts +22 -0
- package/dist/testing/lynx-main-thread.js +11 -0
- package/dist/testing/lynx-main-thread.js.map +1 -0
- package/dist/testing/lynx-main-thread.test.d.ts +1 -0
- package/dist/testing/lynx-main-thread.test.js +24 -0
- package/dist/testing/lynx-main-thread.test.js.map +1 -0
- package/dist/testing/set-input-signal.d.ts +5 -4
- package/dist/testing/set-input-signal.js +5 -4
- package/dist/testing/set-input-signal.js.map +1 -1
- package/dist/ui/boolean-inputs.test.ts +76 -0
- package/dist/ui/components/accordion/accordion.test.ts +158 -144
- package/dist/ui/components/accordion/accordion.ts +19 -14
- package/dist/ui/components/action-sheet/action-sheet.test.ts +637 -183
- package/dist/ui/components/action-sheet/action-sheet.ts +339 -55
- package/dist/ui/components/action-sheet/index.ts +1 -0
- package/dist/ui/components/alert/alert.test.ts +67 -19
- package/dist/ui/components/alert/alert.ts +14 -16
- package/dist/ui/components/alert-dialog/alert-dialog.test.ts +740 -129
- package/dist/ui/components/alert-dialog/alert-dialog.ts +672 -58
- package/dist/ui/components/alert-dialog/index.ts +5 -0
- package/dist/ui/components/area-chart/area-chart.test.ts +81 -1
- package/dist/ui/components/area-chart/area-chart.ts +3 -2
- package/dist/ui/components/avatar/avatar.test.ts +89 -45
- package/dist/ui/components/badge/badge.test.ts +57 -23
- package/dist/ui/components/badge/badge.ts +14 -15
- package/dist/ui/components/bar-chart/bar-chart.test.ts +82 -1
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +1314 -273
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +1046 -119
- package/dist/ui/components/bottom-sheet/index.ts +7 -1
- package/dist/ui/components/button/button.test.ts +646 -2
- package/dist/ui/components/button/button.ts +443 -64
- package/dist/ui/components/button/index.ts +2 -0
- package/dist/ui/components/button-group/button-group.test.ts +528 -2
- package/dist/ui/components/button-group/button-group.ts +428 -70
- package/dist/ui/components/button-group/index.ts +3 -0
- package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +82 -1
- package/dist/ui/components/card/card.test.ts +97 -61
- package/dist/ui/components/card/card.ts +2 -1
- package/dist/ui/components/chart/chart-presets.test.ts +197 -1
- package/dist/ui/components/chart/chart-presets.ts +14 -7
- package/dist/ui/components/chart/chart-theme.ts +12 -0
- package/dist/ui/components/chart/chart.test.ts +1031 -0
- package/dist/ui/components/chart/chart.ts +66 -46
- package/dist/ui/components/checkbox/checkbox.test.ts +124 -42
- package/dist/ui/components/checkbox/checkbox.ts +25 -24
- package/dist/ui/components/collapsible/collapsible.test.ts +158 -81
- package/dist/ui/components/collapsible/collapsible.ts +21 -15
- package/dist/ui/components/context-menu/context-menu.test.ts +224 -0
- package/dist/ui/components/context-menu/context-menu.ts +110 -0
- package/dist/ui/components/context-menu/index.ts +1 -0
- package/dist/ui/components/dialog/dialog.test.ts +210 -152
- package/dist/ui/components/dialog/dialog.ts +35 -29
- package/dist/ui/components/dock/dock-scroll.test.ts +130 -0
- package/dist/ui/components/dock/dock-scroll.ts +142 -0
- package/dist/ui/components/dock/dock-search.test.ts +594 -0
- package/dist/ui/components/dock/dock.test.ts +752 -0
- package/dist/ui/components/dock/dock.ts +1438 -0
- package/dist/ui/components/dock/index.ts +2 -0
- package/dist/ui/components/dynamic-color/dynamic-color.test.ts +144 -0
- package/dist/ui/components/dynamic-color/dynamic-color.ts +146 -0
- package/dist/ui/components/dynamic-color/index.ts +5 -0
- package/dist/ui/components/empty-state/empty-state.test.ts +28 -39
- package/dist/ui/components/glass/concentric-radius.test.ts +13 -0
- package/dist/ui/components/glass/concentric-radius.ts +17 -0
- package/dist/ui/components/glass/glass-design.test.ts +78 -0
- package/dist/ui/components/glass/glass-design.ts +119 -0
- package/dist/ui/components/glass/glass.test.ts +376 -0
- package/dist/ui/components/glass/glass.ts +430 -0
- package/dist/ui/components/glass/index.ts +19 -0
- package/dist/ui/components/icon/icon-theme.test.ts +64 -0
- package/dist/ui/components/icon/icon-theme.ts +70 -16
- package/dist/ui/components/icon/icon.test.ts +52 -41
- package/dist/ui/components/input/input.test.ts +178 -101
- package/dist/ui/components/input/input.ts +17 -13
- package/dist/ui/components/label/label.test.ts +11 -24
- package/dist/ui/components/label/label.ts +8 -2
- package/dist/ui/components/line-chart/line-chart.test.ts +76 -1
- package/dist/ui/components/line-chart/line-chart.ts +3 -2
- package/dist/ui/components/list/list.test.ts +80 -49
- package/dist/ui/components/list/list.ts +5 -4
- package/dist/ui/components/menu/index.ts +25 -0
- package/dist/ui/components/menu/menu-placement.test.ts +153 -0
- package/dist/ui/components/menu/menu-placement.ts +159 -0
- package/dist/ui/components/menu/menu-popup.test.ts +636 -0
- package/dist/ui/components/menu/menu-popup.ts +565 -0
- package/dist/ui/components/menu/menu.test.ts +523 -0
- package/dist/ui/components/menu/menu.ts +455 -0
- package/dist/ui/components/nav-drawer/nav-drawer.test.ts +425 -260
- package/dist/ui/components/nav-drawer/nav-drawer.ts +75 -58
- package/dist/ui/components/navigation-bar/index.ts +1 -0
- package/dist/ui/components/navigation-bar/navigation-bar.test.ts +425 -0
- package/dist/ui/components/navigation-bar/navigation-bar.ts +411 -0
- package/dist/ui/components/pagination/pagination.test.ts +136 -86
- package/dist/ui/components/popover/index.ts +9 -0
- package/dist/ui/components/popover/popover-panel.test.ts +735 -0
- package/dist/ui/components/popover/popover-panel.ts +603 -0
- package/dist/ui/components/popover/popover.test.ts +286 -0
- package/dist/ui/components/popover/popover.ts +207 -0
- package/dist/ui/components/progress/progress.test.ts +102 -46
- package/dist/ui/components/progress/progress.ts +54 -55
- package/dist/ui/components/radio-group/radio-group.test.ts +128 -83
- package/dist/ui/components/radio-group/radio-group.ts +26 -25
- package/dist/ui/components/scatter-chart/scatter-chart.test.ts +74 -1
- package/dist/ui/components/scroll-area/scroll-area.test.ts +72 -36
- package/dist/ui/components/scroll-area/scroll-area.ts +4 -3
- package/dist/ui/components/search-field/index.ts +15 -0
- package/dist/ui/components/search-field/keyboard.test.ts +147 -0
- package/dist/ui/components/search-field/keyboard.ts +123 -0
- package/dist/ui/components/search-field/search-field.test.ts +549 -0
- package/dist/ui/components/search-field/search-field.ts +628 -0
- package/dist/ui/components/segmented-control/index.ts +8 -0
- package/dist/ui/components/segmented-control/segmented-control.test.ts +861 -0
- package/dist/ui/components/segmented-control/segmented-control.ts +884 -0
- package/dist/ui/components/select/select.test.ts +138 -78
- package/dist/ui/components/select/select.ts +19 -12
- package/dist/ui/components/separator/separator.test.ts +18 -17
- package/dist/ui/components/sheet/sheet.test.ts +151 -11
- package/dist/ui/components/sheet/sheet.ts +68 -5
- package/dist/ui/components/skeleton/skeleton.test.ts +24 -7
- package/dist/ui/components/skeleton/skeleton.ts +10 -16
- package/dist/ui/components/slider/index.ts +1 -0
- package/dist/ui/components/slider/slider.test.ts +695 -0
- package/dist/ui/components/slider/slider.ts +901 -0
- package/dist/ui/components/spinner/spinner.test.ts +64 -46
- package/dist/ui/components/spinner/spinner.ts +16 -19
- package/dist/ui/components/switch/index.ts +1 -1
- package/dist/ui/components/switch/switch.test.ts +534 -48
- package/dist/ui/components/switch/switch.ts +613 -48
- package/dist/ui/components/tab-bar/index.ts +1 -0
- package/dist/ui/components/tab-bar/tab-bar.test.ts +280 -0
- package/dist/ui/components/tab-bar/tab-bar.ts +230 -0
- package/dist/ui/components/tabs/index.ts +7 -1
- package/dist/ui/components/tabs/tabs.test.ts +484 -150
- package/dist/ui/components/tabs/tabs.ts +344 -149
- package/dist/ui/components/textarea/textarea.test.ts +206 -100
- package/dist/ui/components/textarea/textarea.ts +16 -13
- package/dist/ui/components/toast/toast.test.ts +434 -350
- package/dist/ui/components/toast/toast.ts +42 -51
- package/dist/ui/components/toggle/index.ts +1 -0
- package/dist/ui/components/toggle/toggle.test.ts +559 -146
- package/dist/ui/components/toggle/toggle.ts +241 -26
- package/dist/ui/components/toolbar/bar-scroll.test.ts +310 -0
- package/dist/ui/components/toolbar/bar-scroll.ts +372 -0
- package/dist/ui/components/toolbar/index.ts +12 -0
- package/dist/ui/components/toolbar/toolbar.test.ts +438 -0
- package/dist/ui/components/toolbar/toolbar.ts +535 -0
- package/dist/ui/theme/chart-colors.test.ts +8 -3
- package/dist/ui/theme/dark.css +38 -2
- package/dist/ui/theme/default.css +55 -7
- package/dist/ui/theme/icon-colors.test.ts +8 -3
- package/dist/ui/theme/rgb-channels.test.ts +84 -0
- package/dist/ui/theme/tailwind-build.test.ts +114 -0
- package/dist/ui/theme/tailwind-css.test.ts +83 -0
- package/dist/ui/theme/tailwind-plugin.test.ts +42 -2
- package/dist/ui/theme/tailwind-plugin.ts +93 -29
- package/dist/ui/theme/tailwind.css +113 -0
- package/dist/ui/utils/animate.test.ts +13 -1
- package/dist/utils/analyze.d.ts +19 -0
- package/dist/utils/analyze.js +16 -0
- package/dist/utils/analyze.js.map +1 -1
- package/dist/utils/analyze.test.js +14 -1
- package/dist/utils/analyze.test.js.map +1 -1
- package/dist/utils/installed.d.ts +25 -1
- package/dist/utils/installed.js +33 -0
- package/dist/utils/installed.js.map +1 -1
- package/dist/utils/installed.test.js +55 -1
- package/dist/utils/installed.test.js.map +1 -1
- package/dist/utils/resolve-paths.d.ts +4 -4
- package/dist/utils/resolve-paths.js +5 -7
- package/dist/utils/resolve-paths.js.map +1 -1
- package/dist/utils/resolve-paths.test.js +9 -5
- package/dist/utils/resolve-paths.test.js.map +1 -1
- package/dist/utils/tailwind.d.ts +45 -0
- package/dist/utils/tailwind.js +90 -0
- package/dist/utils/tailwind.js.map +1 -0
- package/dist/utils/tailwind.test.d.ts +1 -0
- package/dist/utils/tailwind.test.js +134 -0
- package/dist/utils/tailwind.test.js.map +1 -0
- package/package.json +9 -1
- package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -63
- package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -58
- package/dist/ui/components/button/button-render.test.ts +0 -132
- package/dist/ui/components/button-group/button-group-render.test.ts +0 -146
- package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +0 -59
- package/dist/ui/components/chart/chart-render.test.ts +0 -730
- package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -83
- package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -64
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import {
|
|
3
|
+
fireEvent,
|
|
4
|
+
render,
|
|
5
|
+
waitForUpdate,
|
|
6
|
+
within,
|
|
7
|
+
} from '@blotch/angular-lynx-testing-library';
|
|
8
|
+
import { LYNX_ELEMENTS, LynxAccessibilitySettings } from '@blotch/angular-lynx';
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
10
|
+
import { fireLynxEvent } from '../../../test-utils/fire-lynx-event';
|
|
11
|
+
import { UiGlass } from '../glass/glass';
|
|
12
|
+
import {
|
|
13
|
+
UiMenu,
|
|
14
|
+
UiMenuItem,
|
|
15
|
+
UiMenuSection,
|
|
16
|
+
UiMenuSeparator,
|
|
17
|
+
UiMenuTrigger,
|
|
18
|
+
} from './menu';
|
|
19
|
+
|
|
20
|
+
@Component({
|
|
21
|
+
imports: [
|
|
22
|
+
LYNX_ELEMENTS,
|
|
23
|
+
UiGlass,
|
|
24
|
+
UiMenu,
|
|
25
|
+
UiMenuTrigger,
|
|
26
|
+
UiMenuItem,
|
|
27
|
+
UiMenuSection,
|
|
28
|
+
UiMenuSeparator,
|
|
29
|
+
],
|
|
30
|
+
template: `
|
|
31
|
+
<ui-menu
|
|
32
|
+
[open]="open()"
|
|
33
|
+
(openChange)="open.set($event)"
|
|
34
|
+
(closed)="closed()"
|
|
35
|
+
>
|
|
36
|
+
@if (glassTrigger()) {
|
|
37
|
+
<blur-view
|
|
38
|
+
ui-glass
|
|
39
|
+
uiMenuTrigger
|
|
40
|
+
class="trigger"
|
|
41
|
+
[accessibility-element]="true"
|
|
42
|
+
accessibility-traits="button"
|
|
43
|
+
accessibility-label="Sort"
|
|
44
|
+
>
|
|
45
|
+
<text>Sort</text>
|
|
46
|
+
</blur-view>
|
|
47
|
+
} @else if (withTrigger()) {
|
|
48
|
+
<view
|
|
49
|
+
uiMenuTrigger
|
|
50
|
+
class="trigger"
|
|
51
|
+
[accessibility-element]="true"
|
|
52
|
+
accessibility-traits="button"
|
|
53
|
+
accessibility-label="Sort"
|
|
54
|
+
>
|
|
55
|
+
<text>Sort</text>
|
|
56
|
+
</view>
|
|
57
|
+
}
|
|
58
|
+
<ui-menu-section title="Sort by">
|
|
59
|
+
<ui-menu-item
|
|
60
|
+
label="Name"
|
|
61
|
+
[checked]="sort() === 'name'"
|
|
62
|
+
(pressed)="sort.set('name')"
|
|
63
|
+
/>
|
|
64
|
+
<ui-menu-item
|
|
65
|
+
label="Date"
|
|
66
|
+
[checked]="sort() === 'date'"
|
|
67
|
+
(pressed)="sort.set('date')"
|
|
68
|
+
/>
|
|
69
|
+
</ui-menu-section>
|
|
70
|
+
<ui-menu-separator />
|
|
71
|
+
<ui-menu-section>
|
|
72
|
+
<ui-menu-item label="Share" icon="upload" (pressed)="shared()" />
|
|
73
|
+
<ui-menu-item
|
|
74
|
+
label="Delete"
|
|
75
|
+
icon="trash"
|
|
76
|
+
variant="destructive"
|
|
77
|
+
(pressed)="deleted()"
|
|
78
|
+
/>
|
|
79
|
+
<ui-menu-item
|
|
80
|
+
label="Archive"
|
|
81
|
+
accessibilityLabel="Archive, unavailable offline"
|
|
82
|
+
disabled
|
|
83
|
+
(pressed)="archived()"
|
|
84
|
+
/>
|
|
85
|
+
<ui-menu-item label="Bold" checked />
|
|
86
|
+
<ui-menu-item label="Pin" [checked]="pinned()" />
|
|
87
|
+
</ui-menu-section>
|
|
88
|
+
</ui-menu>
|
|
89
|
+
`,
|
|
90
|
+
})
|
|
91
|
+
class MenuHost {
|
|
92
|
+
readonly open = signal(false);
|
|
93
|
+
readonly withTrigger = signal(true);
|
|
94
|
+
readonly glassTrigger = signal(false);
|
|
95
|
+
readonly sort = signal<'name' | 'date'>('date');
|
|
96
|
+
readonly pinned = signal<boolean | undefined>(undefined);
|
|
97
|
+
readonly closed = vi.fn();
|
|
98
|
+
readonly shared = vi.fn();
|
|
99
|
+
readonly deleted = vi.fn();
|
|
100
|
+
readonly archived = vi.fn();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The Lynx function that runs an element's UI methods, such as measuring it
|
|
105
|
+
* (`boundingClientRect`). The testing library defines it on the global
|
|
106
|
+
* object.
|
|
107
|
+
*/
|
|
108
|
+
type InvokeUIMethod = {
|
|
109
|
+
__InvokeUIMethod: (
|
|
110
|
+
element: Element,
|
|
111
|
+
method: string,
|
|
112
|
+
params: Record<string, unknown>,
|
|
113
|
+
callback: (result: { code: number; data?: unknown }) => void,
|
|
114
|
+
) => void;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** Accessibility settings a test can change, as the host app would. */
|
|
118
|
+
const settings = {
|
|
119
|
+
reduceMotion: signal(false),
|
|
120
|
+
reduceTransparency: signal(false),
|
|
121
|
+
increaseContrast: signal(false),
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const IOS_26 = { platform: 'iOS', osVersion: '26.0' };
|
|
125
|
+
const IOS_18 = { platform: 'iOS', osVersion: '18.5' };
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Renders the menu on a platform (none for Material), measuring elements as
|
|
129
|
+
* `boundingClientRect` would on a phone: the safe area (the element that
|
|
130
|
+
* holds the menu) a phone's screen, the menu's content 120pt tall, and
|
|
131
|
+
* anything else, the trigger, a small box near the top.
|
|
132
|
+
*/
|
|
133
|
+
const renderMenu = async (
|
|
134
|
+
platform: { platform: string; osVersion: string } | null = IOS_26,
|
|
135
|
+
) => {
|
|
136
|
+
vi.stubGlobal('SystemInfo', platform ?? undefined);
|
|
137
|
+
const invoke = vi
|
|
138
|
+
.spyOn(globalThis as unknown as InvokeUIMethod, '__InvokeUIMethod')
|
|
139
|
+
.mockImplementation((element, method, _params, callback) => {
|
|
140
|
+
if (method !== 'boundingClientRect') {
|
|
141
|
+
callback({ code: 0 });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const data = element.querySelector(':scope > blur-view[ui-glass]')
|
|
145
|
+
? { left: 0, top: 0, width: 402, height: 800 }
|
|
146
|
+
: element.parentElement?.tagName.toLowerCase() === 'scroll-view'
|
|
147
|
+
? { left: 0, top: 0, width: 250, height: 120 }
|
|
148
|
+
: { left: 16, top: 80, width: 80, height: 44 };
|
|
149
|
+
callback({ code: 0, data });
|
|
150
|
+
});
|
|
151
|
+
const result = await render(MenuHost, {
|
|
152
|
+
providers: [{ provide: LynxAccessibilitySettings, useValue: settings }],
|
|
153
|
+
});
|
|
154
|
+
const host = result.componentRef.instance as MenuHost;
|
|
155
|
+
const overlay = result.container.querySelector('overlay')!;
|
|
156
|
+
return {
|
|
157
|
+
...result,
|
|
158
|
+
host,
|
|
159
|
+
invoke,
|
|
160
|
+
overlay,
|
|
161
|
+
menu: within(overlay as HTMLElement),
|
|
162
|
+
trigger: () => result.container.querySelector('.trigger') as HTMLElement,
|
|
163
|
+
panel: () => overlay.querySelector('blur-view[ui-glass]')!,
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
type Rendered = Awaited<ReturnType<typeof renderMenu>>;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Waits until the menu has opened.
|
|
171
|
+
*/
|
|
172
|
+
const waitForOpen = (rendered: Rendered) =>
|
|
173
|
+
vi.waitFor(() =>
|
|
174
|
+
expect(rendered.panel().getAttribute('accessibility-exclusive-focus')).toBe(
|
|
175
|
+
'true',
|
|
176
|
+
),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Renders the menu and opens it: a screen reader, and the queries, reach
|
|
181
|
+
* its items only while it shows.
|
|
182
|
+
*/
|
|
183
|
+
const renderOpenMenu = async (
|
|
184
|
+
platform: { platform: string; osVersion: string } | null = IOS_26,
|
|
185
|
+
) => {
|
|
186
|
+
const rendered = await renderMenu(platform);
|
|
187
|
+
rendered.host.open.set(true);
|
|
188
|
+
await waitForOpen(rendered);
|
|
189
|
+
return rendered;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Whether `first` comes before `second` in the document.
|
|
194
|
+
*/
|
|
195
|
+
const precedes = (first: Node, second: Node) =>
|
|
196
|
+
!!(first.compareDocumentPosition(second) & Node.DOCUMENT_POSITION_FOLLOWING);
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Waits until the menu has closed and its animation has finished.
|
|
200
|
+
*/
|
|
201
|
+
const waitForClose = async (rendered: Rendered) => {
|
|
202
|
+
await vi.waitFor(() => expect(rendered.host.closed).toHaveBeenCalled());
|
|
203
|
+
await waitForUpdate();
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const style = (element: Element, name: string) =>
|
|
207
|
+
(element as HTMLElement).style.getPropertyValue(name);
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* An item's row: the element a screen reader reads as a button.
|
|
211
|
+
*/
|
|
212
|
+
const row = (rendered: Rendered, name: string | RegExp) =>
|
|
213
|
+
rendered.menu.getByRole('button', { name });
|
|
214
|
+
|
|
215
|
+
// The renderer warns that the components' selectors aren't Lynx elements.
|
|
216
|
+
beforeEach(() => {
|
|
217
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
afterEach(() => {
|
|
221
|
+
vi.restoreAllMocks();
|
|
222
|
+
vi.unstubAllGlobals();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe('UiMenu', () => {
|
|
226
|
+
it('opens from its trigger and grows out of it, hiding it meanwhile', async () => {
|
|
227
|
+
const rendered = await renderMenu();
|
|
228
|
+
const { host, trigger, overlay, invoke } = rendered;
|
|
229
|
+
expect(overlay.getAttribute('visible')).toBe('false');
|
|
230
|
+
|
|
231
|
+
fireEvent.tap(trigger());
|
|
232
|
+
await waitForOpen(rendered);
|
|
233
|
+
|
|
234
|
+
expect(host.open()).toBe(true);
|
|
235
|
+
expect(overlay.getAttribute('visible')).toBe('true');
|
|
236
|
+
// Measured from the trigger.
|
|
237
|
+
expect(invoke).toHaveBeenCalledWith(
|
|
238
|
+
trigger(),
|
|
239
|
+
'boundingClientRect',
|
|
240
|
+
{ relativeTo: 'screen' },
|
|
241
|
+
expect.any(Function),
|
|
242
|
+
);
|
|
243
|
+
expect(style(trigger(), 'visibility')).toBe('hidden');
|
|
244
|
+
|
|
245
|
+
// A tap on the trigger again closes it, and the trigger comes back.
|
|
246
|
+
fireEvent.tap(trigger());
|
|
247
|
+
await waitForClose(rendered);
|
|
248
|
+
expect(host.open()).toBe(false);
|
|
249
|
+
expect(style(trigger(), 'visibility')).toBe('');
|
|
250
|
+
// A trigger that isn't glass has no glass to hand over.
|
|
251
|
+
expect(trigger().hasAttribute('glass-effect-id')).toBe(false);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("takes a glass trigger's glass over, shown as the trigger hides", async () => {
|
|
255
|
+
const rendered = await renderMenu();
|
|
256
|
+
rendered.host.glassTrigger.set(true);
|
|
257
|
+
await waitForUpdate();
|
|
258
|
+
const { trigger, panel } = rendered;
|
|
259
|
+
|
|
260
|
+
// The trigger and the menu share an id of the menu's own, for the shell
|
|
261
|
+
// to pair the two across the change.
|
|
262
|
+
const id = trigger().getAttribute('glass-effect-id');
|
|
263
|
+
expect(id).toMatch(/^ui-menu-\d+$/);
|
|
264
|
+
expect(panel().getAttribute('glass-effect-id')).toBe(id);
|
|
265
|
+
expect(style(panel(), 'visibility')).toBe('hidden');
|
|
266
|
+
|
|
267
|
+
fireEvent.tap(trigger());
|
|
268
|
+
await waitForOpen(rendered);
|
|
269
|
+
expect(style(panel(), 'visibility')).toBe('');
|
|
270
|
+
expect(style(trigger(), 'visibility')).toBe('hidden');
|
|
271
|
+
|
|
272
|
+
fireEvent.tap(trigger());
|
|
273
|
+
await waitForClose(rendered);
|
|
274
|
+
expect(style(panel(), 'visibility')).toBe('hidden');
|
|
275
|
+
expect(style(trigger(), 'visibility')).toBe('');
|
|
276
|
+
|
|
277
|
+
// A trigger that goes takes its glass with it.
|
|
278
|
+
rendered.host.glassTrigger.set(false);
|
|
279
|
+
await waitForUpdate();
|
|
280
|
+
expect(trigger().tagName.toLowerCase()).toBe('view');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('closes when an item is chosen, after telling it', async () => {
|
|
284
|
+
const rendered = await renderMenu();
|
|
285
|
+
rendered.host.open.set(true);
|
|
286
|
+
await waitForOpen(rendered);
|
|
287
|
+
|
|
288
|
+
fireEvent.tap(row(rendered, 'Share'));
|
|
289
|
+
|
|
290
|
+
expect(rendered.host.shared).toHaveBeenCalledTimes(1);
|
|
291
|
+
expect(rendered.host.open()).toBe(false);
|
|
292
|
+
await waitForClose(rendered);
|
|
293
|
+
expect(rendered.overlay.getAttribute('visible')).toBe('false');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('closes on a tap outside it', async () => {
|
|
297
|
+
const rendered = await renderMenu();
|
|
298
|
+
rendered.host.open.set(true);
|
|
299
|
+
await waitForOpen(rendered);
|
|
300
|
+
|
|
301
|
+
fireEvent.tap(rendered.overlay.firstElementChild!);
|
|
302
|
+
|
|
303
|
+
expect(rendered.host.open()).toBe(false);
|
|
304
|
+
await waitForClose(rendered);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('grows out of the menu itself without a marked trigger', async () => {
|
|
308
|
+
const rendered = await renderMenu();
|
|
309
|
+
rendered.host.withTrigger.set(false);
|
|
310
|
+
await waitForUpdate();
|
|
311
|
+
|
|
312
|
+
rendered.host.open.set(true);
|
|
313
|
+
await waitForOpen(rendered);
|
|
314
|
+
|
|
315
|
+
// The menu's own element holds the overlay's element.
|
|
316
|
+
expect(rendered.invoke).toHaveBeenCalledWith(
|
|
317
|
+
rendered.overlay.parentElement!.parentElement,
|
|
318
|
+
'boundingClientRect',
|
|
319
|
+
{ relativeTo: 'screen' },
|
|
320
|
+
expect.any(Function),
|
|
321
|
+
);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('opens beside its trigger on Material, leaving it in view', async () => {
|
|
325
|
+
const rendered = await renderMenu(null);
|
|
326
|
+
|
|
327
|
+
fireEvent.tap(rendered.trigger());
|
|
328
|
+
await waitForOpen(rendered);
|
|
329
|
+
|
|
330
|
+
expect(style(rendered.trigger(), 'visibility')).toBe('');
|
|
331
|
+
// Below the trigger, a gap away.
|
|
332
|
+
expect(style(rendered.panel(), 'top')).toBe(`${80 + 44 + 8}px`);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe('UiMenuItem', () => {
|
|
337
|
+
it('is a button named by its label, and says when it is destructive', async () => {
|
|
338
|
+
const rendered = await renderOpenMenu();
|
|
339
|
+
|
|
340
|
+
expect(row(rendered, 'Share')).toBeTruthy();
|
|
341
|
+
expect(row(rendered, 'Delete, destructive')).toBeTruthy();
|
|
342
|
+
expect(row(rendered, 'Archive, unavailable offline')).toBeTruthy();
|
|
343
|
+
expect(
|
|
344
|
+
within(row(rendered, 'Delete, destructive')).getByText('Delete')
|
|
345
|
+
.className,
|
|
346
|
+
).toContain('text-destructive');
|
|
347
|
+
expect(
|
|
348
|
+
within(row(rendered, 'Share')).getByText('Share').className,
|
|
349
|
+
).toContain('text-glass-foreground');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('shows a checkmark, and is selected to a screen reader, when checked', async () => {
|
|
353
|
+
const rendered = await renderOpenMenu();
|
|
354
|
+
const date = row(rendered, 'Date');
|
|
355
|
+
const name = row(rendered, 'Name');
|
|
356
|
+
|
|
357
|
+
expect(date.getAttribute('accessibility-traits')).toBe('button,selected');
|
|
358
|
+
expect(name.getAttribute('accessibility-traits')).toBe('button');
|
|
359
|
+
// Both keep the checkmark's column, so the labels line up.
|
|
360
|
+
expect(date.querySelector('.h-5.w-5 svg')).not.toBeNull();
|
|
361
|
+
expect(name.querySelector('.h-5.w-5')).not.toBeNull();
|
|
362
|
+
expect(name.querySelector('.h-5.w-5 svg')).toBeNull();
|
|
363
|
+
|
|
364
|
+
fireEvent.tap(name);
|
|
365
|
+
await waitForUpdate();
|
|
366
|
+
expect(rendered.host.sort()).toBe('name');
|
|
367
|
+
expect(name.getAttribute('accessibility-traits')).toBe('button,selected');
|
|
368
|
+
expect(date.getAttribute('accessibility-traits')).toBe('button');
|
|
369
|
+
await waitForClose(rendered);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('is checked by a bare checked attribute, and unset when bound to undefined', async () => {
|
|
373
|
+
const rendered = await renderOpenMenu();
|
|
374
|
+
const bold = row(rendered, 'Bold');
|
|
375
|
+
const pin = row(rendered, 'Pin');
|
|
376
|
+
|
|
377
|
+
expect(bold.getAttribute('accessibility-traits')).toBe('button,selected');
|
|
378
|
+
expect(bold.querySelector('.h-5.w-5 svg')).not.toBeNull();
|
|
379
|
+
// Unset, it's an item that's never checked: no checkmark column.
|
|
380
|
+
expect(pin.querySelector('.h-5.w-5')).toBeNull();
|
|
381
|
+
|
|
382
|
+
rendered.host.pinned.set(false);
|
|
383
|
+
await waitForUpdate();
|
|
384
|
+
expect(pin.querySelector('.h-5.w-5')).not.toBeNull();
|
|
385
|
+
expect(pin.querySelector('.h-5.w-5 svg')).toBeNull();
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('highlights while pressed, by touch or mouse', async () => {
|
|
389
|
+
const rendered = await renderOpenMenu();
|
|
390
|
+
const share = row(rendered, 'Share');
|
|
391
|
+
const highlight = share.firstElementChild!;
|
|
392
|
+
expect(style(highlight, 'opacity')).toBe('0');
|
|
393
|
+
|
|
394
|
+
for (const [down, up] of [
|
|
395
|
+
['touchstart', 'touchend'],
|
|
396
|
+
['touchstart', 'touchcancel'],
|
|
397
|
+
['mousedown', 'mouseup'],
|
|
398
|
+
['mousedown', 'mouseleave'],
|
|
399
|
+
]) {
|
|
400
|
+
fireLynxEvent(share, down!);
|
|
401
|
+
await waitForUpdate();
|
|
402
|
+
expect(style(highlight, 'opacity')).toBe('0.08');
|
|
403
|
+
fireLynxEvent(share, up!);
|
|
404
|
+
await waitForUpdate();
|
|
405
|
+
expect(style(highlight, 'opacity')).toBe('0');
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('highlights a pressed row as its platform does', async () => {
|
|
410
|
+
const pressed = async (
|
|
411
|
+
platform: { platform: string; osVersion: string } | null,
|
|
412
|
+
) => {
|
|
413
|
+
const rendered = await renderOpenMenu(platform);
|
|
414
|
+
const share = row(rendered, 'Share');
|
|
415
|
+
fireLynxEvent(share, 'touchstart');
|
|
416
|
+
await waitForUpdate();
|
|
417
|
+
const highlight = share.firstElementChild!;
|
|
418
|
+
return {
|
|
419
|
+
classes: highlight.className.split(' '),
|
|
420
|
+
opacity: style(highlight, 'opacity'),
|
|
421
|
+
};
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
// iOS 26's capsule, inset from the menu's sides.
|
|
425
|
+
const glass = await pressed(IOS_26);
|
|
426
|
+
expect(glass.classes).toEqual(
|
|
427
|
+
expect.arrayContaining(['left-[10px]', 'right-[10px]', 'rounded-full']),
|
|
428
|
+
);
|
|
429
|
+
expect(glass.opacity).toBe('0.08');
|
|
430
|
+
// Edge to edge before iOS 26, and as Material's state layer.
|
|
431
|
+
for (const platform of [IOS_18, null]) {
|
|
432
|
+
const { classes, opacity } = await pressed(platform);
|
|
433
|
+
expect(classes).toEqual(expect.arrayContaining(['left-0', 'right-0']));
|
|
434
|
+
expect(classes).not.toContain('rounded-full');
|
|
435
|
+
expect(opacity).toBe('0.1');
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it("can't be pressed or chosen when disabled", async () => {
|
|
440
|
+
const rendered = await renderOpenMenu();
|
|
441
|
+
const archive = row(rendered, 'Archive, unavailable offline');
|
|
442
|
+
|
|
443
|
+
expect(archive.className).toContain('opacity-40');
|
|
444
|
+
fireEvent.touchstart(archive);
|
|
445
|
+
await waitForUpdate();
|
|
446
|
+
expect(style(archive.firstElementChild!, 'opacity')).toBe('0');
|
|
447
|
+
fireEvent.tap(archive);
|
|
448
|
+
|
|
449
|
+
expect(rendered.host.archived).not.toHaveBeenCalled();
|
|
450
|
+
expect(rendered.host.open()).toBe(true);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('lays out as iOS 26 does: 44pt rows with the icon first', async () => {
|
|
454
|
+
const rendered = await renderOpenMenu();
|
|
455
|
+
const share = row(rendered, 'Share');
|
|
456
|
+
const label = within(share).getByText('Share');
|
|
457
|
+
|
|
458
|
+
expect(share.className).toContain('min-h-[44px]');
|
|
459
|
+
expect(label.className).toContain('text-[17px]');
|
|
460
|
+
expect(precedes(share.querySelector('svg')!, label)).toBe(true);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('puts the icon after the label before iOS 26', async () => {
|
|
464
|
+
const rendered = await renderOpenMenu(IOS_18);
|
|
465
|
+
const share = row(rendered, 'Share');
|
|
466
|
+
|
|
467
|
+
expect(
|
|
468
|
+
precedes(within(share).getByText('Share'), share.querySelector('svg')!),
|
|
469
|
+
).toBe(true);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('lays out as Material 3 does: 48dp rows with a leading icon', async () => {
|
|
473
|
+
const rendered = await renderOpenMenu(null);
|
|
474
|
+
const share = row(rendered, 'Share');
|
|
475
|
+
const label = within(share).getByText('Share');
|
|
476
|
+
|
|
477
|
+
expect(share.className).toContain('min-h-[48px]');
|
|
478
|
+
expect(label.className).toContain('text-[16px]');
|
|
479
|
+
expect(precedes(share.querySelector('svg')!, label)).toBe(true);
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
describe('UiMenuSection and UiMenuSeparator', () => {
|
|
484
|
+
it('titles a section, as a heading', async () => {
|
|
485
|
+
const rendered = await renderOpenMenu();
|
|
486
|
+
|
|
487
|
+
const title = rendered.menu.getByRole('heading', { name: 'Sort by' });
|
|
488
|
+
expect(title.className).toContain('px-4');
|
|
489
|
+
// The untitled section has no heading.
|
|
490
|
+
expect(rendered.menu.getAllByRole('heading')).toHaveLength(1);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('indents a Material section title as Material does', async () => {
|
|
494
|
+
const rendered = await renderOpenMenu(null);
|
|
495
|
+
|
|
496
|
+
expect(
|
|
497
|
+
rendered.menu.getByRole('heading', { name: 'Sort by' }).className,
|
|
498
|
+
).toContain('px-3');
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('parts sections with a line, or a band before iOS 26', async () => {
|
|
502
|
+
const separatorLine = async (
|
|
503
|
+
platform: { platform: string; osVersion: string } | null,
|
|
504
|
+
) => {
|
|
505
|
+
const rendered = await renderMenu(platform);
|
|
506
|
+
const line = rendered.overlay.querySelector(
|
|
507
|
+
'[class~="h-px"], [class~="h-2"]',
|
|
508
|
+
)!.className;
|
|
509
|
+
rendered.unmount();
|
|
510
|
+
return line;
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
expect((await separatorLine(IOS_26)).split(' ')).toEqual(
|
|
514
|
+
expect.arrayContaining(['h-px', 'bg-glass-foreground', 'opacity-20']),
|
|
515
|
+
);
|
|
516
|
+
expect((await separatorLine(IOS_18)).split(' ')).toEqual(
|
|
517
|
+
expect.arrayContaining(['h-2', 'bg-glass-foreground', 'opacity-10']),
|
|
518
|
+
);
|
|
519
|
+
expect((await separatorLine(null)).split(' ')).toEqual(
|
|
520
|
+
expect.arrayContaining(['h-px', 'bg-border']),
|
|
521
|
+
);
|
|
522
|
+
});
|
|
523
|
+
});
|