@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
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
import { Component, signal } from '@angular/core';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
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 {
|
|
9
|
+
type MockInstance,
|
|
10
|
+
afterEach,
|
|
11
|
+
beforeEach,
|
|
12
|
+
describe,
|
|
13
|
+
expect,
|
|
14
|
+
it,
|
|
15
|
+
vi,
|
|
16
|
+
} from 'vitest';
|
|
5
17
|
import { DURATION } from '../../utils/animate';
|
|
6
|
-
import {
|
|
18
|
+
import { fireLynxEvent } from '../../../test-utils/fire-lynx-event';
|
|
19
|
+
import { UiGlass } from '../glass/glass';
|
|
20
|
+
import {
|
|
21
|
+
type LynxMainThread,
|
|
22
|
+
lynxMainThread,
|
|
23
|
+
} from '../../../test-utils/lynx-main-thread';
|
|
7
24
|
import {
|
|
8
25
|
UiActionSheet,
|
|
26
|
+
UiActionSheetAnchor,
|
|
9
27
|
UiActionSheetCancel,
|
|
10
28
|
UiActionSheetItem,
|
|
11
29
|
UiActionSheetTitle,
|
|
@@ -20,239 +38,675 @@ import {
|
|
|
20
38
|
LYNX_ELEMENTS,
|
|
21
39
|
],
|
|
22
40
|
template: `
|
|
23
|
-
<ui-action-sheet [(open)]="open">
|
|
24
|
-
<ui-action-sheet-title>Pick</ui-action-sheet-title>
|
|
25
|
-
<ui-action-sheet-item>
|
|
26
|
-
<ui-action-sheet-
|
|
41
|
+
<ui-action-sheet [(open)]="open" class="pb-8" (closed)="closed()">
|
|
42
|
+
<ui-action-sheet-title class="py-4">Pick</ui-action-sheet-title>
|
|
43
|
+
<ui-action-sheet-item (pressed)="shared()">Share</ui-action-sheet-item>
|
|
44
|
+
<ui-action-sheet-item
|
|
45
|
+
variant="destructive"
|
|
46
|
+
class="bg-muted"
|
|
47
|
+
(pressed)="deleted()"
|
|
48
|
+
>
|
|
49
|
+
Delete
|
|
50
|
+
</ui-action-sheet-item>
|
|
51
|
+
<ui-action-sheet-cancel class="mt-2" />
|
|
27
52
|
</ui-action-sheet>
|
|
28
53
|
`,
|
|
29
54
|
})
|
|
30
55
|
class ActionSheetHost {
|
|
31
|
-
open =
|
|
56
|
+
readonly open = signal(false);
|
|
57
|
+
readonly closed = vi.fn();
|
|
58
|
+
readonly shared = vi.fn();
|
|
59
|
+
readonly deleted = vi.fn();
|
|
32
60
|
}
|
|
33
61
|
|
|
62
|
+
/**
|
|
63
|
+
* A cancel button given its own label.
|
|
64
|
+
*/
|
|
65
|
+
@Component({
|
|
66
|
+
imports: [UiActionSheet, UiActionSheetCancel],
|
|
67
|
+
template: `
|
|
68
|
+
<ui-action-sheet>
|
|
69
|
+
<ui-action-sheet-cancel label="Dismiss" />
|
|
70
|
+
</ui-action-sheet>
|
|
71
|
+
`,
|
|
72
|
+
})
|
|
73
|
+
class CancelLabelHost {}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* An action sheet opened from the button that presents it.
|
|
77
|
+
*/
|
|
78
|
+
@Component({
|
|
79
|
+
imports: [
|
|
80
|
+
UiActionSheet,
|
|
81
|
+
UiActionSheetAnchor,
|
|
82
|
+
UiActionSheetTitle,
|
|
83
|
+
UiActionSheetItem,
|
|
84
|
+
UiActionSheetCancel,
|
|
85
|
+
UiGlass,
|
|
86
|
+
LYNX_ELEMENTS,
|
|
87
|
+
],
|
|
88
|
+
template: `
|
|
89
|
+
@if (glassAnchor()) {
|
|
90
|
+
<blur-view
|
|
91
|
+
ui-glass
|
|
92
|
+
class="share"
|
|
93
|
+
[uiActionSheetAnchor]="sheet"
|
|
94
|
+
(bindtap)="open.set(true)"
|
|
95
|
+
>
|
|
96
|
+
<text>Share</text>
|
|
97
|
+
</blur-view>
|
|
98
|
+
} @else if (anchored()) {
|
|
99
|
+
<view
|
|
100
|
+
class="share"
|
|
101
|
+
[uiActionSheetAnchor]="sheet"
|
|
102
|
+
(bindtap)="open.set(true)"
|
|
103
|
+
>
|
|
104
|
+
<text>Share</text>
|
|
105
|
+
</view>
|
|
106
|
+
}
|
|
107
|
+
<ui-action-sheet #sheet [(open)]="open" (closed)="closed()">
|
|
108
|
+
<ui-action-sheet-title>Delete this draft?</ui-action-sheet-title>
|
|
109
|
+
<ui-action-sheet-item (pressed)="saved()"
|
|
110
|
+
>Save Draft</ui-action-sheet-item
|
|
111
|
+
>
|
|
112
|
+
<ui-action-sheet-item variant="destructive" (pressed)="deleted()">
|
|
113
|
+
Delete Draft
|
|
114
|
+
</ui-action-sheet-item>
|
|
115
|
+
<ui-action-sheet-cancel />
|
|
116
|
+
</ui-action-sheet>
|
|
117
|
+
`,
|
|
118
|
+
})
|
|
119
|
+
class AnchoredHost {
|
|
120
|
+
readonly open = signal(false);
|
|
121
|
+
readonly anchored = signal(true);
|
|
122
|
+
readonly glassAnchor = signal(false);
|
|
123
|
+
readonly closed = vi.fn();
|
|
124
|
+
readonly saved = vi.fn();
|
|
125
|
+
readonly deleted = vi.fn();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A title on its own, outside any action sheet.
|
|
130
|
+
*/
|
|
131
|
+
@Component({
|
|
132
|
+
imports: [UiActionSheetTitle],
|
|
133
|
+
template: `<ui-action-sheet-title>Alone</ui-action-sheet-title>`,
|
|
134
|
+
})
|
|
135
|
+
class LoneTitleHost {}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The Lynx function an element's `invoke()` calls to run one of its UI
|
|
139
|
+
* methods, such as moving a screen reader's focus. The testing library
|
|
140
|
+
* defines it on the global object.
|
|
141
|
+
*/
|
|
142
|
+
type InvokeUIMethod = {
|
|
143
|
+
__InvokeUIMethod: (
|
|
144
|
+
element: Element,
|
|
145
|
+
method: string,
|
|
146
|
+
params: Record<string, unknown>,
|
|
147
|
+
callback: (result: { code: number }) => void,
|
|
148
|
+
) => void;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/** `__ElementAnimate`'s first argument for starting an animation. */
|
|
152
|
+
const START = 0;
|
|
153
|
+
|
|
154
|
+
const renderSheet = async () => {
|
|
155
|
+
const result = await render(ActionSheetHost);
|
|
156
|
+
const { container } = result;
|
|
157
|
+
return {
|
|
158
|
+
...result,
|
|
159
|
+
host: result.componentRef.instance as ActionSheetHost,
|
|
160
|
+
overlay: container.querySelector('overlay')!,
|
|
161
|
+
backdrop: container.querySelector('overlay > view')!,
|
|
162
|
+
panel: container.querySelector('[accessibility-exclusive-focus]')!,
|
|
163
|
+
// A button's pressable tile: the view around its label.
|
|
164
|
+
button: (label: string) => result.getByText(label).parentElement!,
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Opens the action sheet and waits out its open sequence: the overlay shows
|
|
170
|
+
* on one tick, and the panel slides in on the next.
|
|
171
|
+
*/
|
|
172
|
+
const openSheet = async (host: ActionSheetHost) => {
|
|
173
|
+
host.open.set(true);
|
|
174
|
+
for (let i = 0; i < 3; i++) await waitForUpdate();
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Waits for the close sequence to finish: the panel slides out, then the
|
|
179
|
+
* overlay hides and `closed` fires. Polled rather than timed, since the
|
|
180
|
+
* sequence only starts on the update after `open` changes.
|
|
181
|
+
*/
|
|
182
|
+
const waitForClose = async (host: ActionSheetHost) => {
|
|
183
|
+
await vi.waitFor(() => expect(host.closed).toHaveBeenCalled());
|
|
184
|
+
await waitForUpdate();
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The scale a button's inline style holds, if any.
|
|
189
|
+
*/
|
|
190
|
+
const scaleOf = (element: Element) =>
|
|
191
|
+
(element as HTMLElement).style.getPropertyValue('transform');
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Presses a button and lets go every way Lynx reports it, checking that it
|
|
195
|
+
* scales down while pressed and back up once released.
|
|
196
|
+
*/
|
|
197
|
+
const expectPressFeedback = (button: Element) => {
|
|
198
|
+
fireEvent.touchstart(button);
|
|
199
|
+
expect(scaleOf(button)).toBe('scale(0.985)');
|
|
200
|
+
fireEvent.touchend(button);
|
|
201
|
+
expect(scaleOf(button)).toBe('scale(1)');
|
|
202
|
+
|
|
203
|
+
// A scroll that takes the touch over cancels it: the button scales back.
|
|
204
|
+
fireEvent.touchstart(button);
|
|
205
|
+
fireEvent.touchcancel(button);
|
|
206
|
+
expect(scaleOf(button)).toBe('scale(1)');
|
|
207
|
+
|
|
208
|
+
// On web, the mouse drives the same feedback.
|
|
209
|
+
fireEvent.mousedown(button);
|
|
210
|
+
expect(scaleOf(button)).toBe('scale(0.985)');
|
|
211
|
+
fireEvent.mouseup(button);
|
|
212
|
+
expect(scaleOf(button)).toBe('scale(1)');
|
|
213
|
+
|
|
214
|
+
fireEvent.mousedown(button);
|
|
215
|
+
fireLynxEvent(button, 'mouseleave');
|
|
216
|
+
expect(scaleOf(button)).toBe('scale(1)');
|
|
217
|
+
};
|
|
218
|
+
|
|
34
219
|
describe('UiActionSheet', () => {
|
|
220
|
+
// Watched per test: the testing library resets the main thread after each.
|
|
221
|
+
let animate: MockInstance<LynxMainThread['__ElementAnimate']>;
|
|
222
|
+
beforeEach(() => {
|
|
223
|
+
animate = vi.spyOn(lynxMainThread(), '__ElementAnimate');
|
|
224
|
+
});
|
|
225
|
+
afterEach(() => animate.mockRestore());
|
|
226
|
+
|
|
227
|
+
it('stays hidden until opened, then shows its title and buttons', async () => {
|
|
228
|
+
const { host, overlay, getByText } = await renderSheet();
|
|
229
|
+
expect(overlay.getAttribute('style')).toContain('display: none');
|
|
230
|
+
expect(overlay.getAttribute('visible')).toBe('false');
|
|
231
|
+
|
|
232
|
+
await openSheet(host);
|
|
233
|
+
expect(overlay.getAttribute('style')).not.toContain('display: none');
|
|
234
|
+
expect(overlay.getAttribute('visible')).toBe('true');
|
|
235
|
+
expect(getByText('Pick')).toBeTruthy();
|
|
236
|
+
expect(getByText('Share')).toBeTruthy();
|
|
237
|
+
expect(getByText('Delete')).toBeTruthy();
|
|
238
|
+
expect(getByText('Cancel')).toBeTruthy();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("merges the class into the panel's own", async () => {
|
|
242
|
+
const { panel } = await renderSheet();
|
|
243
|
+
expect(panel.classList).toContain('pb-8');
|
|
244
|
+
expect(panel.classList).not.toContain('pb-6');
|
|
245
|
+
expect(panel.classList).toContain('px-4');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('fades the backdrop in and slides the panel up when opened', async () => {
|
|
249
|
+
const { host, backdrop, panel } = await renderSheet();
|
|
250
|
+
await openSheet(host);
|
|
251
|
+
expect(animate).toHaveBeenCalledWith(backdrop, [
|
|
252
|
+
START,
|
|
253
|
+
expect.any(String),
|
|
254
|
+
[{ opacity: 0 }, { opacity: 1 }],
|
|
255
|
+
expect.objectContaining({ duration: DURATION.normal }),
|
|
256
|
+
]);
|
|
257
|
+
expect(animate).toHaveBeenCalledWith(panel, [
|
|
258
|
+
START,
|
|
259
|
+
expect.any(String),
|
|
260
|
+
[
|
|
261
|
+
{ transform: 'translateY(100%)', opacity: 0.8 },
|
|
262
|
+
{ transform: 'translateY(0%)', opacity: 1 },
|
|
263
|
+
],
|
|
264
|
+
expect.objectContaining({ duration: DURATION.slow }),
|
|
265
|
+
]);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('moves a screen reader into the panel and keeps it there while open', async () => {
|
|
269
|
+
const invoke = vi.spyOn(
|
|
270
|
+
globalThis as unknown as InvokeUIMethod,
|
|
271
|
+
'__InvokeUIMethod',
|
|
272
|
+
);
|
|
273
|
+
const { host, panel } = await renderSheet();
|
|
274
|
+
expect(panel.getAttribute('accessibility-exclusive-focus')).toBe('false');
|
|
275
|
+
|
|
276
|
+
await openSheet(host);
|
|
277
|
+
expect(invoke).toHaveBeenCalledWith(
|
|
278
|
+
panel,
|
|
279
|
+
'requestAccessibilityFocus',
|
|
280
|
+
expect.anything(),
|
|
281
|
+
expect.any(Function),
|
|
282
|
+
);
|
|
283
|
+
expect(panel.getAttribute('accessibility-exclusive-focus')).toBe('true');
|
|
284
|
+
invoke.mockRestore();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('closes when the backdrop is tapped, sliding the panel down', async () => {
|
|
288
|
+
const { host, overlay, backdrop, panel } = await renderSheet();
|
|
289
|
+
await openSheet(host);
|
|
290
|
+
|
|
291
|
+
fireEvent.tap(backdrop);
|
|
292
|
+
expect(host.open()).toBe(false);
|
|
293
|
+
await waitForClose(host);
|
|
294
|
+
expect(animate).toHaveBeenCalledWith(backdrop, [
|
|
295
|
+
START,
|
|
296
|
+
expect.any(String),
|
|
297
|
+
[{ opacity: 1 }, { opacity: 0 }],
|
|
298
|
+
expect.objectContaining({ duration: DURATION.normal }),
|
|
299
|
+
]);
|
|
300
|
+
expect(animate).toHaveBeenCalledWith(panel, [
|
|
301
|
+
START,
|
|
302
|
+
expect.any(String),
|
|
303
|
+
[
|
|
304
|
+
{ transform: 'translateY(0%)', opacity: 1 },
|
|
305
|
+
{ transform: 'translateY(100%)', opacity: 0.8 },
|
|
306
|
+
],
|
|
307
|
+
expect.objectContaining({ duration: DURATION.normal }),
|
|
308
|
+
]);
|
|
309
|
+
// Once the slide is over, the overlay hides and `closed` fires, only for
|
|
310
|
+
// this close: an action sheet that starts closed hasn't closed before.
|
|
311
|
+
expect(overlay.getAttribute('style')).toContain('display: none');
|
|
312
|
+
expect(host.closed).toHaveBeenCalledOnce();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("closes on Android's back button", async () => {
|
|
316
|
+
const { host, overlay } = await renderSheet();
|
|
317
|
+
await openSheet(host);
|
|
318
|
+
|
|
319
|
+
fireLynxEvent(overlay, 'requestclose');
|
|
320
|
+
expect(host.open()).toBe(false);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('stays open when the panel is tapped', async () => {
|
|
324
|
+
const { host, panel } = await renderSheet();
|
|
325
|
+
await openSheet(host);
|
|
326
|
+
// The panel catches its taps, so they never reach the backdrop.
|
|
327
|
+
fireEvent.tap(panel, { eventType: 'catchEvent' });
|
|
328
|
+
expect(host.open()).toBe(true);
|
|
329
|
+
});
|
|
330
|
+
|
|
35
331
|
it('stops its close timers when it is destroyed while closing', async () => {
|
|
36
332
|
const warn = vi.spyOn(console, 'warn');
|
|
37
|
-
const {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
for (let i = 0; i < 5; i++) await waitForUpdate();
|
|
41
|
-
inst.open.set(false);
|
|
333
|
+
const { host, unmount } = await renderSheet();
|
|
334
|
+
await openSheet(host);
|
|
335
|
+
host.open.set(false);
|
|
42
336
|
await waitForUpdate();
|
|
43
|
-
// Destroyed during the close animation, as an `@if` around the action
|
|
44
|
-
// would do. Its close timer (DURATION.normal + 20) used to fire
|
|
45
|
-
// and emit `closed` from the destroyed output.
|
|
337
|
+
// Destroyed during the close animation, as an `@if` around the action
|
|
338
|
+
// sheet would do. Its close timer (DURATION.normal + 20) used to fire
|
|
339
|
+
// anyway and emit `closed` from the destroyed output.
|
|
46
340
|
unmount();
|
|
47
|
-
await new Promise((
|
|
341
|
+
await new Promise((resolve) => setTimeout(resolve, DURATION.normal + 50));
|
|
48
342
|
const warnings = warn.mock.calls.map(([message]) => String(message));
|
|
49
343
|
warn.mockRestore();
|
|
50
344
|
expect(warnings.filter((message) => message.includes('NG0953'))).toEqual(
|
|
51
345
|
[],
|
|
52
346
|
);
|
|
347
|
+
expect(host.closed).not.toHaveBeenCalled();
|
|
53
348
|
});
|
|
349
|
+
});
|
|
54
350
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
)
|
|
61
|
-
expect(
|
|
62
|
-
const invoke = vi.fn();
|
|
63
|
-
panel.invoke = invoke;
|
|
64
|
-
(inst as unknown as { backdropRef: unknown }).backdropRef = signal({
|
|
65
|
-
nativeElement: panel,
|
|
66
|
-
});
|
|
67
|
-
(inst as unknown as { panelRef: unknown }).panelRef = signal({
|
|
68
|
-
nativeElement: panel,
|
|
69
|
-
});
|
|
70
|
-
inst.open.set(true);
|
|
71
|
-
for (let i = 0; i < 5; i++) await waitForUpdate();
|
|
72
|
-
expect(invoke).toHaveBeenCalledWith('requestAccessibilityFocus');
|
|
73
|
-
expect(panel.getAttribute('accessibility-exclusive-focus')).toBe('true');
|
|
351
|
+
describe('UiActionSheetTitle', () => {
|
|
352
|
+
it('shows its text muted, with the class on its container', async () => {
|
|
353
|
+
const { getByText } = await renderSheet();
|
|
354
|
+
const title = getByText('Pick');
|
|
355
|
+
expect(title.classList).toContain('text-muted-foreground');
|
|
356
|
+
expect(title.parentElement?.classList).toContain('py-4');
|
|
357
|
+
expect(title.parentElement?.classList).not.toContain('py-3');
|
|
74
358
|
});
|
|
359
|
+
});
|
|
75
360
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
(inst as unknown as { panelRef: unknown }).panelRef = signal({
|
|
81
|
-
nativeElement: view,
|
|
82
|
-
});
|
|
83
|
-
};
|
|
361
|
+
describe('UiActionSheetItem', () => {
|
|
362
|
+
it('reports the press and closes the action sheet when tapped', async () => {
|
|
363
|
+
const { host, button } = await renderSheet();
|
|
364
|
+
await openSheet(host);
|
|
84
365
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
expect(
|
|
88
|
-
expect(
|
|
89
|
-
expect(getByText('Cancel')).toBeTruthy();
|
|
366
|
+
fireEvent.tap(button('Share'));
|
|
367
|
+
expect(host.shared).toHaveBeenCalledOnce();
|
|
368
|
+
expect(host.deleted).not.toHaveBeenCalled();
|
|
369
|
+
expect(host.open()).toBe(false);
|
|
90
370
|
});
|
|
91
371
|
|
|
92
|
-
it('
|
|
93
|
-
const {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
await waitForUpdate();
|
|
97
|
-
(inst as unknown as { onBackdropTap: () => void }).onBackdropTap();
|
|
98
|
-
expect(inst.open()).toBe(false);
|
|
372
|
+
it('colors its label by variant', async () => {
|
|
373
|
+
const { getByText } = await renderSheet();
|
|
374
|
+
expect(getByText('Share').classList).toContain('text-primary');
|
|
375
|
+
expect(getByText('Delete').classList).toContain('text-destructive');
|
|
99
376
|
});
|
|
100
377
|
|
|
101
|
-
it(
|
|
102
|
-
const {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
inst.close();
|
|
106
|
-
expect(inst.open()).toBe(false);
|
|
378
|
+
it("merges the class into its tile's own", async () => {
|
|
379
|
+
const { button } = await renderSheet();
|
|
380
|
+
expect(button('Delete').classList).toContain('bg-muted');
|
|
381
|
+
expect(button('Delete').classList).toContain('border-t');
|
|
107
382
|
});
|
|
108
383
|
|
|
109
|
-
it('
|
|
110
|
-
const {
|
|
111
|
-
|
|
112
|
-
expect(() =>
|
|
113
|
-
(inst as unknown as { onPanelTap: () => void }).onPanelTap(),
|
|
114
|
-
).not.toThrow();
|
|
384
|
+
it('scales down while pressed, and back on release', async () => {
|
|
385
|
+
const { button } = await renderSheet();
|
|
386
|
+
expectPressFeedback(button('Share'));
|
|
115
387
|
});
|
|
388
|
+
});
|
|
116
389
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
patchRefs(inst, view);
|
|
390
|
+
describe('UiActionSheetCancel', () => {
|
|
391
|
+
it('closes the action sheet when tapped', async () => {
|
|
392
|
+
const { host, button } = await renderSheet();
|
|
393
|
+
await openSheet(host);
|
|
122
394
|
|
|
123
|
-
|
|
124
|
-
|
|
395
|
+
fireEvent.tap(button('Cancel'));
|
|
396
|
+
expect(host.open()).toBe(false);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('shows Cancel, or the label it is given', async () => {
|
|
400
|
+
expect((await renderSheet()).getByText('Cancel')).toBeTruthy();
|
|
401
|
+
const { getByText } = await render(CancelLabelHost);
|
|
402
|
+
expect(getByText('Dismiss')).toBeTruthy();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("merges the class into its tile's own", async () => {
|
|
406
|
+
const { button } = await renderSheet();
|
|
407
|
+
expect(button('Cancel').classList).toContain('mt-2');
|
|
408
|
+
expect(button('Cancel').classList).toContain('bg-card');
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('scales down while pressed, and back on release', async () => {
|
|
412
|
+
const { button } = await renderSheet();
|
|
413
|
+
expectPressFeedback(button('Cancel'));
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* The Lynx function that runs an element's UI methods, here measuring the
|
|
419
|
+
* anchor and the popover (`boundingClientRect`) as on a phone.
|
|
420
|
+
*/
|
|
421
|
+
type MeasureUIMethod = {
|
|
422
|
+
__InvokeUIMethod: (
|
|
423
|
+
element: Element,
|
|
424
|
+
method: string,
|
|
425
|
+
params: Record<string, unknown>,
|
|
426
|
+
callback: (result: { code: number; data?: unknown }) => void,
|
|
427
|
+
) => void;
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
/** Accessibility settings a test can change, as the host app would. */
|
|
431
|
+
const settings = {
|
|
432
|
+
reduceMotion: signal(false),
|
|
433
|
+
reduceTransparency: signal(false),
|
|
434
|
+
increaseContrast: signal(false),
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Renders the anchored sheet on a platform (none for Material), with the
|
|
439
|
+
* safe area's markers a phone's screen, the popover's content 270 by 150,
|
|
440
|
+
* and the anchor a button near the top.
|
|
441
|
+
*/
|
|
442
|
+
const renderAnchored = async (
|
|
443
|
+
platform: { platform: string; osVersion: string } | null = {
|
|
444
|
+
platform: 'iOS',
|
|
445
|
+
osVersion: '26.0',
|
|
446
|
+
},
|
|
447
|
+
) => {
|
|
448
|
+
vi.stubGlobal('SystemInfo', platform ?? undefined);
|
|
449
|
+
vi.spyOn(
|
|
450
|
+
globalThis as unknown as MeasureUIMethod,
|
|
451
|
+
'__InvokeUIMethod',
|
|
452
|
+
).mockImplementation((element, method, _params, callback) => {
|
|
453
|
+
if (method !== 'boundingClientRect') {
|
|
454
|
+
callback({ code: 0 });
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const style = element.getAttribute('style') ?? '';
|
|
458
|
+
const data = style.includes('safe-area-inset')
|
|
459
|
+
? { left: 0, top: 0, width: 402, height: 840 }
|
|
460
|
+
: element.classList.contains('shrink-0')
|
|
461
|
+
? { left: 0, top: 0, width: 270, height: 150 }
|
|
462
|
+
: { left: 16, top: 80, width: 80, height: 44 };
|
|
463
|
+
callback({ code: 0, data });
|
|
464
|
+
});
|
|
465
|
+
const result = await render(AnchoredHost, {
|
|
466
|
+
providers: [{ provide: LynxAccessibilitySettings, useValue: settings }],
|
|
467
|
+
});
|
|
468
|
+
const query = (selector: string) =>
|
|
469
|
+
result.container.querySelector(selector) as HTMLElement | null;
|
|
470
|
+
return {
|
|
471
|
+
...result,
|
|
472
|
+
host: result.componentRef.instance as AnchoredHost,
|
|
473
|
+
overlay: () => query('overlay')!,
|
|
474
|
+
anchor: () => query('.share')!,
|
|
475
|
+
popover: () => query('blur-view[ui-glass]:not(.share)'),
|
|
476
|
+
content: () => query('.shrink-0')!,
|
|
477
|
+
// A button's pressable row: the view around its label.
|
|
478
|
+
button: (label: string) => result.getByText(label).parentElement!,
|
|
479
|
+
};
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
type Anchored = Awaited<ReturnType<typeof renderAnchored>>;
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Opens the anchored sheet and waits until its popover has grown.
|
|
486
|
+
*/
|
|
487
|
+
const openAnchored = async (rendered: Anchored) => {
|
|
488
|
+
fireEvent.tap(rendered.anchor());
|
|
489
|
+
await vi.waitFor(() => expect(rendered.content().style.opacity).toBe('1'));
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Lets Lynx's timers run: the touch check waits for the next one.
|
|
494
|
+
*/
|
|
495
|
+
const nextTimer = () => new Promise((resolve) => setTimeout(resolve, 5));
|
|
496
|
+
|
|
497
|
+
describe('UiActionSheet opened from its control', () => {
|
|
498
|
+
beforeEach(() => {
|
|
499
|
+
// The renderer warns that the components aren't Lynx elements.
|
|
500
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
501
|
+
});
|
|
502
|
+
afterEach(() => {
|
|
503
|
+
vi.restoreAllMocks();
|
|
504
|
+
vi.unstubAllGlobals();
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('grows out of its control as Liquid Glass on iOS 26, leaving the screen undimmed', async () => {
|
|
508
|
+
const rendered = await renderAnchored();
|
|
509
|
+
await openAnchored(rendered);
|
|
510
|
+
|
|
511
|
+
const popover = rendered.popover()!;
|
|
512
|
+
expect(popover.getAttribute('blur-effect')).toBe('glass');
|
|
513
|
+
expect(popover.style.width).toBe('270px');
|
|
514
|
+
// The bottom sheet and its backdrop stay hidden.
|
|
515
|
+
expect(rendered.overlay().getAttribute('visible')).toBe('false');
|
|
516
|
+
// The control hides under the glass that grows out of it.
|
|
517
|
+
expect(rendered.anchor().style.visibility).toBe('hidden');
|
|
518
|
+
// Its title and actions, as iOS 26 lays them out, and no Cancel: a
|
|
519
|
+
// touch outside cancels.
|
|
520
|
+
expect(popover.contains(rendered.getByText('Delete this draft?'))).toBe(
|
|
521
|
+
true,
|
|
522
|
+
);
|
|
523
|
+
expect(rendered.getByText('Delete this draft?').classList).toContain(
|
|
524
|
+
'text-glass-muted-foreground',
|
|
525
|
+
);
|
|
526
|
+
expect(rendered.getByText('Save Draft').classList).toContain(
|
|
527
|
+
'text-glass-foreground',
|
|
528
|
+
);
|
|
529
|
+
expect(rendered.getByText('Delete Draft').classList).toContain(
|
|
530
|
+
'text-destructive',
|
|
531
|
+
);
|
|
532
|
+
expect(rendered.button('Save Draft').classList).toContain('min-h-[44px]');
|
|
533
|
+
expect(rendered.button('Save Draft').classList).toContain('justify-center');
|
|
534
|
+
expect(popover.contains(rendered.getByText('Cancel'))).toBe(false);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it("takes a glass control's glass over, shown as the control hides", async () => {
|
|
538
|
+
const rendered = await renderAnchored();
|
|
539
|
+
expect(rendered.anchor().hasAttribute('glass-effect-id')).toBe(false);
|
|
540
|
+
rendered.host.glassAnchor.set(true);
|
|
125
541
|
await waitForUpdate();
|
|
542
|
+
const id = rendered.anchor().getAttribute('glass-effect-id');
|
|
543
|
+
expect(id).toMatch(/^ui-action-sheet-\d+$/);
|
|
544
|
+
|
|
545
|
+
await openAnchored(rendered);
|
|
546
|
+
expect(rendered.popover()!.getAttribute('glass-effect-id')).toBe(id);
|
|
547
|
+
expect(rendered.anchor().style.visibility).toBe('hidden');
|
|
548
|
+
|
|
549
|
+
// A control that goes takes its glass with it.
|
|
550
|
+
rendered.host.open.set(false);
|
|
551
|
+
await vi.waitFor(() => expect(rendered.host.closed).toHaveBeenCalled());
|
|
552
|
+
rendered.host.glassAnchor.set(false);
|
|
126
553
|
await waitForUpdate();
|
|
554
|
+
expect(rendered.anchor().tagName.toLowerCase()).toBe('view');
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('chooses an action, and closes', async () => {
|
|
558
|
+
const rendered = await renderAnchored();
|
|
559
|
+
await openAnchored(rendered);
|
|
127
560
|
|
|
128
|
-
|
|
129
|
-
|
|
561
|
+
fireEvent.tap(rendered.button('Delete Draft'));
|
|
562
|
+
expect(rendered.host.deleted).toHaveBeenCalledTimes(1);
|
|
563
|
+
expect(rendered.host.open()).toBe(false);
|
|
564
|
+
await vi.waitFor(() => expect(rendered.host.closed).toHaveBeenCalled());
|
|
130
565
|
await waitForUpdate();
|
|
131
|
-
expect(
|
|
566
|
+
expect(rendered.popover()).toBeNull();
|
|
567
|
+
expect(rendered.anchor().style.visibility).toBe('');
|
|
568
|
+
expect(rendered.host.closed).toHaveBeenCalledTimes(1);
|
|
132
569
|
});
|
|
133
570
|
|
|
134
|
-
it('
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
571
|
+
it('lights a row up while it is pressed, rather than shrinking it', async () => {
|
|
572
|
+
const rendered = await renderAnchored();
|
|
573
|
+
await openAnchored(rendered);
|
|
574
|
+
const row = rendered.button('Save Draft');
|
|
575
|
+
const highlight = row.querySelector('.bg-glass-foreground') as HTMLElement;
|
|
576
|
+
// iOS 26's menu rows light up in a capsule, inset from the sides.
|
|
577
|
+
expect(highlight.className.split(' ')).toEqual(
|
|
578
|
+
expect.arrayContaining(['left-[10px]', 'right-[10px]', 'rounded-full']),
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
fireEvent.touchstart(row);
|
|
140
582
|
await waitForUpdate();
|
|
583
|
+
expect(highlight.style.opacity).toBe('0.08');
|
|
584
|
+
expect(row.style.transform).toBe('');
|
|
585
|
+
fireEvent.touchend(row);
|
|
141
586
|
await waitForUpdate();
|
|
142
|
-
|
|
587
|
+
expect(highlight.style.opacity).toBe('0');
|
|
588
|
+
fireEvent.mousedown(row);
|
|
143
589
|
await waitForUpdate();
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
await
|
|
147
|
-
expect(
|
|
148
|
-
expect(closed).toHaveBeenCalledOnce();
|
|
590
|
+
expect(highlight.style.opacity).toBe('0.08');
|
|
591
|
+
fireLynxEvent(row, 'mouseleave');
|
|
592
|
+
await waitForUpdate();
|
|
593
|
+
expect(highlight.style.opacity).toBe('0');
|
|
149
594
|
});
|
|
150
595
|
|
|
151
|
-
it('
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
596
|
+
it('lights a Material row up edge to edge', async () => {
|
|
597
|
+
const rendered = await renderAnchored(null);
|
|
598
|
+
await openAnchored(rendered);
|
|
599
|
+
const row = rendered.button('Save Draft');
|
|
600
|
+
const highlight = row.querySelector('.bg-glass-foreground') as HTMLElement;
|
|
601
|
+
expect(highlight.className.split(' ')).toEqual(
|
|
602
|
+
expect.arrayContaining(['left-0', 'right-0']),
|
|
603
|
+
);
|
|
604
|
+
fireEvent.touchstart(row);
|
|
155
605
|
await waitForUpdate();
|
|
156
|
-
expect(
|
|
157
|
-
Array.from(container.querySelectorAll('view')).some((el) =>
|
|
158
|
-
el.getAttribute('class')?.includes('my-panel'),
|
|
159
|
-
),
|
|
160
|
-
).toBe(true);
|
|
606
|
+
expect(highlight.style.opacity).toBe('0.1');
|
|
161
607
|
});
|
|
162
|
-
});
|
|
163
608
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
609
|
+
it('closes on a touch outside it, without counting one on its control', async () => {
|
|
610
|
+
const rendered = await renderAnchored();
|
|
611
|
+
await openAnchored(rendered);
|
|
612
|
+
|
|
613
|
+
// A touch on the control that presented it isn't outside it.
|
|
614
|
+
fireLynxEvent(rendered.anchor(), 'touchstart', {}, 'bindEvent');
|
|
615
|
+
fireLynxEvent(rendered.popover()!, 'touchstart', {}, 'global-bindEvent');
|
|
616
|
+
await nextTimer();
|
|
617
|
+
fireLynxEvent(rendered.anchor(), 'mousedown', {}, 'bindEvent');
|
|
618
|
+
fireLynxEvent(rendered.popover()!, 'mousedown', {}, 'global-bindEvent');
|
|
619
|
+
await nextTimer();
|
|
620
|
+
expect(rendered.host.open()).toBe(true);
|
|
621
|
+
|
|
622
|
+
fireLynxEvent(rendered.popover()!, 'touchstart', {}, 'global-bindEvent');
|
|
623
|
+
await nextTimer();
|
|
624
|
+
expect(rendered.host.open()).toBe(false);
|
|
169
625
|
});
|
|
170
|
-
});
|
|
171
626
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
627
|
+
it('is a Material 3 menu beside its control on Android', async () => {
|
|
628
|
+
const rendered = await renderAnchored(null);
|
|
629
|
+
await openAnchored(rendered);
|
|
630
|
+
|
|
631
|
+
const popover = rendered.popover()!;
|
|
632
|
+
expect(popover.style.width).toBe('280px');
|
|
633
|
+
expect(popover.classList).toContain('bg-secondary');
|
|
634
|
+
// Beside the control, which stays in view.
|
|
635
|
+
expect(rendered.anchor().style.visibility).toBe('');
|
|
636
|
+
expect(rendered.button('Save Draft').classList).toContain('min-h-[48px]');
|
|
637
|
+
expect(rendered.button('Save Draft').classList).not.toContain(
|
|
638
|
+
'justify-center',
|
|
639
|
+
);
|
|
640
|
+
expect(rendered.getByText('Save Draft').classList).toContain('text-[16px]');
|
|
641
|
+
expect(
|
|
642
|
+
rendered.getByText('Delete this draft?').parentElement?.classList,
|
|
643
|
+
).toContain('px-3');
|
|
644
|
+
});
|
|
178
645
|
|
|
179
|
-
|
|
180
|
-
|
|
646
|
+
it('rises from the bottom before iOS 26, as iPhone action sheets did', async () => {
|
|
647
|
+
const rendered = await renderAnchored({
|
|
648
|
+
platform: 'iOS',
|
|
649
|
+
osVersion: '18.5',
|
|
181
650
|
});
|
|
182
|
-
|
|
183
|
-
|
|
651
|
+
fireEvent.tap(rendered.anchor());
|
|
652
|
+
for (let i = 0; i < 3; i++) await waitForUpdate();
|
|
653
|
+
|
|
654
|
+
expect(rendered.overlay().getAttribute('visible')).toBe('true');
|
|
655
|
+
expect(rendered.popover()).toBeNull();
|
|
656
|
+
expect(rendered.getByText('Save Draft').classList).toContain(
|
|
657
|
+
'text-primary',
|
|
658
|
+
);
|
|
659
|
+
expect(rendered.anchor().style.visibility).toBe('');
|
|
660
|
+
});
|
|
184
661
|
|
|
185
|
-
it('
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
.instance as unknown as UiActionSheetItem & {
|
|
189
|
-
onTap: () => void;
|
|
190
|
-
};
|
|
191
|
-
const emitted = vi.fn();
|
|
192
|
-
inst.pressed.subscribe(emitted);
|
|
193
|
-
inst.onTap();
|
|
194
|
-
expect(emitted).toHaveBeenCalledTimes(1);
|
|
195
|
-
expect(parent.open()).toBe(false);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('press handlers run cleanly', async () => {
|
|
199
|
-
const { itemResult } = await renderItem();
|
|
200
|
-
const inst = itemResult.componentRef.instance as unknown as {
|
|
201
|
-
onPressStart: () => void;
|
|
202
|
-
onPressEnd: () => void;
|
|
203
|
-
onPressCancel: () => void;
|
|
204
|
-
};
|
|
205
|
-
expect(() => inst.onPressStart()).not.toThrow();
|
|
206
|
-
expect(() => inst.onPressEnd()).not.toThrow();
|
|
207
|
-
expect(() => inst.onPressCancel()).not.toThrow();
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it('applies destructive variant text class', async () => {
|
|
211
|
-
const { itemResult } = await renderItem();
|
|
212
|
-
const inst = itemResult.componentRef.instance as UiActionSheetItem;
|
|
213
|
-
setInputSignal(inst.variant, 'destructive');
|
|
662
|
+
it('rises from the bottom again once its control is gone', async () => {
|
|
663
|
+
const rendered = await renderAnchored();
|
|
664
|
+
rendered.host.anchored.set(false);
|
|
214
665
|
await waitForUpdate();
|
|
215
|
-
|
|
216
|
-
|
|
666
|
+
|
|
667
|
+
rendered.host.open.set(true);
|
|
668
|
+
for (let i = 0; i < 3; i++) await waitForUpdate();
|
|
669
|
+
expect(rendered.overlay().getAttribute('visible')).toBe('true');
|
|
670
|
+
expect(rendered.popover()).toBeNull();
|
|
671
|
+
// Its actions are back in the sheet's group, with Cancel below.
|
|
672
|
+
expect(rendered.button('Save Draft').classList).toContain('border-t');
|
|
673
|
+
expect(rendered.getByText('Cancel')).toBeTruthy();
|
|
217
674
|
});
|
|
218
|
-
});
|
|
219
675
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
parent.open.set(true);
|
|
676
|
+
it("doesn't report a close when its control comes or goes while it's closed", async () => {
|
|
677
|
+
const rendered = await renderAnchored();
|
|
678
|
+
// Opened and closed from the bottom once...
|
|
679
|
+
rendered.host.anchored.set(false);
|
|
225
680
|
await waitForUpdate();
|
|
681
|
+
rendered.host.open.set(true);
|
|
682
|
+
for (let i = 0; i < 3; i++) await waitForUpdate();
|
|
683
|
+
rendered.host.open.set(false);
|
|
684
|
+
await vi.waitFor(() => expect(rendered.host.closed).toHaveBeenCalled());
|
|
226
685
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
686
|
+
// ...then given its control back, closed.
|
|
687
|
+
rendered.host.anchored.set(true);
|
|
688
|
+
await waitForUpdate();
|
|
689
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
690
|
+
expect(rendered.host.closed).toHaveBeenCalledTimes(1);
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
describe('UiActionSheet accessibility', () => {
|
|
695
|
+
it('reads its title as a heading and its actions as buttons', async () => {
|
|
696
|
+
const { getByText } = await renderSheet();
|
|
697
|
+
expect(getByText('Pick').getAttribute('accessibility-traits')).toBe(
|
|
698
|
+
'header',
|
|
699
|
+
);
|
|
700
|
+
expect(getByText('Share').getAttribute('accessibility-traits')).toBe(
|
|
701
|
+
'button',
|
|
702
|
+
);
|
|
703
|
+
expect(getByText('Cancel').getAttribute('accessibility-traits')).toBe(
|
|
704
|
+
'button',
|
|
705
|
+
);
|
|
706
|
+
});
|
|
232
707
|
|
|
233
|
-
it('
|
|
234
|
-
const {
|
|
235
|
-
expect(
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it('onTap closes the parent sheet', async () => {
|
|
239
|
-
const { parent, cancelResult } = await renderCancel();
|
|
240
|
-
const inst = cancelResult.componentRef.instance as unknown as {
|
|
241
|
-
onTap: () => void;
|
|
242
|
-
};
|
|
243
|
-
inst.onTap();
|
|
244
|
-
expect(parent.open()).toBe(false);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it('press handlers run cleanly', async () => {
|
|
248
|
-
const { cancelResult } = await renderCancel();
|
|
249
|
-
const inst = cancelResult.componentRef.instance as unknown as {
|
|
250
|
-
onPressStart: () => void;
|
|
251
|
-
onPressEnd: () => void;
|
|
252
|
-
onPressCancel: () => void;
|
|
253
|
-
};
|
|
254
|
-
expect(() => inst.onPressStart()).not.toThrow();
|
|
255
|
-
expect(() => inst.onPressEnd()).not.toThrow();
|
|
256
|
-
expect(() => inst.onPressCancel()).not.toThrow();
|
|
708
|
+
it('draws a title outside an action sheet as in one', async () => {
|
|
709
|
+
const { getByText } = await render(LoneTitleHost);
|
|
710
|
+
expect(getByText('Alone').classList).toContain('text-muted-foreground');
|
|
257
711
|
});
|
|
258
712
|
});
|