@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
|
UiDialog,
|
|
8
24
|
UiDialogDescription,
|
|
@@ -12,7 +28,6 @@ import {
|
|
|
12
28
|
} from './dialog';
|
|
13
29
|
|
|
14
30
|
@Component({
|
|
15
|
-
standalone: true,
|
|
16
31
|
imports: [
|
|
17
32
|
UiDialog,
|
|
18
33
|
UiDialogHeader,
|
|
@@ -22,136 +37,226 @@ import {
|
|
|
22
37
|
LYNX_ELEMENTS,
|
|
23
38
|
],
|
|
24
39
|
template: `
|
|
25
|
-
|
|
26
|
-
<ui-dialog
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
@if (shown()) {
|
|
41
|
+
<ui-dialog
|
|
42
|
+
[(open)]="open"
|
|
43
|
+
class="max-w-sm"
|
|
44
|
+
(closed)="closes.set(closes() + 1)"
|
|
45
|
+
>
|
|
46
|
+
<ui-dialog-header class="gap-2">
|
|
47
|
+
<ui-dialog-title class="text-xl">Title</ui-dialog-title>
|
|
48
|
+
<ui-dialog-description class="italic">
|
|
49
|
+
Description
|
|
50
|
+
</ui-dialog-description>
|
|
51
|
+
</ui-dialog-header>
|
|
52
|
+
<ui-dialog-footer class="justify-start">
|
|
53
|
+
<text>Actions</text>
|
|
54
|
+
</ui-dialog-footer>
|
|
55
|
+
</ui-dialog>
|
|
56
|
+
}
|
|
32
57
|
`,
|
|
33
58
|
})
|
|
34
59
|
class DialogHost {
|
|
35
|
-
open =
|
|
60
|
+
readonly open = signal(false);
|
|
61
|
+
// Takes the dialog out of the tree, as an `@if` or a route change would.
|
|
62
|
+
readonly shown = signal(true);
|
|
63
|
+
readonly closes = signal(0);
|
|
64
|
+
readonly dialog = viewChild.required(UiDialog);
|
|
36
65
|
}
|
|
37
66
|
|
|
67
|
+
const renderDialog = async () => {
|
|
68
|
+
const result = await render(DialogHost);
|
|
69
|
+
const panel = () =>
|
|
70
|
+
result.container.querySelector('[accessibility-exclusive-focus]')!;
|
|
71
|
+
return {
|
|
72
|
+
...result,
|
|
73
|
+
host: result.componentRef.instance as DialogHost,
|
|
74
|
+
overlay: () => result.container.querySelector('overlay')!,
|
|
75
|
+
panel,
|
|
76
|
+
backdrop: () => panel().parentElement!,
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Waits for the dialog's open or close sequence, which runs on timers, then
|
|
82
|
+
* renders what it changed.
|
|
83
|
+
*/
|
|
84
|
+
const after = async (ms: number) => {
|
|
85
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
86
|
+
await waitForUpdate();
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Long enough for the open sequence, including the panel's 30ms stagger. The
|
|
91
|
+
* sequence is chained timers, and on Windows each can fire up to a clock
|
|
92
|
+
* tick (about 16ms) late, so 50ms was too short on CI's Windows runner.
|
|
93
|
+
*/
|
|
94
|
+
const OPENED = 150;
|
|
95
|
+
/** Long enough for the close animation and the hide after it, as above. */
|
|
96
|
+
const CLOSED = DURATION.normal + 150;
|
|
97
|
+
|
|
98
|
+
/** `__ElementAnimate`'s first argument for starting an animation. */
|
|
99
|
+
const START = 0;
|
|
100
|
+
/** `__ElementAnimate`'s first argument for canceling an animation. */
|
|
101
|
+
const CANCEL = 3;
|
|
102
|
+
|
|
38
103
|
describe('UiDialog', () => {
|
|
39
|
-
|
|
40
|
-
|
|
104
|
+
// Watched per test: the testing library resets the main thread after each.
|
|
105
|
+
let animate: MockInstance<LynxMainThread['__ElementAnimate']>;
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
animate = vi.spyOn(lynxMainThread(), '__ElementAnimate');
|
|
108
|
+
});
|
|
109
|
+
afterEach(() => animate.mockRestore());
|
|
110
|
+
|
|
111
|
+
it('renders each part with its projected content', async () => {
|
|
112
|
+
const { getByText } = await renderDialog();
|
|
41
113
|
expect(getByText('Title')).toBeTruthy();
|
|
42
114
|
expect(getByText('Description')).toBeTruthy();
|
|
43
115
|
expect(getByText('Actions')).toBeTruthy();
|
|
44
116
|
});
|
|
45
117
|
|
|
46
|
-
it('
|
|
47
|
-
const {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
(
|
|
52
|
-
expect(
|
|
118
|
+
it("merges each part's class with its own, the given class winning", async () => {
|
|
119
|
+
const { container, getByText, panel } = await renderDialog();
|
|
120
|
+
expect(panel().classList).toContain('bg-background');
|
|
121
|
+
expect(panel().classList).toContain('max-w-sm');
|
|
122
|
+
expect(getByText('Title').classList).toContain('font-semibold');
|
|
123
|
+
expect(getByText('Title').classList).toContain('text-xl');
|
|
124
|
+
expect(getByText('Description').classList).toContain('text-sm');
|
|
125
|
+
expect(getByText('Description').classList).toContain('italic');
|
|
126
|
+
// Each part's own view, not its host element, which keeps the bare class.
|
|
127
|
+
const header = container.querySelector('.flex-col.gap-2');
|
|
128
|
+
expect(header?.classList).not.toContain('gap-1.5');
|
|
129
|
+
const footer = container.querySelector('.pt-4');
|
|
130
|
+
expect(footer?.classList).toContain('justify-start');
|
|
131
|
+
expect(footer?.classList).not.toContain('justify-end');
|
|
53
132
|
});
|
|
54
133
|
|
|
55
|
-
it('
|
|
56
|
-
const {
|
|
57
|
-
|
|
58
|
-
expect(()
|
|
59
|
-
|
|
60
|
-
).not.
|
|
134
|
+
it('stays hidden until it is opened', async () => {
|
|
135
|
+
const { host, overlay, panel } = await renderDialog();
|
|
136
|
+
await after(CLOSED);
|
|
137
|
+
expect(overlay().getAttribute('style')).toContain('display: none');
|
|
138
|
+
expect(panel().getAttribute('accessibility-exclusive-focus')).toBe('false');
|
|
139
|
+
expect(animate).not.toHaveBeenCalled();
|
|
140
|
+
// Starting closed isn't closing: `closed` waits for a real close.
|
|
141
|
+
expect(host.closes()).toBe(0);
|
|
61
142
|
});
|
|
62
143
|
|
|
63
|
-
it('
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
// inside #doClose runs DURATION.normal (250ms) + 20ms later; wait real time.
|
|
84
|
-
inst.open.set(false);
|
|
85
|
-
await new Promise((r) => setTimeout(r, 320));
|
|
86
|
-
await waitForUpdate();
|
|
87
|
-
expect(inst.open()).toBe(false);
|
|
144
|
+
it('shows the overlay and animates the backdrop, then the panel, in', async () => {
|
|
145
|
+
const { host, overlay, backdrop, panel } = await renderDialog();
|
|
146
|
+
host.open.set(true);
|
|
147
|
+
await after(OPENED);
|
|
148
|
+
expect(overlay().getAttribute('style')).not.toContain('display: none');
|
|
149
|
+
expect(animate).toHaveBeenCalledWith(backdrop(), [
|
|
150
|
+
START,
|
|
151
|
+
expect.any(String),
|
|
152
|
+
[{ opacity: 0 }, { opacity: 1 }],
|
|
153
|
+
expect.objectContaining({ duration: DURATION.normal }),
|
|
154
|
+
]);
|
|
155
|
+
expect(animate).toHaveBeenCalledWith(panel(), [
|
|
156
|
+
START,
|
|
157
|
+
expect.any(String),
|
|
158
|
+
[
|
|
159
|
+
{ transform: 'scale(0.9) translateY(10px)', opacity: 0 },
|
|
160
|
+
{ transform: 'scale(1) translateY(0px)', opacity: 1 },
|
|
161
|
+
],
|
|
162
|
+
expect.objectContaining({ duration: DURATION.slow }),
|
|
163
|
+
]);
|
|
88
164
|
});
|
|
89
165
|
|
|
90
|
-
it('
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
inst.open.set(false);
|
|
98
|
-
await waitForUpdate();
|
|
99
|
-
expect(inst.open()).toBe(false);
|
|
166
|
+
it('moves a screen reader into the panel and keeps it there while open', async () => {
|
|
167
|
+
const { host, panel } = await renderDialog();
|
|
168
|
+
const invoke = vi.spyOn(host.dialog().panelRef().nativeElement, 'invoke');
|
|
169
|
+
host.open.set(true);
|
|
170
|
+
await after(OPENED);
|
|
171
|
+
expect(invoke).toHaveBeenCalledWith('requestAccessibilityFocus');
|
|
172
|
+
expect(panel().getAttribute('accessibility-exclusive-focus')).toBe('true');
|
|
100
173
|
});
|
|
101
174
|
|
|
102
|
-
it('
|
|
103
|
-
const {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
expect(overlay?.getAttribute('style')).toContain('display: none');
|
|
108
|
-
|
|
109
|
-
// Open + advance so overlayVisible flips true.
|
|
110
|
-
const view = container.querySelector('view') as Element;
|
|
111
|
-
(inst as unknown as { backdropRef: unknown }).backdropRef = signal({
|
|
112
|
-
nativeElement: view,
|
|
113
|
-
});
|
|
114
|
-
(inst as unknown as { panelRef: unknown }).panelRef = signal({
|
|
115
|
-
nativeElement: view,
|
|
116
|
-
});
|
|
117
|
-
inst.open.set(true);
|
|
118
|
-
for (let i = 0; i < 10; i++) await waitForUpdate();
|
|
119
|
-
overlay = container.querySelector('overlay');
|
|
120
|
-
expect(overlay?.getAttribute('style')).not.toContain('display: none');
|
|
121
|
-
});
|
|
175
|
+
it('animates out, then hides and emits closed', async () => {
|
|
176
|
+
const { host, overlay, backdrop, panel } = await renderDialog();
|
|
177
|
+
host.open.set(true);
|
|
178
|
+
await after(OPENED);
|
|
179
|
+
animate.mockClear();
|
|
122
180
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
181
|
+
host.open.set(false);
|
|
182
|
+
await after(CLOSED);
|
|
183
|
+
expect(animate).toHaveBeenCalledWith(backdrop(), [
|
|
184
|
+
START,
|
|
185
|
+
expect.any(String),
|
|
186
|
+
[{ opacity: 1 }, { opacity: 0 }],
|
|
187
|
+
expect.any(Object),
|
|
188
|
+
]);
|
|
189
|
+
expect(animate).toHaveBeenCalledWith(panel(), [
|
|
190
|
+
START,
|
|
191
|
+
expect.any(String),
|
|
192
|
+
[
|
|
193
|
+
{ transform: 'scale(1) translateY(0px)', opacity: 1 },
|
|
194
|
+
{ transform: 'scale(0.95) translateY(10px)', opacity: 0 },
|
|
195
|
+
],
|
|
196
|
+
expect.any(Object),
|
|
197
|
+
]);
|
|
198
|
+
expect(overlay().getAttribute('style')).toContain('display: none');
|
|
199
|
+
expect(panel().getAttribute('accessibility-exclusive-focus')).toBe('false');
|
|
200
|
+
expect(host.closes()).toBe(1);
|
|
134
201
|
});
|
|
135
|
-
});
|
|
136
202
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
203
|
+
it('cancels the previous animations when it opens again', async () => {
|
|
204
|
+
const { host, backdrop, panel } = await renderDialog();
|
|
205
|
+
host.open.set(true);
|
|
206
|
+
await after(OPENED);
|
|
207
|
+
host.open.set(false);
|
|
208
|
+
await after(CLOSED);
|
|
209
|
+
animate.mockClear();
|
|
210
|
+
|
|
211
|
+
host.open.set(true);
|
|
212
|
+
await after(OPENED);
|
|
213
|
+
// The close animations hold their last frame (fill: forwards) until
|
|
214
|
+
// they're canceled, which would hide the panel being opened.
|
|
215
|
+
expect(animate).toHaveBeenCalledWith(backdrop(), [
|
|
216
|
+
CANCEL,
|
|
217
|
+
expect.any(String),
|
|
218
|
+
]);
|
|
219
|
+
expect(animate).toHaveBeenCalledWith(panel(), [CANCEL, expect.any(String)]);
|
|
141
220
|
});
|
|
142
221
|
|
|
143
|
-
it('
|
|
144
|
-
const {
|
|
145
|
-
|
|
222
|
+
it('closes when the backdrop is tapped', async () => {
|
|
223
|
+
const { host, backdrop } = await renderDialog();
|
|
224
|
+
host.open.set(true);
|
|
225
|
+
await after(OPENED);
|
|
226
|
+
fireEvent.tap(backdrop());
|
|
227
|
+
expect(host.open()).toBe(false);
|
|
228
|
+
await after(CLOSED);
|
|
229
|
+
expect(host.closes()).toBe(1);
|
|
146
230
|
});
|
|
147
231
|
|
|
148
|
-
it('
|
|
149
|
-
const {
|
|
150
|
-
|
|
232
|
+
it('stays open when the panel is tapped', async () => {
|
|
233
|
+
const { host, panel } = await renderDialog();
|
|
234
|
+
host.open.set(true);
|
|
235
|
+
await after(OPENED);
|
|
236
|
+
// The panel catches its taps, so they don't reach the backdrop.
|
|
237
|
+
fireEvent.tap(panel(), { eventType: 'catchEvent' });
|
|
238
|
+
await waitForUpdate();
|
|
239
|
+
expect(host.open()).toBe(true);
|
|
151
240
|
});
|
|
152
241
|
|
|
153
|
-
it('
|
|
154
|
-
const
|
|
155
|
-
|
|
242
|
+
it('stops its close sequence when it is destroyed while closing', async () => {
|
|
243
|
+
const warn = vi.spyOn(console, 'warn');
|
|
244
|
+
const { host } = await renderDialog();
|
|
245
|
+
host.open.set(true);
|
|
246
|
+
await after(OPENED);
|
|
247
|
+
host.open.set(false);
|
|
248
|
+
await after(0);
|
|
249
|
+
// Destroyed during the close animation. Its close timer
|
|
250
|
+
// (DURATION.normal + 20) used to fire anyway and emit `closed` from the
|
|
251
|
+
// destroyed output, which Angular warns about in dev builds.
|
|
252
|
+
host.shown.set(false);
|
|
253
|
+
await waitForUpdate();
|
|
254
|
+
await after(CLOSED);
|
|
255
|
+
const warnings = warn.mock.calls.map(([message]) => String(message));
|
|
256
|
+
warn.mockRestore();
|
|
257
|
+
expect(warnings.filter((message) => message.includes('NG0953'))).toEqual(
|
|
258
|
+
[],
|
|
259
|
+
);
|
|
260
|
+
expect(host.closes()).toBe(0);
|
|
156
261
|
});
|
|
157
262
|
});
|
|
@@ -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-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()" (bindtap)="onBackdropTap()">
|
|
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 UiDialog {
|
|
47
|
+
export class UiDialog implements OnDestroy {
|
|
43
48
|
readonly open = model(false);
|
|
44
49
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
45
50
|
|
|
@@ -47,27 +52,38 @@ export class UiDialog {
|
|
|
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
|
+
* very first run would call #doClose() immediately, resulting in a no-op
|
|
76
|
+
* animation that still hides the overlay and emits `closed` unexpectedly.
|
|
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
|
* Hide the overlay with `display:none` on web where `visible` attr has no
|
|
@@ -110,15 +126,37 @@ export class UiDialog {
|
|
|
110
126
|
*/
|
|
111
127
|
protected onPanelTap(): void {}
|
|
112
128
|
|
|
129
|
+
/**
|
|
130
|
+
* The dialog can be destroyed while its open or close sequence is still
|
|
131
|
+
* running, for example by an `@if` around it or a route change right after
|
|
132
|
+
* closing it. The rest of the sequence then ran on the destroyed component:
|
|
133
|
+
* it animated detached elements and emitted `closed` from a destroyed
|
|
134
|
+
* output, which Angular warns about in dev builds (NG0953).
|
|
135
|
+
*/
|
|
136
|
+
ngOnDestroy(): void {
|
|
137
|
+
for (const timer of this.#timers) clearTimeout(timer);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* `setTimeout`, tracked so that ngOnDestroy can cancel it.
|
|
142
|
+
*/
|
|
143
|
+
#later(callback: () => void, ms: number): void {
|
|
144
|
+
const timer = setTimeout(() => {
|
|
145
|
+
this.#timers.delete(timer);
|
|
146
|
+
callback();
|
|
147
|
+
}, ms);
|
|
148
|
+
this.#timers.add(timer);
|
|
149
|
+
}
|
|
150
|
+
|
|
113
151
|
/**
|
|
114
152
|
* Two-phase open: make overlay visible first (so native elements exist in
|
|
115
153
|
* the tree), then animate on the next frame. See nav-drawer for the same
|
|
116
154
|
* pattern — Lynx animate() requires elements to be mounted before use.
|
|
117
155
|
*/
|
|
118
156
|
#doOpen(): void {
|
|
119
|
-
|
|
157
|
+
this.#later(() => {
|
|
120
158
|
this.overlayVisible.set(true);
|
|
121
|
-
|
|
159
|
+
this.#later(() => this.#animateIn(), 0);
|
|
122
160
|
}, 0);
|
|
123
161
|
}
|
|
124
162
|
|
|
@@ -127,9 +165,9 @@ export class UiDialog {
|
|
|
127
165
|
* causing elements to disappear before the animation finishes.
|
|
128
166
|
*/
|
|
129
167
|
#doClose(): void {
|
|
130
|
-
|
|
168
|
+
this.#later(() => {
|
|
131
169
|
this.#animateOut();
|
|
132
|
-
|
|
170
|
+
this.#later(() => {
|
|
133
171
|
this.overlayVisible.set(false);
|
|
134
172
|
this.closed.emit();
|
|
135
173
|
}, DURATION.normal + 20);
|
|
@@ -137,18 +175,22 @@ export class UiDialog {
|
|
|
137
175
|
}
|
|
138
176
|
|
|
139
177
|
#animateIn(): void {
|
|
140
|
-
const backdrop = this.backdropRef()
|
|
141
|
-
const panel = this.panelRef()
|
|
142
|
-
if (!backdrop || !panel) return;
|
|
178
|
+
const backdrop = this.backdropRef().nativeElement;
|
|
179
|
+
const panel = this.panelRef().nativeElement;
|
|
143
180
|
|
|
144
181
|
this.#backdropAnim?.cancel();
|
|
145
182
|
this.#panelAnim?.cancel();
|
|
146
183
|
|
|
147
184
|
// Backdrop fades in first
|
|
148
185
|
this.#backdropAnim = fadeIn(backdrop, { duration: DURATION.normal });
|
|
186
|
+
// Move a screen reader into the panel. It would otherwise stay on whatever
|
|
187
|
+
// opened it, which is now behind the overlay. With the panel's
|
|
188
|
+
// accessibility-exclusive-focus (see the template), it then can't leave
|
|
189
|
+
// the panel until it closes.
|
|
190
|
+
panel.invoke?.('requestAccessibilityFocus');
|
|
149
191
|
|
|
150
192
|
// Panel scales in with gentle spring (staggered 30ms after backdrop)
|
|
151
|
-
|
|
193
|
+
this.#later(() => {
|
|
152
194
|
this.#panelAnim = scaleIn(panel, {
|
|
153
195
|
duration: DURATION.slow,
|
|
154
196
|
easing: EASING.springSubtle,
|
|
@@ -159,9 +201,8 @@ export class UiDialog {
|
|
|
159
201
|
}
|
|
160
202
|
|
|
161
203
|
#animateOut(): void {
|
|
162
|
-
const backdrop = this.backdropRef()
|
|
163
|
-
const panel = this.panelRef()
|
|
164
|
-
if (!backdrop || !panel) return;
|
|
204
|
+
const backdrop = this.backdropRef().nativeElement;
|
|
205
|
+
const panel = this.panelRef().nativeElement;
|
|
165
206
|
|
|
166
207
|
this.#backdropAnim?.cancel();
|
|
167
208
|
this.#panelAnim?.cancel();
|
|
@@ -181,7 +222,6 @@ export class UiDialog {
|
|
|
181
222
|
|
|
182
223
|
@Component({
|
|
183
224
|
selector: 'ui-dialog-header',
|
|
184
|
-
standalone: true,
|
|
185
225
|
imports: [LYNX_ELEMENTS],
|
|
186
226
|
encapsulation: ViewEncapsulation.None,
|
|
187
227
|
template: `
|
|
@@ -200,7 +240,6 @@ export class UiDialogHeader {
|
|
|
200
240
|
|
|
201
241
|
@Component({
|
|
202
242
|
selector: 'ui-dialog-title',
|
|
203
|
-
standalone: true,
|
|
204
243
|
imports: [LYNX_ELEMENTS],
|
|
205
244
|
encapsulation: ViewEncapsulation.None,
|
|
206
245
|
template: `<text [class]="textClass()"><ng-content /></text>`,
|
|
@@ -215,7 +254,6 @@ export class UiDialogTitle {
|
|
|
215
254
|
|
|
216
255
|
@Component({
|
|
217
256
|
selector: 'ui-dialog-description',
|
|
218
|
-
standalone: true,
|
|
219
257
|
imports: [LYNX_ELEMENTS],
|
|
220
258
|
encapsulation: ViewEncapsulation.None,
|
|
221
259
|
template: `<text [class]="textClass()"><ng-content /></text>`,
|
|
@@ -230,7 +268,6 @@ export class UiDialogDescription {
|
|
|
230
268
|
|
|
231
269
|
@Component({
|
|
232
270
|
selector: 'ui-dialog-footer',
|
|
233
|
-
standalone: true,
|
|
234
271
|
imports: [LYNX_ELEMENTS],
|
|
235
272
|
encapsulation: ViewEncapsulation.None,
|
|
236
273
|
template: `
|