@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,30 @@
|
|
|
1
|
-
import { Component, signal } from '@angular/core';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { Component, signal, viewChild } 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 {
|
|
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 {
|
|
20
|
+
type LynxMainThread,
|
|
21
|
+
lynxMainThread,
|
|
22
|
+
} from '../../../test-utils/lynx-main-thread';
|
|
7
23
|
import {
|
|
24
|
+
type AlertDialogFooterOrientation,
|
|
25
|
+
type AlertDialogVariant,
|
|
8
26
|
UiAlertDialog,
|
|
27
|
+
UiAlertDialogAction,
|
|
9
28
|
UiAlertDialogDescription,
|
|
10
29
|
UiAlertDialogFooter,
|
|
11
30
|
UiAlertDialogHeader,
|
|
@@ -22,167 +41,759 @@ import {
|
|
|
22
41
|
LYNX_ELEMENTS,
|
|
23
42
|
],
|
|
24
43
|
template: `
|
|
25
|
-
|
|
26
|
-
<ui-alert-dialog
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
44
|
+
@if (shown()) {
|
|
45
|
+
<ui-alert-dialog
|
|
46
|
+
[(open)]="open"
|
|
47
|
+
class="max-w-sm"
|
|
48
|
+
(closed)="closes.set(closes() + 1)"
|
|
49
|
+
>
|
|
50
|
+
<ui-alert-dialog-header class="gap-2">
|
|
51
|
+
<ui-alert-dialog-title class="text-xl">Sure?</ui-alert-dialog-title>
|
|
52
|
+
<ui-alert-dialog-description class="italic">
|
|
53
|
+
Cannot undo
|
|
54
|
+
</ui-alert-dialog-description>
|
|
55
|
+
</ui-alert-dialog-header>
|
|
56
|
+
<ui-alert-dialog-footer class="justify-start">
|
|
57
|
+
<text>Buttons</text>
|
|
58
|
+
</ui-alert-dialog-footer>
|
|
59
|
+
</ui-alert-dialog>
|
|
60
|
+
}
|
|
32
61
|
`,
|
|
33
62
|
})
|
|
34
63
|
class AlertDialogHost {
|
|
35
|
-
open =
|
|
64
|
+
readonly open = signal(false);
|
|
65
|
+
// Takes the alert dialog out of the tree, as an `@if` or a route change
|
|
66
|
+
// would.
|
|
67
|
+
readonly shown = signal(true);
|
|
68
|
+
readonly closes = signal(0);
|
|
69
|
+
readonly dialog = viewChild.required(UiAlertDialog);
|
|
36
70
|
}
|
|
37
71
|
|
|
72
|
+
const renderAlertDialog = async () => {
|
|
73
|
+
const result = await render(AlertDialogHost);
|
|
74
|
+
const panel = () =>
|
|
75
|
+
result.container.querySelector('[accessibility-exclusive-focus]')!;
|
|
76
|
+
return {
|
|
77
|
+
...result,
|
|
78
|
+
host: result.componentRef.instance as AlertDialogHost,
|
|
79
|
+
overlay: () => result.container.querySelector('overlay')!,
|
|
80
|
+
panel,
|
|
81
|
+
backdrop: () => panel().parentElement!,
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Waits for the alert dialog's open or close sequence, which runs on timers, then
|
|
87
|
+
* renders what it changed.
|
|
88
|
+
*/
|
|
89
|
+
const after = async (ms: number) => {
|
|
90
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
91
|
+
await waitForUpdate();
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Long enough for the open sequence, including the panel's 30ms stagger. The
|
|
96
|
+
* sequence is chained timers, and on Windows each can fire up to a clock
|
|
97
|
+
* tick (about 16ms) late, so 50ms was too short on CI's Windows runner.
|
|
98
|
+
*/
|
|
99
|
+
const OPENED = 150;
|
|
100
|
+
/** Long enough for the close animation and the hide after it, as above. */
|
|
101
|
+
const CLOSED = DURATION.normal + 150;
|
|
102
|
+
|
|
103
|
+
/** `__ElementAnimate`'s first argument for starting an animation. */
|
|
104
|
+
const START = 0;
|
|
105
|
+
/** `__ElementAnimate`'s first argument for canceling an animation. */
|
|
106
|
+
const CANCEL = 3;
|
|
107
|
+
|
|
108
|
+
/** Accessibility settings a test can change, as the host app would. */
|
|
109
|
+
const settings = {
|
|
110
|
+
reduceMotion: signal(false),
|
|
111
|
+
reduceTransparency: signal(false),
|
|
112
|
+
increaseContrast: signal(false),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Watched per test: the testing library resets the main thread after each.
|
|
116
|
+
let animate: MockInstance<LynxMainThread['__ElementAnimate']>;
|
|
117
|
+
beforeEach(() => {
|
|
118
|
+
animate = vi.spyOn(lynxMainThread(), '__ElementAnimate');
|
|
119
|
+
});
|
|
120
|
+
afterEach(() => {
|
|
121
|
+
animate.mockRestore();
|
|
122
|
+
vi.unstubAllGlobals();
|
|
123
|
+
settings.reduceMotion.set(false);
|
|
124
|
+
});
|
|
125
|
+
|
|
38
126
|
describe('UiAlertDialog', () => {
|
|
39
|
-
it('
|
|
127
|
+
it('renders each part with its projected content', async () => {
|
|
128
|
+
const { getByText } = await renderAlertDialog();
|
|
129
|
+
expect(getByText('Sure?')).toBeTruthy();
|
|
130
|
+
expect(getByText('Cannot undo')).toBeTruthy();
|
|
131
|
+
expect(getByText('Buttons')).toBeTruthy();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("merges each part's class with its own, the given class winning", async () => {
|
|
135
|
+
const { container, getByText, panel } = await renderAlertDialog();
|
|
136
|
+
expect(panel().classList).toContain('bg-background');
|
|
137
|
+
expect(panel().classList).toContain('max-w-sm');
|
|
138
|
+
expect(getByText('Sure?').classList).toContain('font-semibold');
|
|
139
|
+
expect(getByText('Sure?').classList).toContain('text-xl');
|
|
140
|
+
expect(getByText('Cannot undo').classList).toContain('text-sm');
|
|
141
|
+
expect(getByText('Cannot undo').classList).toContain('italic');
|
|
142
|
+
// Each part's own view, not its host element, which keeps the bare class.
|
|
143
|
+
const header = container.querySelector('.flex-col.gap-2');
|
|
144
|
+
expect(header?.classList).not.toContain('gap-1.5');
|
|
145
|
+
const footer = container.querySelector('.pt-4');
|
|
146
|
+
expect(footer?.classList).toContain('justify-start');
|
|
147
|
+
expect(footer?.classList).not.toContain('justify-end');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('stays hidden until it is opened', async () => {
|
|
151
|
+
const { host, overlay, panel } = await renderAlertDialog();
|
|
152
|
+
await after(CLOSED);
|
|
153
|
+
expect(overlay().getAttribute('style')).toContain('display: none');
|
|
154
|
+
expect(panel().getAttribute('accessibility-exclusive-focus')).toBe('false');
|
|
155
|
+
expect(animate).not.toHaveBeenCalled();
|
|
156
|
+
// Starting closed isn't closing: `closed` waits for a real close.
|
|
157
|
+
expect(host.closes()).toBe(0);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('shows the overlay and animates the backdrop, then the panel, in', async () => {
|
|
161
|
+
const { host, overlay, backdrop, panel } = await renderAlertDialog();
|
|
162
|
+
host.open.set(true);
|
|
163
|
+
await after(OPENED);
|
|
164
|
+
expect(overlay().getAttribute('style')).not.toContain('display: none');
|
|
165
|
+
expect(animate).toHaveBeenCalledWith(backdrop(), [
|
|
166
|
+
START,
|
|
167
|
+
expect.any(String),
|
|
168
|
+
[{ opacity: 0 }, { opacity: 1 }],
|
|
169
|
+
expect.objectContaining({ duration: DURATION.normal }),
|
|
170
|
+
]);
|
|
171
|
+
expect(animate).toHaveBeenCalledWith(panel(), [
|
|
172
|
+
START,
|
|
173
|
+
expect.any(String),
|
|
174
|
+
[
|
|
175
|
+
{ transform: 'scale(0.9) translateY(10px)', opacity: 0 },
|
|
176
|
+
{ transform: 'scale(1) translateY(0px)', opacity: 1 },
|
|
177
|
+
],
|
|
178
|
+
expect.objectContaining({ duration: DURATION.slow }),
|
|
179
|
+
]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('moves a screen reader into the panel and keeps it there while open', async () => {
|
|
183
|
+
const { host, panel } = await renderAlertDialog();
|
|
184
|
+
const invoke = vi.spyOn(host.dialog().panelRef().nativeElement, 'invoke');
|
|
185
|
+
host.open.set(true);
|
|
186
|
+
await after(OPENED);
|
|
187
|
+
expect(invoke).toHaveBeenCalledWith('requestAccessibilityFocus');
|
|
188
|
+
expect(panel().getAttribute('accessibility-exclusive-focus')).toBe('true');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('animates out, then hides and emits closed', async () => {
|
|
192
|
+
const { host, overlay, backdrop, panel } = await renderAlertDialog();
|
|
193
|
+
host.open.set(true);
|
|
194
|
+
await after(OPENED);
|
|
195
|
+
animate.mockClear();
|
|
196
|
+
|
|
197
|
+
host.open.set(false);
|
|
198
|
+
await after(CLOSED);
|
|
199
|
+
expect(animate).toHaveBeenCalledWith(backdrop(), [
|
|
200
|
+
START,
|
|
201
|
+
expect.any(String),
|
|
202
|
+
[{ opacity: 1 }, { opacity: 0 }],
|
|
203
|
+
expect.any(Object),
|
|
204
|
+
]);
|
|
205
|
+
expect(animate).toHaveBeenCalledWith(panel(), [
|
|
206
|
+
START,
|
|
207
|
+
expect.any(String),
|
|
208
|
+
[
|
|
209
|
+
{ transform: 'scale(1) translateY(0px)', opacity: 1 },
|
|
210
|
+
{ transform: 'scale(0.95) translateY(10px)', opacity: 0 },
|
|
211
|
+
],
|
|
212
|
+
expect.any(Object),
|
|
213
|
+
]);
|
|
214
|
+
expect(overlay().getAttribute('style')).toContain('display: none');
|
|
215
|
+
expect(panel().getAttribute('accessibility-exclusive-focus')).toBe('false');
|
|
216
|
+
expect(host.closes()).toBe(1);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('cancels the previous animations when it opens again', async () => {
|
|
220
|
+
const { host, backdrop, panel } = await renderAlertDialog();
|
|
221
|
+
host.open.set(true);
|
|
222
|
+
await after(OPENED);
|
|
223
|
+
host.open.set(false);
|
|
224
|
+
await after(CLOSED);
|
|
225
|
+
animate.mockClear();
|
|
226
|
+
|
|
227
|
+
host.open.set(true);
|
|
228
|
+
await after(OPENED);
|
|
229
|
+
// The close animations hold their last frame (fill: forwards) until
|
|
230
|
+
// they're canceled, which would hide the panel being opened.
|
|
231
|
+
expect(animate).toHaveBeenCalledWith(backdrop(), [
|
|
232
|
+
CANCEL,
|
|
233
|
+
expect.any(String),
|
|
234
|
+
]);
|
|
235
|
+
expect(animate).toHaveBeenCalledWith(panel(), [CANCEL, expect.any(String)]);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('stays open when the backdrop or the panel is tapped', async () => {
|
|
239
|
+
const { host, backdrop, panel } = await renderAlertDialog();
|
|
240
|
+
host.open.set(true);
|
|
241
|
+
await after(OPENED);
|
|
242
|
+
// Unlike a dialog, an alert dialog waits for an answer from its actions.
|
|
243
|
+
fireEvent.tap(backdrop());
|
|
244
|
+
// The panel catches its taps, so they don't reach the backdrop either.
|
|
245
|
+
fireEvent.tap(panel(), { eventType: 'catchEvent' });
|
|
246
|
+
await waitForUpdate();
|
|
247
|
+
expect(host.open()).toBe(true);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('only fades its panel under Reduce Motion', async () => {
|
|
251
|
+
const result = await render(AlertDialogHost, {
|
|
252
|
+
providers: [{ provide: LynxAccessibilitySettings, useValue: settings }],
|
|
253
|
+
});
|
|
254
|
+
const host = result.componentRef.instance as AlertDialogHost;
|
|
255
|
+
const panel = result.container.querySelector(
|
|
256
|
+
'[accessibility-exclusive-focus]',
|
|
257
|
+
)!;
|
|
258
|
+
settings.reduceMotion.set(true);
|
|
259
|
+
host.open.set(true);
|
|
260
|
+
await after(OPENED);
|
|
261
|
+
expect(animate).toHaveBeenCalledWith(panel, [
|
|
262
|
+
START,
|
|
263
|
+
expect.any(String),
|
|
264
|
+
[{ opacity: 0 }, { opacity: 1 }],
|
|
265
|
+
expect.any(Object),
|
|
266
|
+
]);
|
|
267
|
+
|
|
268
|
+
host.open.set(false);
|
|
269
|
+
await after(CLOSED);
|
|
270
|
+
expect(animate).toHaveBeenCalledWith(panel, [
|
|
271
|
+
START,
|
|
272
|
+
expect.any(String),
|
|
273
|
+
[{ opacity: 1 }, { opacity: 0 }],
|
|
274
|
+
expect.any(Object),
|
|
275
|
+
]);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('stops its close sequence when it is destroyed while closing', async () => {
|
|
40
279
|
const warn = vi.spyOn(console, 'warn');
|
|
41
|
-
const {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
280
|
+
const { host } = await renderAlertDialog();
|
|
281
|
+
host.open.set(true);
|
|
282
|
+
await after(OPENED);
|
|
283
|
+
host.open.set(false);
|
|
284
|
+
await after(0);
|
|
285
|
+
// Destroyed during the close animation. Its close timer
|
|
286
|
+
// (DURATION.normal + 20) used to fire anyway and emit `closed` from the
|
|
287
|
+
// destroyed output, which Angular warns about in dev builds.
|
|
288
|
+
host.shown.set(false);
|
|
46
289
|
await waitForUpdate();
|
|
47
|
-
|
|
48
|
-
// would do. Its close timer (DURATION.normal + 20) used to fire anyway
|
|
49
|
-
// and emit `closed` from the destroyed output.
|
|
50
|
-
unmount();
|
|
51
|
-
await new Promise((r) => setTimeout(r, DURATION.normal + 50));
|
|
290
|
+
await after(CLOSED);
|
|
52
291
|
const warnings = warn.mock.calls.map(([message]) => String(message));
|
|
53
292
|
warn.mockRestore();
|
|
54
293
|
expect(warnings.filter((message) => message.includes('NG0953'))).toEqual(
|
|
55
294
|
[],
|
|
56
295
|
);
|
|
296
|
+
expect(host.closes()).toBe(0);
|
|
57
297
|
});
|
|
298
|
+
});
|
|
58
299
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
300
|
+
@Component({
|
|
301
|
+
imports: [
|
|
302
|
+
UiAlertDialog,
|
|
303
|
+
UiAlertDialogHeader,
|
|
304
|
+
UiAlertDialogTitle,
|
|
305
|
+
UiAlertDialogDescription,
|
|
306
|
+
UiAlertDialogFooter,
|
|
307
|
+
UiAlertDialogAction,
|
|
308
|
+
LYNX_ELEMENTS,
|
|
309
|
+
],
|
|
310
|
+
template: `
|
|
311
|
+
<ui-alert-dialog
|
|
312
|
+
[variant]="variant()"
|
|
313
|
+
[(open)]="open"
|
|
314
|
+
class="tall"
|
|
315
|
+
(closed)="closes.set(closes() + 1)"
|
|
316
|
+
>
|
|
317
|
+
<ui-alert-dialog-header>
|
|
318
|
+
<ui-alert-dialog-title>Delete Photo?</ui-alert-dialog-title>
|
|
319
|
+
<ui-alert-dialog-description>
|
|
320
|
+
It's deleted from all your devices.
|
|
321
|
+
</ui-alert-dialog-description>
|
|
322
|
+
</ui-alert-dialog-header>
|
|
323
|
+
<ui-alert-dialog-footer [orientation]="orientation()">
|
|
324
|
+
<ui-alert-dialog-action
|
|
325
|
+
variant="cancel"
|
|
326
|
+
(pressed)="choices.push('cancel')"
|
|
327
|
+
>
|
|
328
|
+
Cancel
|
|
329
|
+
</ui-alert-dialog-action>
|
|
330
|
+
@if (withSave()) {
|
|
331
|
+
<ui-alert-dialog-action
|
|
332
|
+
[preferred]="savePreferred()"
|
|
333
|
+
(pressed)="choices.push('save')"
|
|
334
|
+
>
|
|
335
|
+
Save
|
|
336
|
+
</ui-alert-dialog-action>
|
|
337
|
+
}
|
|
338
|
+
<ui-alert-dialog-action
|
|
339
|
+
variant="destructive"
|
|
340
|
+
class="wide"
|
|
341
|
+
[preferred]="deletePreferred()"
|
|
342
|
+
[accessibilityLabel]="deleteLabel()"
|
|
343
|
+
(pressed)="choices.push('delete')"
|
|
344
|
+
>
|
|
345
|
+
Delete
|
|
346
|
+
</ui-alert-dialog-action>
|
|
347
|
+
</ui-alert-dialog-footer>
|
|
348
|
+
</ui-alert-dialog>
|
|
349
|
+
`,
|
|
350
|
+
})
|
|
351
|
+
class GlassAlertHost {
|
|
352
|
+
readonly variant = signal<AlertDialogVariant>('glass');
|
|
353
|
+
readonly open = signal(false);
|
|
354
|
+
readonly orientation = signal<AlertDialogFooterOrientation>('auto');
|
|
355
|
+
readonly withSave = signal(false);
|
|
356
|
+
readonly savePreferred = signal(false);
|
|
357
|
+
readonly deletePreferred = signal(false);
|
|
358
|
+
readonly deleteLabel = signal<string | undefined>(undefined);
|
|
359
|
+
readonly closes = signal(0);
|
|
360
|
+
readonly choices: string[] = [];
|
|
361
|
+
}
|
|
79
362
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
expect(getByText('Sure?')).toBeTruthy();
|
|
83
|
-
expect(getByText('Cannot undo')).toBeTruthy();
|
|
84
|
-
expect(getByText('Buttons')).toBeTruthy();
|
|
85
|
-
});
|
|
363
|
+
const IOS_26 = { platform: 'iOS', osVersion: '26.0' };
|
|
364
|
+
const IOS_18 = { platform: 'iOS', osVersion: '18.6' };
|
|
86
365
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
366
|
+
/**
|
|
367
|
+
* Renders the glass alert on a platform, as `SystemInfo` reports it, or with
|
|
368
|
+
* none, which is Material's design.
|
|
369
|
+
*/
|
|
370
|
+
const renderGlassAlert = async (
|
|
371
|
+
platform: { platform: string; osVersion: string } | null = IOS_26,
|
|
372
|
+
) => {
|
|
373
|
+
vi.stubGlobal('SystemInfo', platform ?? undefined);
|
|
374
|
+
const result = await render(GlassAlertHost, {
|
|
375
|
+
providers: [{ provide: LynxAccessibilitySettings, useValue: settings }],
|
|
93
376
|
});
|
|
377
|
+
const host = result.componentRef.instance as GlassAlertHost;
|
|
378
|
+
const panel = () =>
|
|
379
|
+
result.container.querySelector('[accessibility-exclusive-focus]')!;
|
|
380
|
+
const shell = () => panel().parentElement!;
|
|
381
|
+
/**
|
|
382
|
+
* An action's own element, from its label.
|
|
383
|
+
*/
|
|
384
|
+
const action = (label: string) => result.getByText(label).parentElement!;
|
|
385
|
+
const update = async (change: () => void) => {
|
|
386
|
+
change();
|
|
387
|
+
await waitForUpdate();
|
|
388
|
+
};
|
|
389
|
+
return {
|
|
390
|
+
...result,
|
|
391
|
+
host,
|
|
392
|
+
panel,
|
|
393
|
+
shell,
|
|
394
|
+
dim: () => shell().previousElementSibling!,
|
|
395
|
+
action,
|
|
396
|
+
footer: () => action('Cancel').parentElement!,
|
|
397
|
+
style: (element: Element) => element.getAttribute('style') ?? '',
|
|
398
|
+
update,
|
|
399
|
+
};
|
|
400
|
+
};
|
|
94
401
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
402
|
+
describe('UiAlertDialog with variant="glass"', () => {
|
|
403
|
+
describe('on iOS 26 and later', () => {
|
|
404
|
+
it('draws a Liquid Glass panel with concentric corners', async () => {
|
|
405
|
+
const { panel, style } = await renderGlassAlert();
|
|
406
|
+
expect(panel().tagName.toLowerCase()).toBe('blur-view');
|
|
407
|
+
expect(panel().getAttribute('blur-effect')).toBe('glass');
|
|
408
|
+
// 40 = the 24px capsule buttons' radius + their 16px inset.
|
|
409
|
+
expect(style(panel())).toContain('border-radius: 40px');
|
|
410
|
+
expect(panel().classList).toContain('max-w-[320px]');
|
|
411
|
+
expect(panel().classList).toContain('pt-[22px]');
|
|
412
|
+
// The dialog's class still goes on the panel.
|
|
413
|
+
expect(panel().classList).toContain('tall');
|
|
103
414
|
});
|
|
104
|
-
|
|
105
|
-
|
|
415
|
+
|
|
416
|
+
it('sets the title and message leading, in the glass colors', async () => {
|
|
417
|
+
const { getByText, container } = await renderGlassAlert();
|
|
418
|
+
expect(getByText('Delete Photo?').classList).toContain('text-[17px]');
|
|
419
|
+
expect(getByText('Delete Photo?').classList).toContain(
|
|
420
|
+
'text-glass-foreground',
|
|
421
|
+
);
|
|
422
|
+
expect(getByText('Delete Photo?').classList).not.toContain('text-center');
|
|
423
|
+
expect(
|
|
424
|
+
getByText("It's deleted from all your devices.").classList,
|
|
425
|
+
).toContain('text-glass-muted-foreground');
|
|
426
|
+
expect(container.querySelector('.px-\\[14px\\]')).toBeTruthy();
|
|
106
427
|
});
|
|
107
428
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
429
|
+
it('puts two actions side by side on capsules, cancel first', async () => {
|
|
430
|
+
const { action, footer, style } = await renderGlassAlert();
|
|
431
|
+
expect(footer().classList).toContain('flex-row');
|
|
432
|
+
expect(action('Cancel').classList).toContain('rounded-full');
|
|
433
|
+
expect(action('Cancel').classList).toContain('flex-1');
|
|
434
|
+
expect(action('Cancel').classList).toContain('h-[48px]');
|
|
435
|
+
expect(style(action('Cancel'))).toContain('order: 0');
|
|
436
|
+
expect(style(action('Delete'))).toContain('order: 1');
|
|
437
|
+
// The capsule's fill: the glass's foreground, faintly.
|
|
438
|
+
const fill = action('Cancel').firstElementChild!;
|
|
439
|
+
expect(fill.classList).toContain('bg-glass-foreground');
|
|
440
|
+
expect(style(fill)).toContain('opacity: 0.12');
|
|
441
|
+
expect(action('Delete').lastElementChild!.classList).toContain(
|
|
442
|
+
'text-destructive',
|
|
443
|
+
);
|
|
444
|
+
});
|
|
112
445
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
446
|
+
it('stacks three actions, cancel at the bottom wherever it is written', async () => {
|
|
447
|
+
const { host, action, footer, style, update } = await renderGlassAlert();
|
|
448
|
+
await update(() => host.withSave.set(true));
|
|
449
|
+
expect(footer().classList).toContain('flex-col');
|
|
450
|
+
expect(action('Cancel').classList).not.toContain('flex-1');
|
|
451
|
+
expect(style(action('Save'))).toContain('order: 0');
|
|
452
|
+
expect(style(action('Delete'))).toContain('order: 1');
|
|
453
|
+
expect(style(action('Cancel'))).toContain('order: 2');
|
|
454
|
+
});
|
|
118
455
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
await waitForUpdate();
|
|
130
|
-
// Wait out the close sequence, so #animateOut runs (and bails) while the
|
|
131
|
-
// component is still alive. Destroying it clears the sequence's timers.
|
|
132
|
-
await new Promise((r) => setTimeout(r, DURATION.normal + 50));
|
|
133
|
-
expect(inst.open()).toBe(false);
|
|
134
|
-
expect(closed).toHaveBeenCalledOnce();
|
|
135
|
-
});
|
|
456
|
+
it('lays its actions out as the footer says', async () => {
|
|
457
|
+
const { host, footer, update } = await renderGlassAlert();
|
|
458
|
+
await update(() => host.orientation.set('vertical'));
|
|
459
|
+
expect(footer().classList).toContain('flex-col');
|
|
460
|
+
await update(() => {
|
|
461
|
+
host.withSave.set(true);
|
|
462
|
+
host.orientation.set('horizontal');
|
|
463
|
+
});
|
|
464
|
+
expect(footer().classList).toContain('flex-row');
|
|
465
|
+
});
|
|
136
466
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
467
|
+
it('fills the preferred action with the tint, or the destructive color', async () => {
|
|
468
|
+
const { host, action, update } = await renderGlassAlert();
|
|
469
|
+
await update(() => {
|
|
470
|
+
host.withSave.set(true);
|
|
471
|
+
host.savePreferred.set(true);
|
|
472
|
+
});
|
|
473
|
+
expect(action('Save').classList).toContain('bg-glass-tint');
|
|
474
|
+
expect(action('Save').lastElementChild!.classList).toContain(
|
|
475
|
+
'text-glass-tint-foreground',
|
|
476
|
+
);
|
|
477
|
+
expect(action('Save').lastElementChild!.classList).toContain(
|
|
478
|
+
'font-semibold',
|
|
479
|
+
);
|
|
480
|
+
// Its highlight is the tint's foreground, shown only under the finger.
|
|
481
|
+
expect(action('Save').firstElementChild!.classList).toContain(
|
|
482
|
+
'bg-glass-tint-foreground',
|
|
483
|
+
);
|
|
142
484
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
485
|
+
await update(() => host.deletePreferred.set(true));
|
|
486
|
+
expect(action('Delete').classList).toContain('bg-destructive');
|
|
487
|
+
expect(action('Delete').lastElementChild!.classList).toContain(
|
|
488
|
+
'text-destructive-foreground',
|
|
489
|
+
);
|
|
146
490
|
});
|
|
147
|
-
|
|
148
|
-
|
|
491
|
+
|
|
492
|
+
it('deepens a capsule while it is pressed, by touch or by mouse', async () => {
|
|
493
|
+
const { host, action, style, update } = await renderGlassAlert();
|
|
494
|
+
const fill = () => action('Cancel').firstElementChild!;
|
|
495
|
+
await update(() => fireEvent.touchstart(action('Cancel')));
|
|
496
|
+
expect(style(fill())).toContain('opacity: 0.2');
|
|
497
|
+
await update(() => fireEvent.touchend(action('Cancel')));
|
|
498
|
+
expect(style(fill())).toContain('opacity: 0.12');
|
|
499
|
+
await update(() => fireLynxEvent(action('Cancel'), 'mousedown'));
|
|
500
|
+
expect(style(fill())).toContain('opacity: 0.2');
|
|
501
|
+
await update(() => fireLynxEvent(action('Cancel'), 'mouseleave'));
|
|
502
|
+
expect(style(fill())).toContain('opacity: 0.12');
|
|
503
|
+
await update(() => fireLynxEvent(action('Cancel'), 'mousedown'));
|
|
504
|
+
await update(() => fireLynxEvent(action('Cancel'), 'mouseup'));
|
|
505
|
+
expect(style(fill())).toContain('opacity: 0.12');
|
|
506
|
+
await update(() => fireEvent.touchstart(action('Cancel')));
|
|
507
|
+
await update(() => fireEvent.touchcancel(action('Cancel')));
|
|
508
|
+
expect(style(fill())).toContain('opacity: 0.12');
|
|
509
|
+
|
|
510
|
+
// A preferred capsule lightens under the finger instead.
|
|
511
|
+
await update(() => host.deletePreferred.set(true));
|
|
512
|
+
const highlight = () => action('Delete').firstElementChild!;
|
|
513
|
+
expect(style(highlight())).toContain('opacity: 0');
|
|
514
|
+
await update(() => fireEvent.touchstart(action('Delete')));
|
|
515
|
+
expect(style(highlight())).toContain('opacity: 0.1');
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('scales its glass in from a little larger as it fades, and fades it out', async () => {
|
|
519
|
+
const { host, shell, dim, update } = await renderGlassAlert();
|
|
520
|
+
await update(() => host.open.set(true));
|
|
521
|
+
await after(OPENED);
|
|
522
|
+
expect(animate).toHaveBeenCalledWith(dim(), [
|
|
523
|
+
START,
|
|
524
|
+
expect.any(String),
|
|
525
|
+
[{ opacity: 0 }, { opacity: 1 }],
|
|
526
|
+
expect.any(Object),
|
|
527
|
+
]);
|
|
528
|
+
// The view the glass is in, never the glass: on iOS a blur view loses
|
|
529
|
+
// its material at less than full opacity.
|
|
530
|
+
expect(animate).toHaveBeenCalledWith(shell(), [
|
|
531
|
+
START,
|
|
532
|
+
expect.any(String),
|
|
533
|
+
[
|
|
534
|
+
{ transform: 'scale(1.15) translateY(0px)', opacity: 0 },
|
|
535
|
+
{ transform: 'scale(1) translateY(0px)', opacity: 1 },
|
|
536
|
+
],
|
|
537
|
+
expect.objectContaining({ duration: DURATION.slow }),
|
|
538
|
+
]);
|
|
539
|
+
animate.mockClear();
|
|
540
|
+
|
|
541
|
+
await update(() => host.open.set(false));
|
|
542
|
+
await after(CLOSED);
|
|
543
|
+
expect(animate).toHaveBeenCalledWith(shell(), [
|
|
544
|
+
START,
|
|
545
|
+
expect.any(String),
|
|
546
|
+
[
|
|
547
|
+
{ transform: 'scale(1) translateY(0px)', opacity: 1 },
|
|
548
|
+
{ transform: 'scale(1) translateY(0px)', opacity: 0 },
|
|
549
|
+
],
|
|
550
|
+
expect.any(Object),
|
|
551
|
+
]);
|
|
552
|
+
expect(animate).toHaveBeenCalledWith(dim(), [
|
|
553
|
+
START,
|
|
554
|
+
expect.any(String),
|
|
555
|
+
[{ opacity: 1 }, { opacity: 0 }],
|
|
556
|
+
expect.any(Object),
|
|
557
|
+
]);
|
|
558
|
+
expect(host.closes()).toBe(1);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it('only fades under Reduce Motion', async () => {
|
|
562
|
+
const { host, shell, update } = await renderGlassAlert();
|
|
563
|
+
settings.reduceMotion.set(true);
|
|
564
|
+
await update(() => host.open.set(true));
|
|
565
|
+
await after(OPENED);
|
|
566
|
+
expect(animate).toHaveBeenCalledWith(shell(), [
|
|
567
|
+
START,
|
|
568
|
+
expect.any(String),
|
|
569
|
+
[{ opacity: 0 }, { opacity: 1 }],
|
|
570
|
+
expect.any(Object),
|
|
571
|
+
]);
|
|
572
|
+
await update(() => host.open.set(false));
|
|
573
|
+
await after(CLOSED);
|
|
574
|
+
expect(animate).toHaveBeenCalledWith(shell(), [
|
|
575
|
+
START,
|
|
576
|
+
expect.any(String),
|
|
577
|
+
[{ opacity: 1 }, { opacity: 0 }],
|
|
578
|
+
expect.any(Object),
|
|
579
|
+
]);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
it('stays open when the dimmed screen or the panel is tapped', async () => {
|
|
583
|
+
const { host, dim, panel, update } = await renderGlassAlert();
|
|
584
|
+
await update(() => host.open.set(true));
|
|
585
|
+
await after(OPENED);
|
|
586
|
+
await update(() => fireEvent.tap(dim()));
|
|
587
|
+
await update(() => fireEvent.tap(panel(), { eventType: 'catchEvent' }));
|
|
588
|
+
expect(host.open()).toBe(true);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('dims the screen at 20%, with no blur', async () => {
|
|
592
|
+
const { dim } = await renderGlassAlert();
|
|
593
|
+
expect(dim().classList).toContain('bg-black/20');
|
|
594
|
+
expect(dim().tagName.toLowerCase()).toBe('view');
|
|
149
595
|
});
|
|
150
|
-
inst.open.set(true);
|
|
151
|
-
for (let i = 0; i < 10; i++) await waitForUpdate();
|
|
152
|
-
overlay = container.querySelector('overlay');
|
|
153
|
-
expect(overlay?.getAttribute('style')).not.toContain('display: none');
|
|
154
596
|
});
|
|
155
597
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
598
|
+
describe('on iOS 15 to 25', () => {
|
|
599
|
+
it("draws that era's frosted alert, 270pt wide with 14pt corners", async () => {
|
|
600
|
+
const { panel, style, getByText } = await renderGlassAlert(IOS_18);
|
|
601
|
+
expect(panel().getAttribute('blur-effect')).toBe('extra-light');
|
|
602
|
+
expect(style(panel())).toContain('border-radius: 14px');
|
|
603
|
+
expect(panel().classList).toContain('w-[270px]');
|
|
604
|
+
expect(getByText('Delete Photo?').classList).toContain('text-center');
|
|
605
|
+
expect(
|
|
606
|
+
getByText("It's deleted from all your devices.").classList,
|
|
607
|
+
).toContain('text-[13px]');
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it('parts its buttons with hairlines', async () => {
|
|
611
|
+
const { host, action, footer, update } = await renderGlassAlert(IOS_18);
|
|
612
|
+
// The footer's own line over the buttons.
|
|
613
|
+
expect(footer().firstElementChild!.classList).toContain('h-[0.5px]');
|
|
614
|
+
// Beside the second button in a row, but not the first.
|
|
615
|
+
expect(action('Cancel').querySelector('.w-\\[0\\.5px\\]')).toBeNull();
|
|
616
|
+
expect(action('Delete').querySelector('.w-\\[0\\.5px\\]')).toBeTruthy();
|
|
617
|
+
expect(action('Delete').classList).toContain('h-[44px]');
|
|
618
|
+
expect(action('Delete').classList).toContain('flex-1');
|
|
619
|
+
|
|
620
|
+
// Above every button but the first in a stack.
|
|
621
|
+
await update(() => host.withSave.set(true));
|
|
622
|
+
expect(action('Save').querySelector('.h-\\[0\\.5px\\]')).toBeNull();
|
|
623
|
+
expect(action('Delete').querySelector('.h-\\[0\\.5px\\]')).toBeTruthy();
|
|
624
|
+
expect(action('Cancel').querySelector('.h-\\[0\\.5px\\]')).toBeTruthy();
|
|
625
|
+
expect(action('Cancel').classList).not.toContain('flex-1');
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it('sets the preferred action in bold, or else the cancel', async () => {
|
|
629
|
+
const { host, getByText, update } = await renderGlassAlert(IOS_18);
|
|
630
|
+
await update(() => host.withSave.set(true));
|
|
631
|
+
expect(getByText('Cancel').classList).toContain('font-semibold');
|
|
632
|
+
expect(getByText('Cancel').classList).toContain('text-glass-tint');
|
|
633
|
+
expect(getByText('Save').classList).not.toContain('font-semibold');
|
|
634
|
+
expect(getByText('Delete').classList).toContain('text-destructive');
|
|
635
|
+
|
|
636
|
+
await update(() => host.savePreferred.set(true));
|
|
637
|
+
expect(getByText('Save').classList).toContain('font-semibold');
|
|
638
|
+
expect(getByText('Cancel').classList).not.toContain('font-semibold');
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it('lights a button up while it is pressed', async () => {
|
|
642
|
+
const { action, style, update } = await renderGlassAlert(IOS_18);
|
|
643
|
+
const highlight = () => action('Delete').firstElementChild!;
|
|
644
|
+
expect(style(highlight())).toContain('opacity: 0');
|
|
645
|
+
await update(() => fireEvent.touchstart(action('Delete')));
|
|
646
|
+
expect(style(highlight())).toContain('opacity: 0.1');
|
|
647
|
+
});
|
|
165
648
|
});
|
|
166
|
-
});
|
|
167
649
|
|
|
168
|
-
describe('
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
650
|
+
describe('on Android', () => {
|
|
651
|
+
it("draws Material 3's basic dialog", async () => {
|
|
652
|
+
const { panel, style, getByText, dim, footer } =
|
|
653
|
+
await renderGlassAlert(null);
|
|
654
|
+
expect(style(panel())).toContain('border-radius: 28px');
|
|
655
|
+
expect(panel().classList).toContain('bg-secondary');
|
|
656
|
+
expect(panel().classList).toContain('p-[24px]');
|
|
657
|
+
expect(panel().classList).toContain('min-w-[280px]');
|
|
658
|
+
expect(getByText('Delete Photo?').classList).toContain('text-[24px]');
|
|
659
|
+
expect(
|
|
660
|
+
getByText("It's deleted from all your devices.").classList,
|
|
661
|
+
).toContain('text-[14px]');
|
|
662
|
+
expect(dim().classList).toContain('bg-black/[0.32]');
|
|
663
|
+
expect(footer().classList).toContain('justify-end');
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it('keeps its text buttons in a row at the end, cancel first', async () => {
|
|
667
|
+
const { host, action, footer, getByText, style, update } =
|
|
668
|
+
await renderGlassAlert(null);
|
|
669
|
+
await update(() => host.withSave.set(true));
|
|
670
|
+
expect(footer().classList).toContain('flex-row');
|
|
671
|
+
expect(style(action('Cancel'))).toContain('order: 0');
|
|
672
|
+
expect(action('Save').classList).toContain('h-[40px]');
|
|
673
|
+
expect(getByText('Save').classList).toContain('text-primary');
|
|
674
|
+
expect(getByText('Delete').classList).toContain('text-destructive');
|
|
675
|
+
// The state layer takes the label's color.
|
|
676
|
+
expect(action('Save').firstElementChild!.classList).toContain(
|
|
677
|
+
'bg-primary',
|
|
678
|
+
);
|
|
679
|
+
expect(action('Delete').firstElementChild!.classList).toContain(
|
|
680
|
+
'bg-destructive',
|
|
681
|
+
);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it('grows in and shrinks back as it fades', async () => {
|
|
685
|
+
const { host, shell, update } = await renderGlassAlert(null);
|
|
686
|
+
await update(() => host.open.set(true));
|
|
687
|
+
await after(OPENED);
|
|
688
|
+
expect(animate).toHaveBeenCalledWith(shell(), [
|
|
689
|
+
START,
|
|
690
|
+
expect.any(String),
|
|
691
|
+
[
|
|
692
|
+
{ transform: 'scale(0.9) translateY(0px)', opacity: 0 },
|
|
693
|
+
{ transform: 'scale(1) translateY(0px)', opacity: 1 },
|
|
694
|
+
],
|
|
695
|
+
expect.any(Object),
|
|
696
|
+
]);
|
|
697
|
+
await update(() => host.open.set(false));
|
|
698
|
+
await after(CLOSED);
|
|
699
|
+
expect(animate).toHaveBeenCalledWith(shell(), [
|
|
700
|
+
START,
|
|
701
|
+
expect.any(String),
|
|
702
|
+
[
|
|
703
|
+
{ transform: 'scale(1) translateY(0px)', opacity: 1 },
|
|
704
|
+
{ transform: 'scale(0.95) translateY(0px)', opacity: 0 },
|
|
705
|
+
],
|
|
706
|
+
expect.any(Object),
|
|
707
|
+
]);
|
|
708
|
+
});
|
|
172
709
|
});
|
|
173
710
|
|
|
174
|
-
it('
|
|
175
|
-
const {
|
|
176
|
-
|
|
711
|
+
it('emits the chosen action and closes, once', async () => {
|
|
712
|
+
const { host, getByRole, action, update } = await renderGlassAlert();
|
|
713
|
+
await update(() => host.open.set(true));
|
|
714
|
+
await after(OPENED);
|
|
715
|
+
expect(getByRole('button', { name: 'Delete' })).toBeTruthy();
|
|
716
|
+
await update(() => fireEvent.tap(action('Delete')));
|
|
717
|
+
// A second tap while it closes does nothing.
|
|
718
|
+
await update(() => fireEvent.tap(action('Cancel')));
|
|
719
|
+
expect(host.choices).toEqual(['delete']);
|
|
720
|
+
expect(host.open()).toBe(false);
|
|
721
|
+
await after(CLOSED);
|
|
722
|
+
expect(host.closes()).toBe(1);
|
|
177
723
|
});
|
|
178
724
|
|
|
179
|
-
it('
|
|
180
|
-
const {
|
|
181
|
-
|
|
725
|
+
it('makes an action one element that says its accessibility label', async () => {
|
|
726
|
+
const { host, getByRole, action, update } = await renderGlassAlert();
|
|
727
|
+
// Open: a screen reader reaches nothing in the hidden overlay.
|
|
728
|
+
await update(() => {
|
|
729
|
+
host.open.set(true);
|
|
730
|
+
host.deleteLabel.set('Delete the photo');
|
|
731
|
+
});
|
|
732
|
+
await after(OPENED);
|
|
733
|
+
expect(getByRole('button', { name: 'Delete the photo' })).toBe(
|
|
734
|
+
action('Delete'),
|
|
735
|
+
);
|
|
736
|
+
expect(
|
|
737
|
+
action('Delete').lastElementChild!.getAttribute('accessibility-traits'),
|
|
738
|
+
).toBeNull();
|
|
182
739
|
});
|
|
183
740
|
|
|
184
|
-
it('
|
|
185
|
-
const {
|
|
186
|
-
|
|
741
|
+
it('draws its own panel again with variant="default", and its actions as the default buttons', async () => {
|
|
742
|
+
const { host, panel, action, getByText, style, update } =
|
|
743
|
+
await renderGlassAlert();
|
|
744
|
+
await update(() => host.variant.set('default'));
|
|
745
|
+
expect(panel().tagName.toLowerCase()).toBe('view');
|
|
746
|
+
expect(panel().classList).toContain('bg-background');
|
|
747
|
+
expect(action('Cancel').classList).toContain('border-border');
|
|
748
|
+
expect(action('Delete').classList).toContain('bg-destructive');
|
|
749
|
+
// The given class wins over the action's own.
|
|
750
|
+
expect(action('Delete').classList).toContain('wide');
|
|
751
|
+
expect(getByText('Cancel').classList).toContain('text-foreground');
|
|
752
|
+
expect(getByText('Delete').classList).toContain(
|
|
753
|
+
'text-destructive-foreground',
|
|
754
|
+
);
|
|
755
|
+
// Written order, no fill or hairline.
|
|
756
|
+
expect(style(action('Cancel'))).not.toContain('order');
|
|
757
|
+
expect(action('Cancel').querySelector('.absolute')).toBeNull();
|
|
758
|
+
|
|
759
|
+
await update(() => host.withSave.set(true));
|
|
760
|
+
expect(action('Save').classList).toContain('bg-primary');
|
|
761
|
+
expect(getByText('Save').classList).toContain('text-primary-foreground');
|
|
762
|
+
|
|
763
|
+
// A pressed button dims.
|
|
764
|
+
await update(() => fireEvent.touchstart(action('Save')));
|
|
765
|
+
expect(style(action('Save'))).toContain('opacity: 0.8');
|
|
766
|
+
await update(() => fireEvent.touchend(action('Save')));
|
|
767
|
+
expect(style(action('Save'))).not.toContain('opacity');
|
|
187
768
|
});
|
|
188
769
|
});
|
|
770
|
+
|
|
771
|
+
@Component({
|
|
772
|
+
imports: [
|
|
773
|
+
UiAlertDialogHeader,
|
|
774
|
+
UiAlertDialogTitle,
|
|
775
|
+
UiAlertDialogDescription,
|
|
776
|
+
UiAlertDialogFooter,
|
|
777
|
+
LYNX_ELEMENTS,
|
|
778
|
+
],
|
|
779
|
+
template: `
|
|
780
|
+
<ui-alert-dialog-header>
|
|
781
|
+
<ui-alert-dialog-title>Alone</ui-alert-dialog-title>
|
|
782
|
+
<ui-alert-dialog-description
|
|
783
|
+
>Outside a dialog</ui-alert-dialog-description
|
|
784
|
+
>
|
|
785
|
+
</ui-alert-dialog-header>
|
|
786
|
+
<ui-alert-dialog-footer><text>Footer</text></ui-alert-dialog-footer>
|
|
787
|
+
`,
|
|
788
|
+
})
|
|
789
|
+
class LoosePartsHost {}
|
|
790
|
+
|
|
791
|
+
it("draws an alert dialog's parts in the default look outside one", async () => {
|
|
792
|
+
const { getByText } = await render(LoosePartsHost);
|
|
793
|
+
expect(getByText('Alone').classList).toContain('text-lg');
|
|
794
|
+
expect(getByText('Outside a dialog').classList).toContain('text-sm');
|
|
795
|
+
expect(getByText('Footer').parentElement!.classList).toContain('pt-4');
|
|
796
|
+
expect(getByText('Alone').parentElement!.parentElement!.classList).toContain(
|
|
797
|
+
'gap-1.5',
|
|
798
|
+
);
|
|
799
|
+
});
|