@blotch/dolan 22.2.2 → 22.2.4
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.d.ts +6 -2
- package/dist/commands/add.js +41 -11
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/add.test.js +162 -9
- package/dist/commands/add.test.js.map +1 -1
- package/dist/commands/diff.d.ts +2 -1
- package/dist/commands/diff.js +38 -25
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/diff.test.js +72 -11
- package/dist/commands/diff.test.js.map +1 -1
- package/dist/commands/doctor.js +228 -34
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +367 -10
- package/dist/commands/doctor.test.js.map +1 -1
- package/dist/commands/eject.js +7 -0
- package/dist/commands/eject.js.map +1 -1
- package/dist/commands/eject.test.js +39 -0
- package/dist/commands/eject.test.js.map +1 -1
- package/dist/commands/info.js +3 -2
- package/dist/commands/info.js.map +1 -1
- package/dist/commands/init.d.ts +37 -1
- package/dist/commands/init.js +169 -40
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/init.test.js +392 -27
- package/dist/commands/init.test.js.map +1 -1
- package/dist/commands/install-packages.d.ts +6 -0
- package/dist/commands/install-packages.js +20 -0
- package/dist/commands/install-packages.js.map +1 -0
- package/dist/commands/install-packages.test.d.ts +1 -0
- package/dist/commands/install-packages.test.js +55 -0
- package/dist/commands/install-packages.test.js.map +1 -0
- package/dist/commands/list.js +21 -14
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/list.test.js +62 -5
- package/dist/commands/list.test.js.map +1 -1
- package/dist/commands/outdated.js +11 -10
- package/dist/commands/outdated.js.map +1 -1
- package/dist/commands/outdated.test.js +20 -5
- package/dist/commands/outdated.test.js.map +1 -1
- package/dist/commands/remove.d.ts +4 -0
- package/dist/commands/remove.js +17 -4
- package/dist/commands/remove.js.map +1 -1
- package/dist/commands/remove.test.js +53 -2
- package/dist/commands/remove.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 +11 -0
- package/dist/commands/update.js +174 -58
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/update.test.js +587 -33
- package/dist/commands/update.test.js.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/lockfile.d.ts +7 -0
- package/dist/lockfile.js +36 -0
- package/dist/lockfile.js.map +1 -1
- package/dist/lockfile.test.d.ts +1 -0
- package/dist/lockfile.test.js +25 -0
- package/dist/lockfile.test.js.map +1 -0
- package/dist/registry.d.ts +14 -0
- package/dist/registry.js +78 -24
- package/dist/registry.js.map +1 -1
- package/dist/registry.test.js +119 -38
- package/dist/registry.test.js.map +1 -1
- package/dist/test-utils.d.ts +5 -0
- package/dist/test-utils.js +6 -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 +14 -0
- package/dist/testing/set-input-signal.js +21 -0
- package/dist/testing/set-input-signal.js.map +1 -0
- package/dist/testing/set-input-signal.test.d.ts +1 -0
- package/dist/testing/set-input-signal.test.js +20 -0
- package/dist/testing/set-input-signal.test.js.map +1 -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 +283 -151
- package/dist/ui/components/action-sheet/action-sheet.ts +77 -43
- 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 +208 -94
- package/dist/ui/components/alert-dialog/alert-dialog.ts +75 -38
- package/dist/ui/components/area-chart/area-chart.test.ts +134 -119
- package/dist/ui/components/area-chart/area-chart.ts +100 -315
- package/dist/ui/components/area-chart/index.ts +2 -3
- 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 +12 -14
- package/dist/ui/components/bar-chart/bar-chart.test.ts +172 -45
- package/dist/ui/components/bar-chart/bar-chart.ts +140 -294
- package/dist/ui/components/bar-chart/index.ts +3 -3
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +386 -128
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +170 -53
- package/dist/ui/components/button/button.test.ts +611 -2
- package/dist/ui/components/button/button.ts +434 -49
- package/dist/ui/components/button/index.ts +2 -0
- package/dist/ui/components/button-group/button-group.test.ts +484 -2
- package/dist/ui/components/button-group/button-group.ts +380 -68
- package/dist/ui/components/button-group/index.ts +3 -0
- package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +183 -58
- package/dist/ui/components/candlestick-chart/candlestick-chart.ts +150 -323
- package/dist/ui/components/candlestick-chart/index.ts +3 -4
- package/dist/ui/components/card/card.test.ts +82 -62
- package/dist/ui/components/chart/chart-focus.test.ts +132 -0
- package/dist/ui/components/chart/chart-focus.ts +115 -0
- package/dist/ui/components/chart/chart-presets.test.ts +335 -0
- package/dist/ui/components/chart/chart-presets.ts +278 -0
- package/dist/ui/components/chart/chart-scene.test.ts +589 -0
- package/dist/ui/components/chart/chart-scene.ts +483 -0
- package/dist/ui/components/chart/chart-theme.test.ts +85 -0
- package/dist/ui/components/chart/chart-theme.ts +210 -0
- package/dist/ui/components/chart/chart-zoom.test.ts +159 -0
- package/dist/ui/components/chart/chart-zoom.ts +162 -0
- package/dist/ui/components/chart/chart.test.ts +1017 -0
- package/dist/ui/components/chart/chart.ts +934 -0
- package/dist/ui/components/chart/index.ts +24 -0
- package/dist/ui/components/checkbox/checkbox.test.ts +124 -42
- package/dist/ui/components/checkbox/checkbox.ts +23 -23
- package/dist/ui/components/collapsible/collapsible.test.ts +158 -81
- package/dist/ui/components/collapsible/collapsible.ts +19 -14
- 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 +213 -108
- package/dist/ui/components/dialog/dialog.ts +72 -35
- package/dist/ui/components/dock/dock-scroll.test.ts +130 -0
- package/dist/ui/components/dock/dock-scroll.ts +133 -0
- package/dist/ui/components/dock/dock.test.ts +749 -0
- package/dist/ui/components/dock/dock.ts +960 -0
- package/dist/ui/components/dock/index.ts +2 -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 +338 -0
- package/dist/ui/components/glass/glass.ts +360 -0
- package/dist/ui/components/glass/index.ts +18 -0
- package/dist/ui/components/icon/icon-theme.test.ts +169 -0
- package/dist/ui/components/icon/icon-theme.ts +227 -0
- package/dist/ui/components/icon/icon.test.ts +85 -25
- package/dist/ui/components/icon/icon.ts +38 -21
- package/dist/ui/components/input/input.test.ts +178 -101
- package/dist/ui/components/input/input.ts +15 -12
- package/dist/ui/components/label/label.test.ts +11 -24
- package/dist/ui/components/line-chart/index.ts +2 -3
- package/dist/ui/components/line-chart/line-chart.test.ts +149 -56
- package/dist/ui/components/line-chart/line-chart.ts +83 -241
- package/dist/ui/components/list/list.test.ts +119 -21
- package/dist/ui/components/list/list.ts +21 -15
- 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 +632 -0
- package/dist/ui/components/menu/menu-popup.ts +544 -0
- package/dist/ui/components/menu/menu.test.ts +429 -0
- package/dist/ui/components/menu/menu.ts +415 -0
- package/dist/ui/components/nav-drawer/nav-drawer.test.ts +435 -191
- package/dist/ui/components/nav-drawer/nav-drawer.ts +127 -56
- package/dist/ui/components/pagination/pagination.test.ts +177 -65
- package/dist/ui/components/pagination/pagination.ts +55 -8
- package/dist/ui/components/progress/progress.test.ts +102 -46
- package/dist/ui/components/progress/progress.ts +52 -54
- package/dist/ui/components/radio-group/radio-group.test.ts +128 -83
- package/dist/ui/components/radio-group/radio-group.ts +24 -24
- package/dist/ui/components/scatter-chart/index.ts +2 -3
- package/dist/ui/components/scatter-chart/scatter-chart.test.ts +131 -26
- package/dist/ui/components/scatter-chart/scatter-chart.ts +104 -250
- package/dist/ui/components/scroll-area/scroll-area.test.ts +95 -24
- package/dist/ui/components/scroll-area/scroll-area.ts +8 -8
- package/dist/ui/components/select/select.test.ts +138 -78
- package/dist/ui/components/select/select.ts +17 -11
- package/dist/ui/components/separator/separator.test.ts +18 -17
- package/dist/ui/components/sheet/sheet.test.ts +64 -11
- package/dist/ui/components/sheet/sheet.ts +9 -1
- package/dist/ui/components/skeleton/skeleton.test.ts +24 -7
- package/dist/ui/components/skeleton/skeleton.ts +10 -16
- package/dist/ui/components/spinner/spinner.test.ts +67 -22
- package/dist/ui/components/spinner/spinner.ts +48 -41
- package/dist/ui/components/switch/switch.test.ts +116 -46
- package/dist/ui/components/switch/switch.ts +8 -13
- 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/tabs.test.ts +215 -157
- package/dist/ui/components/tabs/tabs.ts +110 -118
- package/dist/ui/components/textarea/textarea.test.ts +206 -100
- package/dist/ui/components/textarea/textarea.ts +14 -12
- package/dist/ui/components/toast/toast.test.ts +436 -315
- package/dist/ui/components/toast/toast.ts +62 -55
- package/dist/ui/components/toggle/toggle.test.ts +247 -140
- package/dist/ui/theme/chart-colors.test.ts +35 -0
- package/dist/ui/theme/dark.css +46 -2
- package/dist/ui/theme/default.css +63 -7
- package/dist/ui/theme/icon-colors.test.ts +35 -0
- 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 +98 -25
- package/dist/ui/theme/tailwind.css +113 -0
- package/dist/ui/utils/animate.test.ts +13 -1
- package/dist/utils/analyze.d.ts +14 -0
- package/dist/utils/analyze.js +15 -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 +29 -0
- package/dist/utils/installed.js +43 -0
- package/dist/utils/installed.js.map +1 -0
- package/dist/utils/installed.test.d.ts +1 -0
- package/dist/utils/installed.test.js +57 -0
- package/dist/utils/installed.test.js.map +1 -0
- package/dist/utils/interactive.d.ts +9 -0
- package/dist/utils/interactive.js +10 -0
- package/dist/utils/interactive.js.map +1 -0
- package/dist/utils/interactive.test.d.ts +1 -0
- package/dist/utils/interactive.test.js +24 -0
- package/dist/utils/interactive.test.js.map +1 -0
- package/dist/utils/packages.d.ts +36 -0
- package/dist/utils/packages.js +70 -0
- package/dist/utils/packages.js.map +1 -0
- package/dist/utils/packages.test.d.ts +1 -0
- package/dist/utils/packages.test.js +89 -0
- package/dist/utils/packages.test.js.map +1 -0
- package/dist/utils/resolve-paths.d.ts +17 -1
- package/dist/utils/resolve-paths.js +21 -3
- package/dist/utils/resolve-paths.js.map +1 -1
- package/dist/utils/resolve-paths.test.js +38 -2
- package/dist/utils/resolve-paths.test.js.map +1 -1
- package/dist/utils/tailwind.d.ts +25 -0
- package/dist/utils/tailwind.js +51 -0
- package/dist/utils/tailwind.js.map +1 -0
- package/dist/utils/tailwind.test.d.ts +1 -0
- package/dist/utils/tailwind.test.js +71 -0
- package/dist/utils/tailwind.test.js.map +1 -0
- package/package.json +13 -1
- package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -121
- package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -174
- package/dist/ui/components/button/button-render.test.ts +0 -113
- 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 -174
- package/dist/ui/components/cartesian-chart/cartesian-chart-render.test.ts +0 -188
- package/dist/ui/components/cartesian-chart/cartesian-chart.test.ts +0 -525
- package/dist/ui/components/cartesian-chart/cartesian-chart.ts +0 -1140
- package/dist/ui/components/cartesian-chart/index.ts +0 -18
- package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -121
- package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -157
|
@@ -1,8 +1,24 @@
|
|
|
1
|
-
import { Component, signal } from '@angular/core';
|
|
2
|
-
import {
|
|
1
|
+
import { Component, signal, viewChild } from '@angular/core';
|
|
2
|
+
import {
|
|
3
|
+
fireEvent,
|
|
4
|
+
render,
|
|
5
|
+
waitForUpdate,
|
|
6
|
+
} from '@blotch/angular-lynx-testing-library';
|
|
3
7
|
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
4
|
-
import {
|
|
5
|
-
|
|
8
|
+
import {
|
|
9
|
+
type MockInstance,
|
|
10
|
+
afterEach,
|
|
11
|
+
beforeEach,
|
|
12
|
+
describe,
|
|
13
|
+
expect,
|
|
14
|
+
it,
|
|
15
|
+
vi,
|
|
16
|
+
} from 'vitest';
|
|
17
|
+
import { DURATION } from '../../utils/animate';
|
|
18
|
+
import {
|
|
19
|
+
type LynxMainThread,
|
|
20
|
+
lynxMainThread,
|
|
21
|
+
} from '../../../test-utils/lynx-main-thread';
|
|
6
22
|
import {
|
|
7
23
|
UiAlertDialog,
|
|
8
24
|
UiAlertDialogDescription,
|
|
@@ -12,7 +28,6 @@ import {
|
|
|
12
28
|
} from './alert-dialog';
|
|
13
29
|
|
|
14
30
|
@Component({
|
|
15
|
-
standalone: true,
|
|
16
31
|
imports: [
|
|
17
32
|
UiAlertDialog,
|
|
18
33
|
UiAlertDialogHeader,
|
|
@@ -22,120 +37,219 @@ import {
|
|
|
22
37
|
LYNX_ELEMENTS,
|
|
23
38
|
],
|
|
24
39
|
template: `
|
|
25
|
-
|
|
26
|
-
<ui-alert-dialog
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
@if (shown()) {
|
|
41
|
+
<ui-alert-dialog
|
|
42
|
+
[(open)]="open"
|
|
43
|
+
class="max-w-sm"
|
|
44
|
+
(closed)="closes.set(closes() + 1)"
|
|
45
|
+
>
|
|
46
|
+
<ui-alert-dialog-header class="gap-2">
|
|
47
|
+
<ui-alert-dialog-title class="text-xl">Sure?</ui-alert-dialog-title>
|
|
48
|
+
<ui-alert-dialog-description class="italic">
|
|
49
|
+
Cannot undo
|
|
50
|
+
</ui-alert-dialog-description>
|
|
51
|
+
</ui-alert-dialog-header>
|
|
52
|
+
<ui-alert-dialog-footer class="justify-start">
|
|
53
|
+
<text>Buttons</text>
|
|
54
|
+
</ui-alert-dialog-footer>
|
|
55
|
+
</ui-alert-dialog>
|
|
56
|
+
}
|
|
32
57
|
`,
|
|
33
58
|
})
|
|
34
59
|
class AlertDialogHost {
|
|
35
|
-
open =
|
|
60
|
+
readonly open = signal(false);
|
|
61
|
+
// Takes the alert dialog out of the tree, as an `@if` or a route change
|
|
62
|
+
// would.
|
|
63
|
+
readonly shown = signal(true);
|
|
64
|
+
readonly closes = signal(0);
|
|
65
|
+
readonly dialog = viewChild.required(UiAlertDialog);
|
|
36
66
|
}
|
|
37
67
|
|
|
68
|
+
const renderAlertDialog = async () => {
|
|
69
|
+
const result = await render(AlertDialogHost);
|
|
70
|
+
const panel = () =>
|
|
71
|
+
result.container.querySelector('[accessibility-exclusive-focus]')!;
|
|
72
|
+
return {
|
|
73
|
+
...result,
|
|
74
|
+
host: result.componentRef.instance as AlertDialogHost,
|
|
75
|
+
overlay: () => result.container.querySelector('overlay')!,
|
|
76
|
+
panel,
|
|
77
|
+
backdrop: () => panel().parentElement!,
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Waits for the alert dialog's open or close sequence, which runs on timers, then
|
|
83
|
+
* renders what it changed.
|
|
84
|
+
*/
|
|
85
|
+
const after = async (ms: number) => {
|
|
86
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
87
|
+
await waitForUpdate();
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Long enough for the open sequence, including the panel's 30ms stagger. The
|
|
92
|
+
* sequence is chained timers, and on Windows each can fire up to a clock
|
|
93
|
+
* tick (about 16ms) late, so 50ms was too short on CI's Windows runner.
|
|
94
|
+
*/
|
|
95
|
+
const OPENED = 150;
|
|
96
|
+
/** Long enough for the close animation and the hide after it, as above. */
|
|
97
|
+
const CLOSED = DURATION.normal + 150;
|
|
98
|
+
|
|
99
|
+
/** `__ElementAnimate`'s first argument for starting an animation. */
|
|
100
|
+
const START = 0;
|
|
101
|
+
/** `__ElementAnimate`'s first argument for canceling an animation. */
|
|
102
|
+
const CANCEL = 3;
|
|
103
|
+
|
|
38
104
|
describe('UiAlertDialog', () => {
|
|
39
|
-
|
|
40
|
-
|
|
105
|
+
// Watched per test: the testing library resets the main thread after each.
|
|
106
|
+
let animate: MockInstance<LynxMainThread['__ElementAnimate']>;
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
animate = vi.spyOn(lynxMainThread(), '__ElementAnimate');
|
|
109
|
+
});
|
|
110
|
+
afterEach(() => animate.mockRestore());
|
|
111
|
+
|
|
112
|
+
it('renders each part with its projected content', async () => {
|
|
113
|
+
const { getByText } = await renderAlertDialog();
|
|
41
114
|
expect(getByText('Sure?')).toBeTruthy();
|
|
42
115
|
expect(getByText('Cannot undo')).toBeTruthy();
|
|
43
116
|
expect(getByText('Buttons')).toBeTruthy();
|
|
44
117
|
});
|
|
45
118
|
|
|
46
|
-
it('
|
|
47
|
-
const {
|
|
48
|
-
|
|
49
|
-
expect(()
|
|
50
|
-
|
|
51
|
-
).
|
|
119
|
+
it("merges each part's class with its own, the given class winning", async () => {
|
|
120
|
+
const { container, getByText, panel } = await renderAlertDialog();
|
|
121
|
+
expect(panel().classList).toContain('bg-background');
|
|
122
|
+
expect(panel().classList).toContain('max-w-sm');
|
|
123
|
+
expect(getByText('Sure?').classList).toContain('font-semibold');
|
|
124
|
+
expect(getByText('Sure?').classList).toContain('text-xl');
|
|
125
|
+
expect(getByText('Cannot undo').classList).toContain('text-sm');
|
|
126
|
+
expect(getByText('Cannot undo').classList).toContain('italic');
|
|
127
|
+
// Each part's own view, not its host element, which keeps the bare class.
|
|
128
|
+
const header = container.querySelector('.flex-col.gap-2');
|
|
129
|
+
expect(header?.classList).not.toContain('gap-1.5');
|
|
130
|
+
const footer = container.querySelector('.pt-4');
|
|
131
|
+
expect(footer?.classList).toContain('justify-start');
|
|
132
|
+
expect(footer?.classList).not.toContain('justify-end');
|
|
52
133
|
});
|
|
53
134
|
|
|
54
|
-
it('
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
});
|
|
63
|
-
(inst as unknown as { panelRef: unknown }).panelRef = signal({
|
|
64
|
-
nativeElement: view,
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
inst.open.set(true);
|
|
68
|
-
await waitForUpdate();
|
|
69
|
-
await waitForUpdate();
|
|
70
|
-
await waitForUpdate();
|
|
71
|
-
|
|
72
|
-
inst.open.set(false);
|
|
73
|
-
await new Promise((r) => setTimeout(r, 320));
|
|
74
|
-
await waitForUpdate();
|
|
75
|
-
expect(inst.open()).toBe(false);
|
|
135
|
+
it('stays hidden until it is opened', async () => {
|
|
136
|
+
const { host, overlay, panel } = await renderAlertDialog();
|
|
137
|
+
await after(CLOSED);
|
|
138
|
+
expect(overlay().getAttribute('style')).toContain('display: none');
|
|
139
|
+
expect(panel().getAttribute('accessibility-exclusive-focus')).toBe('false');
|
|
140
|
+
expect(animate).not.toHaveBeenCalled();
|
|
141
|
+
// Starting closed isn't closing: `closed` waits for a real close.
|
|
142
|
+
expect(host.closes()).toBe(0);
|
|
76
143
|
});
|
|
77
144
|
|
|
78
|
-
it('
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
145
|
+
it('shows the overlay and animates the backdrop, then the panel, in', async () => {
|
|
146
|
+
const { host, overlay, backdrop, panel } = await renderAlertDialog();
|
|
147
|
+
host.open.set(true);
|
|
148
|
+
await after(OPENED);
|
|
149
|
+
expect(overlay().getAttribute('style')).not.toContain('display: none');
|
|
150
|
+
expect(animate).toHaveBeenCalledWith(backdrop(), [
|
|
151
|
+
START,
|
|
152
|
+
expect.any(String),
|
|
153
|
+
[{ opacity: 0 }, { opacity: 1 }],
|
|
154
|
+
expect.objectContaining({ duration: DURATION.normal }),
|
|
155
|
+
]);
|
|
156
|
+
expect(animate).toHaveBeenCalledWith(panel(), [
|
|
157
|
+
START,
|
|
158
|
+
expect.any(String),
|
|
159
|
+
[
|
|
160
|
+
{ transform: 'scale(0.9) translateY(10px)', opacity: 0 },
|
|
161
|
+
{ transform: 'scale(1) translateY(0px)', opacity: 1 },
|
|
162
|
+
],
|
|
163
|
+
expect.objectContaining({ duration: DURATION.slow }),
|
|
164
|
+
]);
|
|
88
165
|
});
|
|
89
166
|
|
|
90
|
-
it('
|
|
91
|
-
const {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
(inst as unknown as { backdropRef: unknown }).backdropRef = signal({
|
|
98
|
-
nativeElement: view,
|
|
99
|
-
});
|
|
100
|
-
(inst as unknown as { panelRef: unknown }).panelRef = signal({
|
|
101
|
-
nativeElement: view,
|
|
102
|
-
});
|
|
103
|
-
inst.open.set(true);
|
|
104
|
-
for (let i = 0; i < 10; i++) await waitForUpdate();
|
|
105
|
-
overlay = container.querySelector('overlay');
|
|
106
|
-
expect(overlay?.getAttribute('style')).not.toContain('display: none');
|
|
167
|
+
it('moves a screen reader into the panel and keeps it there while open', async () => {
|
|
168
|
+
const { host, panel } = await renderAlertDialog();
|
|
169
|
+
const invoke = vi.spyOn(host.dialog().panelRef().nativeElement, 'invoke');
|
|
170
|
+
host.open.set(true);
|
|
171
|
+
await after(OPENED);
|
|
172
|
+
expect(invoke).toHaveBeenCalledWith('requestAccessibilityFocus');
|
|
173
|
+
expect(panel().getAttribute('accessibility-exclusive-focus')).toBe('true');
|
|
107
174
|
});
|
|
108
175
|
|
|
109
|
-
it('
|
|
110
|
-
const {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const views = Array.from(container.querySelectorAll('view'));
|
|
115
|
-
expect(
|
|
116
|
-
views.some((el) => el.getAttribute('class')?.includes('custom-panel')),
|
|
117
|
-
).toBe(true);
|
|
118
|
-
});
|
|
119
|
-
});
|
|
176
|
+
it('animates out, then hides and emits closed', async () => {
|
|
177
|
+
const { host, overlay, backdrop, panel } = await renderAlertDialog();
|
|
178
|
+
host.open.set(true);
|
|
179
|
+
await after(OPENED);
|
|
180
|
+
animate.mockClear();
|
|
120
181
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
182
|
+
host.open.set(false);
|
|
183
|
+
await after(CLOSED);
|
|
184
|
+
expect(animate).toHaveBeenCalledWith(backdrop(), [
|
|
185
|
+
START,
|
|
186
|
+
expect.any(String),
|
|
187
|
+
[{ opacity: 1 }, { opacity: 0 }],
|
|
188
|
+
expect.any(Object),
|
|
189
|
+
]);
|
|
190
|
+
expect(animate).toHaveBeenCalledWith(panel(), [
|
|
191
|
+
START,
|
|
192
|
+
expect.any(String),
|
|
193
|
+
[
|
|
194
|
+
{ transform: 'scale(1) translateY(0px)', opacity: 1 },
|
|
195
|
+
{ transform: 'scale(0.95) translateY(10px)', opacity: 0 },
|
|
196
|
+
],
|
|
197
|
+
expect.any(Object),
|
|
198
|
+
]);
|
|
199
|
+
expect(overlay().getAttribute('style')).toContain('display: none');
|
|
200
|
+
expect(panel().getAttribute('accessibility-exclusive-focus')).toBe('false');
|
|
201
|
+
expect(host.closes()).toBe(1);
|
|
125
202
|
});
|
|
126
203
|
|
|
127
|
-
it('
|
|
128
|
-
const {
|
|
129
|
-
|
|
204
|
+
it('cancels the previous animations when it opens again', async () => {
|
|
205
|
+
const { host, backdrop, panel } = await renderAlertDialog();
|
|
206
|
+
host.open.set(true);
|
|
207
|
+
await after(OPENED);
|
|
208
|
+
host.open.set(false);
|
|
209
|
+
await after(CLOSED);
|
|
210
|
+
animate.mockClear();
|
|
211
|
+
|
|
212
|
+
host.open.set(true);
|
|
213
|
+
await after(OPENED);
|
|
214
|
+
// The close animations hold their last frame (fill: forwards) until
|
|
215
|
+
// they're canceled, which would hide the panel being opened.
|
|
216
|
+
expect(animate).toHaveBeenCalledWith(backdrop(), [
|
|
217
|
+
CANCEL,
|
|
218
|
+
expect.any(String),
|
|
219
|
+
]);
|
|
220
|
+
expect(animate).toHaveBeenCalledWith(panel(), [CANCEL, expect.any(String)]);
|
|
130
221
|
});
|
|
131
222
|
|
|
132
|
-
it('
|
|
133
|
-
const {
|
|
134
|
-
|
|
223
|
+
it('stays open when the backdrop or the panel is tapped', async () => {
|
|
224
|
+
const { host, backdrop, panel } = await renderAlertDialog();
|
|
225
|
+
host.open.set(true);
|
|
226
|
+
await after(OPENED);
|
|
227
|
+
// Unlike a dialog, an alert dialog waits for an answer from its actions.
|
|
228
|
+
fireEvent.tap(backdrop());
|
|
229
|
+
// The panel catches its taps, so they don't reach the backdrop either.
|
|
230
|
+
fireEvent.tap(panel(), { eventType: 'catchEvent' });
|
|
231
|
+
await waitForUpdate();
|
|
232
|
+
expect(host.open()).toBe(true);
|
|
135
233
|
});
|
|
136
234
|
|
|
137
|
-
it('
|
|
138
|
-
const
|
|
139
|
-
|
|
235
|
+
it('stops its close sequence when it is destroyed while closing', async () => {
|
|
236
|
+
const warn = vi.spyOn(console, 'warn');
|
|
237
|
+
const { host } = await renderAlertDialog();
|
|
238
|
+
host.open.set(true);
|
|
239
|
+
await after(OPENED);
|
|
240
|
+
host.open.set(false);
|
|
241
|
+
await after(0);
|
|
242
|
+
// Destroyed during the close animation. Its close timer
|
|
243
|
+
// (DURATION.normal + 20) used to fire anyway and emit `closed` from the
|
|
244
|
+
// destroyed output, which Angular warns about in dev builds.
|
|
245
|
+
host.shown.set(false);
|
|
246
|
+
await waitForUpdate();
|
|
247
|
+
await after(CLOSED);
|
|
248
|
+
const warnings = warn.mock.calls.map(([message]) => String(message));
|
|
249
|
+
warn.mockRestore();
|
|
250
|
+
expect(warnings.filter((message) => message.includes('NG0953'))).toEqual(
|
|
251
|
+
[],
|
|
252
|
+
);
|
|
253
|
+
expect(host.closes()).toBe(0);
|
|
140
254
|
});
|
|
141
255
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type ElementRef,
|
|
3
|
+
type OnDestroy,
|
|
3
4
|
Component,
|
|
4
5
|
ViewEncapsulation,
|
|
5
6
|
computed,
|
|
@@ -26,20 +27,24 @@ import { cn } from '../../utils/cn';
|
|
|
26
27
|
|
|
27
28
|
@Component({
|
|
28
29
|
selector: 'ui-alert-dialog',
|
|
29
|
-
standalone: true,
|
|
30
30
|
imports: [LYNX_ELEMENTS],
|
|
31
31
|
encapsulation: ViewEncapsulation.None,
|
|
32
32
|
template: `
|
|
33
33
|
<overlay [attr.visible]="overlayVisible()" [style]="overlayStyle()">
|
|
34
34
|
<view #backdrop [class]="backdropClass()">
|
|
35
|
-
<view
|
|
35
|
+
<view
|
|
36
|
+
#panel
|
|
37
|
+
[class]="panelClass()"
|
|
38
|
+
[accessibility-exclusive-focus]="overlayVisible()"
|
|
39
|
+
(catchtap)="onPanelTap()"
|
|
40
|
+
>
|
|
36
41
|
<ng-content />
|
|
37
42
|
</view>
|
|
38
43
|
</view>
|
|
39
44
|
</overlay>
|
|
40
45
|
`,
|
|
41
46
|
})
|
|
42
|
-
export class UiAlertDialog {
|
|
47
|
+
export class UiAlertDialog implements OnDestroy {
|
|
43
48
|
readonly open = model(false);
|
|
44
49
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
45
50
|
|
|
@@ -47,27 +52,38 @@ export class UiAlertDialog {
|
|
|
47
52
|
|
|
48
53
|
protected readonly overlayVisible = signal(false);
|
|
49
54
|
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
// Required: both are always in the template (the overlay hides with
|
|
56
|
+
// display:none rather than leaving the tree), and the sequences only read
|
|
57
|
+
// them from timers that start after the first render.
|
|
58
|
+
readonly backdropRef = viewChild.required<ElementRef>('backdrop');
|
|
59
|
+
readonly panelRef = viewChild.required<ElementRef>('panel');
|
|
52
60
|
#backdropAnim?: AnimationHandle;
|
|
53
61
|
#panelAnim?: AnimationHandle;
|
|
54
62
|
#hasBeenOpen = false;
|
|
63
|
+
// The open and close sequences' pending timers, so ngOnDestroy can stop
|
|
64
|
+
// them.
|
|
65
|
+
readonly #timers = new Set<ReturnType<typeof setTimeout>>();
|
|
55
66
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Plays the open or close sequence whenever `open` changes. A field rather
|
|
69
|
+
* than a constructor: Angular's test build keeps a function for a
|
|
70
|
+
* constructor that nothing calls, which an app's coverage report would show
|
|
71
|
+
* as uncovered.
|
|
72
|
+
*
|
|
73
|
+
* `#hasBeenOpen` prevents the close animation from running on the initial
|
|
74
|
+
* effect evaluation when `open` starts as false. Without this guard, the
|
|
75
|
+
* first run would call #doClose() immediately and emit `closed` before the
|
|
76
|
+
* dialog has ever been opened.
|
|
77
|
+
*/
|
|
78
|
+
protected readonly openAndClose = effect(() => {
|
|
79
|
+
const isOpen = this.open();
|
|
80
|
+
if (isOpen) {
|
|
81
|
+
this.#hasBeenOpen = true;
|
|
82
|
+
this.#doOpen();
|
|
83
|
+
} else if (this.#hasBeenOpen) {
|
|
84
|
+
this.#doClose();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
71
87
|
|
|
72
88
|
/**
|
|
73
89
|
* `width`/`height`/`top`/`left` are web-only necessities: on web
|
|
@@ -84,9 +100,9 @@ export class UiAlertDialog {
|
|
|
84
100
|
// `bg-black/50` supplies the dim. It lives on the backdrop specifically so
|
|
85
101
|
// the existing fadeIn/fadeOut (opacity 0↔1, staggered 30ms ahead of the
|
|
86
102
|
// panel scale-in) animate the dim in lockstep with the dialog appearing —
|
|
87
|
-
// no separate animation needed.
|
|
88
|
-
//
|
|
89
|
-
//
|
|
103
|
+
// no separate animation needed. It's a named color, not a theme one,
|
|
104
|
+
// because the dim is black in light and dark mode alike; it compiles to
|
|
105
|
+
// `rgb(0 0 0 / 0.5)`, which Lynx accepts.
|
|
90
106
|
protected readonly backdropClass = computed(() =>
|
|
91
107
|
cn('items-center flex justify-center', 'h-full w-full', 'bg-black/50'),
|
|
92
108
|
);
|
|
@@ -106,15 +122,37 @@ export class UiAlertDialog {
|
|
|
106
122
|
*/
|
|
107
123
|
protected onPanelTap(): void {}
|
|
108
124
|
|
|
125
|
+
/**
|
|
126
|
+
* The alert dialog can be destroyed while its open or close sequence is still
|
|
127
|
+
* running, for example by an `@if` around it or a route change right after
|
|
128
|
+
* closing it. The rest of the sequence then ran on the destroyed component:
|
|
129
|
+
* it animated detached elements and emitted `closed` from a destroyed
|
|
130
|
+
* output, which Angular warns about in dev builds (NG0953).
|
|
131
|
+
*/
|
|
132
|
+
ngOnDestroy(): void {
|
|
133
|
+
for (const timer of this.#timers) clearTimeout(timer);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* `setTimeout`, tracked so that ngOnDestroy can cancel it.
|
|
138
|
+
*/
|
|
139
|
+
#later(callback: () => void, ms: number): void {
|
|
140
|
+
const timer = setTimeout(() => {
|
|
141
|
+
this.#timers.delete(timer);
|
|
142
|
+
callback();
|
|
143
|
+
}, ms);
|
|
144
|
+
this.#timers.add(timer);
|
|
145
|
+
}
|
|
146
|
+
|
|
109
147
|
/**
|
|
110
148
|
* Two-phase open: make overlay visible first (so native elements exist in
|
|
111
149
|
* the tree), then animate on the next frame. See nav-drawer for the same
|
|
112
150
|
* pattern — Lynx animate() requires elements to be mounted before use.
|
|
113
151
|
*/
|
|
114
152
|
#doOpen(): void {
|
|
115
|
-
|
|
153
|
+
this.#later(() => {
|
|
116
154
|
this.overlayVisible.set(true);
|
|
117
|
-
|
|
155
|
+
this.#later(() => this.#animateIn(), 0);
|
|
118
156
|
}, 0);
|
|
119
157
|
}
|
|
120
158
|
|
|
@@ -123,9 +161,9 @@ export class UiAlertDialog {
|
|
|
123
161
|
* causing elements to disappear before the animation finishes.
|
|
124
162
|
*/
|
|
125
163
|
#doClose(): void {
|
|
126
|
-
|
|
164
|
+
this.#later(() => {
|
|
127
165
|
this.#animateOut();
|
|
128
|
-
|
|
166
|
+
this.#later(() => {
|
|
129
167
|
this.overlayVisible.set(false);
|
|
130
168
|
this.closed.emit();
|
|
131
169
|
}, DURATION.normal + 20);
|
|
@@ -133,19 +171,23 @@ export class UiAlertDialog {
|
|
|
133
171
|
}
|
|
134
172
|
|
|
135
173
|
#animateIn(): void {
|
|
136
|
-
const backdrop = this.backdropRef()
|
|
137
|
-
const panel = this.panelRef()
|
|
138
|
-
if (!backdrop || !panel) return;
|
|
174
|
+
const backdrop = this.backdropRef().nativeElement;
|
|
175
|
+
const panel = this.panelRef().nativeElement;
|
|
139
176
|
|
|
140
177
|
this.#backdropAnim?.cancel();
|
|
141
178
|
this.#panelAnim?.cancel();
|
|
142
179
|
|
|
143
180
|
this.#backdropAnim = fadeIn(backdrop, { duration: DURATION.normal });
|
|
181
|
+
// Move a screen reader into the panel. It would otherwise stay on whatever
|
|
182
|
+
// opened it, which is now behind the overlay. With the panel's
|
|
183
|
+
// accessibility-exclusive-focus (see the template), it then can't leave
|
|
184
|
+
// the panel until it closes.
|
|
185
|
+
panel.invoke?.('requestAccessibilityFocus');
|
|
144
186
|
|
|
145
187
|
// 30ms stagger: let the backdrop fade start first so the panel scale-in
|
|
146
188
|
// feels like it's emerging from behind the dimmed overlay rather than
|
|
147
189
|
// appearing simultaneously with it.
|
|
148
|
-
|
|
190
|
+
this.#later(() => {
|
|
149
191
|
this.#panelAnim = scaleIn(panel, {
|
|
150
192
|
duration: DURATION.slow,
|
|
151
193
|
easing: EASING.springSubtle,
|
|
@@ -156,9 +198,8 @@ export class UiAlertDialog {
|
|
|
156
198
|
}
|
|
157
199
|
|
|
158
200
|
#animateOut(): void {
|
|
159
|
-
const backdrop = this.backdropRef()
|
|
160
|
-
const panel = this.panelRef()
|
|
161
|
-
if (!backdrop || !panel) return;
|
|
201
|
+
const backdrop = this.backdropRef().nativeElement;
|
|
202
|
+
const panel = this.panelRef().nativeElement;
|
|
162
203
|
|
|
163
204
|
this.#backdropAnim?.cancel();
|
|
164
205
|
this.#panelAnim?.cancel();
|
|
@@ -176,7 +217,6 @@ export class UiAlertDialog {
|
|
|
176
217
|
|
|
177
218
|
@Component({
|
|
178
219
|
selector: 'ui-alert-dialog-header',
|
|
179
|
-
standalone: true,
|
|
180
220
|
imports: [LYNX_ELEMENTS],
|
|
181
221
|
encapsulation: ViewEncapsulation.None,
|
|
182
222
|
template: `
|
|
@@ -195,7 +235,6 @@ export class UiAlertDialogHeader {
|
|
|
195
235
|
|
|
196
236
|
@Component({
|
|
197
237
|
selector: 'ui-alert-dialog-title',
|
|
198
|
-
standalone: true,
|
|
199
238
|
imports: [LYNX_ELEMENTS],
|
|
200
239
|
encapsulation: ViewEncapsulation.None,
|
|
201
240
|
template: `<text [class]="textClass()"><ng-content /></text>`,
|
|
@@ -210,7 +249,6 @@ export class UiAlertDialogTitle {
|
|
|
210
249
|
|
|
211
250
|
@Component({
|
|
212
251
|
selector: 'ui-alert-dialog-description',
|
|
213
|
-
standalone: true,
|
|
214
252
|
imports: [LYNX_ELEMENTS],
|
|
215
253
|
encapsulation: ViewEncapsulation.None,
|
|
216
254
|
template: `<text [class]="textClass()"><ng-content /></text>`,
|
|
@@ -225,7 +263,6 @@ export class UiAlertDialogDescription {
|
|
|
225
263
|
|
|
226
264
|
@Component({
|
|
227
265
|
selector: 'ui-alert-dialog-footer',
|
|
228
|
-
standalone: true,
|
|
229
266
|
imports: [LYNX_ELEMENTS],
|
|
230
267
|
encapsulation: ViewEncapsulation.None,
|
|
231
268
|
template: `
|