@blotch/dolan 22.2.3 → 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.js +9 -1
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/add.test.js +36 -3
- package/dist/commands/add.test.js.map +1 -1
- package/dist/commands/doctor.js +187 -7
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +280 -7
- 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 +107 -1
- 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 +5 -0
- package/dist/commands/update.js +126 -45
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/update.test.js +314 -2
- package/dist/commands/update.test.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 +6 -0
- package/dist/registry.js +48 -2
- package/dist/registry.js.map +1 -1
- package/dist/registry.test.js +45 -3
- 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 +5 -4
- package/dist/testing/set-input-signal.js +5 -4
- package/dist/testing/set-input-signal.js.map +1 -1
- 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 +272 -187
- package/dist/ui/components/action-sheet/action-sheet.ts +40 -34
- 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 +206 -139
- package/dist/ui/components/alert-dialog/alert-dialog.ts +32 -26
- package/dist/ui/components/area-chart/area-chart.test.ts +81 -1
- 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 +82 -1
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +373 -297
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +34 -33
- package/dist/ui/components/button/button.test.ts +611 -2
- package/dist/ui/components/button/button.ts +431 -62
- 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 +82 -1
- package/dist/ui/components/card/card.test.ts +82 -62
- package/dist/ui/components/chart/chart-presets.test.ts +197 -1
- package/dist/ui/components/chart/chart-presets.ts +8 -1
- package/dist/ui/components/chart/chart-theme.ts +12 -0
- package/dist/ui/components/chart/chart.test.ts +1017 -0
- package/dist/ui/components/chart/chart.ts +56 -43
- 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 +210 -152
- package/dist/ui/components/dialog/dialog.ts +29 -23
- 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 +36 -0
- package/dist/ui/components/icon/icon-theme.ts +36 -13
- 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 +15 -12
- package/dist/ui/components/label/label.test.ts +11 -24
- package/dist/ui/components/line-chart/line-chart.test.ts +76 -1
- package/dist/ui/components/list/list.test.ts +80 -49
- 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 +425 -260
- package/dist/ui/components/nav-drawer/nav-drawer.ts +72 -56
- package/dist/ui/components/pagination/pagination.test.ts +136 -86
- 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/scatter-chart.test.ts +74 -1
- package/dist/ui/components/scroll-area/scroll-area.test.ts +72 -36
- 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 +64 -46
- package/dist/ui/components/spinner/spinner.ts +16 -19
- 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 +434 -350
- package/dist/ui/components/toast/toast.ts +42 -51
- package/dist/ui/components/toggle/toggle.test.ts +247 -140
- 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 +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 +12 -0
- package/dist/utils/installed.js +16 -0
- package/dist/utils/installed.js.map +1 -1
- package/dist/utils/installed.test.js +17 -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 +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 +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,9 +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 {
|
|
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 {
|
|
19
|
+
type LynxMainThread,
|
|
20
|
+
lynxMainThread,
|
|
21
|
+
} from '../../../test-utils/lynx-main-thread';
|
|
7
22
|
import {
|
|
8
23
|
UiDialog,
|
|
9
24
|
UiDialogDescription,
|
|
@@ -22,183 +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
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
unmount();
|
|
51
|
-
await new Promise((r) => setTimeout(r, DURATION.normal + 50));
|
|
52
|
-
const warnings = warn.mock.calls.map(([message]) => String(message));
|
|
53
|
-
warn.mockRestore();
|
|
54
|
-
expect(warnings.filter((message) => message.includes('NG0953'))).toEqual(
|
|
55
|
-
[],
|
|
56
|
-
);
|
|
57
|
-
});
|
|
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
|
+
};
|
|
58
79
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
+
|
|
103
|
+
describe('UiDialog', () => {
|
|
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');
|
|
78
108
|
});
|
|
109
|
+
afterEach(() => animate.mockRestore());
|
|
79
110
|
|
|
80
|
-
it('renders
|
|
81
|
-
const { getByText } = await
|
|
111
|
+
it('renders each part with its projected content', async () => {
|
|
112
|
+
const { getByText } = await renderDialog();
|
|
82
113
|
expect(getByText('Title')).toBeTruthy();
|
|
83
114
|
expect(getByText('Description')).toBeTruthy();
|
|
84
115
|
expect(getByText('Actions')).toBeTruthy();
|
|
85
116
|
});
|
|
86
117
|
|
|
87
|
-
it('
|
|
88
|
-
const {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
(
|
|
93
|
-
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');
|
|
94
132
|
});
|
|
95
133
|
|
|
96
|
-
it('
|
|
97
|
-
const {
|
|
98
|
-
|
|
99
|
-
expect(()
|
|
100
|
-
|
|
101
|
-
).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);
|
|
102
142
|
});
|
|
103
143
|
|
|
104
|
-
it('
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// inside #doClose runs DURATION.normal (250ms) + 20ms later; wait real time.
|
|
125
|
-
inst.open.set(false);
|
|
126
|
-
await new Promise((r) => setTimeout(r, 320));
|
|
127
|
-
await waitForUpdate();
|
|
128
|
-
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
|
+
]);
|
|
129
164
|
});
|
|
130
165
|
|
|
131
|
-
it('
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
await waitForUpdate();
|
|
139
|
-
await waitForUpdate();
|
|
140
|
-
inst.open.set(false);
|
|
141
|
-
await waitForUpdate();
|
|
142
|
-
// Wait out the close sequence, so #animateOut runs (and bails) while the
|
|
143
|
-
// component is still alive. Destroying it clears the sequence's timers.
|
|
144
|
-
await new Promise((r) => setTimeout(r, DURATION.normal + 50));
|
|
145
|
-
expect(inst.open()).toBe(false);
|
|
146
|
-
expect(closed).toHaveBeenCalledOnce();
|
|
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');
|
|
147
173
|
});
|
|
148
174
|
|
|
149
|
-
it('
|
|
150
|
-
const {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
expect(overlay?.getAttribute('style')).toContain('display: none');
|
|
155
|
-
|
|
156
|
-
// Open + advance so overlayVisible flips true.
|
|
157
|
-
const view = container.querySelector('view') as Element;
|
|
158
|
-
(inst as unknown as { backdropRef: unknown }).backdropRef = signal({
|
|
159
|
-
nativeElement: view,
|
|
160
|
-
});
|
|
161
|
-
(inst as unknown as { panelRef: unknown }).panelRef = signal({
|
|
162
|
-
nativeElement: view,
|
|
163
|
-
});
|
|
164
|
-
inst.open.set(true);
|
|
165
|
-
for (let i = 0; i < 10; i++) await waitForUpdate();
|
|
166
|
-
overlay = container.querySelector('overlay');
|
|
167
|
-
expect(overlay?.getAttribute('style')).not.toContain('display: none');
|
|
168
|
-
});
|
|
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();
|
|
169
180
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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);
|
|
181
201
|
});
|
|
182
|
-
});
|
|
183
202
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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)]);
|
|
188
220
|
});
|
|
189
221
|
|
|
190
|
-
it('
|
|
191
|
-
const {
|
|
192
|
-
|
|
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);
|
|
193
230
|
});
|
|
194
231
|
|
|
195
|
-
it('
|
|
196
|
-
const {
|
|
197
|
-
|
|
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);
|
|
198
240
|
});
|
|
199
241
|
|
|
200
|
-
it('
|
|
201
|
-
const
|
|
202
|
-
|
|
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);
|
|
203
261
|
});
|
|
204
262
|
});
|
|
@@ -52,8 +52,11 @@ export class UiDialog implements OnDestroy {
|
|
|
52
52
|
|
|
53
53
|
protected readonly overlayVisible = signal(false);
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
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');
|
|
57
60
|
#backdropAnim?: AnimationHandle;
|
|
58
61
|
#panelAnim?: AnimationHandle;
|
|
59
62
|
#hasBeenOpen = false;
|
|
@@ -61,21 +64,26 @@ export class UiDialog implements OnDestroy {
|
|
|
61
64
|
// them.
|
|
62
65
|
readonly #timers = new Set<ReturnType<typeof setTimeout>>();
|
|
63
66
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
+
});
|
|
79
87
|
|
|
80
88
|
/**
|
|
81
89
|
* Hide the overlay with `display:none` on web where `visible` attr has no
|
|
@@ -167,9 +175,8 @@ export class UiDialog implements OnDestroy {
|
|
|
167
175
|
}
|
|
168
176
|
|
|
169
177
|
#animateIn(): void {
|
|
170
|
-
const backdrop = this.backdropRef()
|
|
171
|
-
const panel = this.panelRef()
|
|
172
|
-
if (!backdrop || !panel) return;
|
|
178
|
+
const backdrop = this.backdropRef().nativeElement;
|
|
179
|
+
const panel = this.panelRef().nativeElement;
|
|
173
180
|
|
|
174
181
|
this.#backdropAnim?.cancel();
|
|
175
182
|
this.#panelAnim?.cancel();
|
|
@@ -194,9 +201,8 @@ export class UiDialog implements OnDestroy {
|
|
|
194
201
|
}
|
|
195
202
|
|
|
196
203
|
#animateOut(): void {
|
|
197
|
-
const backdrop = this.backdropRef()
|
|
198
|
-
const panel = this.panelRef()
|
|
199
|
-
if (!backdrop || !panel) return;
|
|
204
|
+
const backdrop = this.backdropRef().nativeElement;
|
|
205
|
+
const panel = this.panelRef().nativeElement;
|
|
200
206
|
|
|
201
207
|
this.#backdropAnim?.cancel();
|
|
202
208
|
this.#panelAnim?.cancel();
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import {
|
|
3
|
+
fireEvent,
|
|
4
|
+
render,
|
|
5
|
+
waitForUpdate,
|
|
6
|
+
} from '@blotch/angular-lynx-testing-library';
|
|
7
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
8
|
+
import { describe, expect, it } from 'vitest';
|
|
9
|
+
import { UiDockScroll, UiDockState } from './dock-scroll';
|
|
10
|
+
|
|
11
|
+
@Component({
|
|
12
|
+
imports: [LYNX_ELEMENTS, UiDockScroll],
|
|
13
|
+
template: `
|
|
14
|
+
@if (shown()) {
|
|
15
|
+
<scroll-view ui-dock-scroll scroll-orientation="vertical">
|
|
16
|
+
<view style="height: 2000px"></view>
|
|
17
|
+
</scroll-view>
|
|
18
|
+
}
|
|
19
|
+
`,
|
|
20
|
+
})
|
|
21
|
+
class ScrollHost {
|
|
22
|
+
readonly shown = signal(true);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const renderScroll = async () => {
|
|
26
|
+
const result = await render(ScrollHost);
|
|
27
|
+
const host = result.componentRef.instance as ScrollHost;
|
|
28
|
+
const state = result.componentRef.injector.get(UiDockState);
|
|
29
|
+
const scrollView = () => result.container.querySelector('scroll-view')!;
|
|
30
|
+
/**
|
|
31
|
+
* Scrolls the content to `scrollTop`, as Lynx reports it.
|
|
32
|
+
*/
|
|
33
|
+
const scrollTo = (scrollTop: number) =>
|
|
34
|
+
fireEvent.scroll(scrollView(), {
|
|
35
|
+
detail: { scrollTop, scrollHeight: 2000 },
|
|
36
|
+
});
|
|
37
|
+
return { ...result, host, state, scrollView, scrollTo };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
describe('UiDockScroll', () => {
|
|
41
|
+
it('minimizes the dock as content scrolls down, and expands it as it scrolls up', async () => {
|
|
42
|
+
const { state, scrollTo } = await renderScroll();
|
|
43
|
+
expect(state.minimized()).toBe(false);
|
|
44
|
+
|
|
45
|
+
// A little way down isn't enough: a small drag leaves the dock whole.
|
|
46
|
+
scrollTo(10);
|
|
47
|
+
scrollTo(20);
|
|
48
|
+
expect(state.minimized()).toBe(false);
|
|
49
|
+
scrollTo(30);
|
|
50
|
+
expect(state.minimized()).toBe(true);
|
|
51
|
+
|
|
52
|
+
// Further down keeps it minimized.
|
|
53
|
+
scrollTo(300);
|
|
54
|
+
expect(state.minimized()).toBe(true);
|
|
55
|
+
|
|
56
|
+
// A glance back up brings it back.
|
|
57
|
+
scrollTo(295);
|
|
58
|
+
scrollTo(290);
|
|
59
|
+
expect(state.minimized()).toBe(true);
|
|
60
|
+
scrollTo(287);
|
|
61
|
+
expect(state.minimized()).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('counts the distance again when content turns', async () => {
|
|
65
|
+
const { state, scrollTo } = await renderScroll();
|
|
66
|
+
scrollTo(100);
|
|
67
|
+
expect(state.minimized()).toBe(true);
|
|
68
|
+
scrollTo(80);
|
|
69
|
+
expect(state.minimized()).toBe(false);
|
|
70
|
+
|
|
71
|
+
// Down 20 after coming up 20 starts a new count, short of minimizing.
|
|
72
|
+
scrollTo(100);
|
|
73
|
+
expect(state.minimized()).toBe(false);
|
|
74
|
+
// An event with no movement keeps the count.
|
|
75
|
+
scrollTo(100);
|
|
76
|
+
scrollTo(104);
|
|
77
|
+
expect(state.minimized()).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('expands the dock at the top, including the bounce above it', async () => {
|
|
81
|
+
const { state, scrollTo } = await renderScroll();
|
|
82
|
+
scrollTo(200);
|
|
83
|
+
expect(state.minimized()).toBe(true);
|
|
84
|
+
|
|
85
|
+
scrollTo(5);
|
|
86
|
+
expect(state.minimized()).toBe(false);
|
|
87
|
+
|
|
88
|
+
scrollTo(200);
|
|
89
|
+
expect(state.minimized()).toBe(true);
|
|
90
|
+
scrollTo(-30);
|
|
91
|
+
expect(state.minimized()).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("ignores iOS's bounce back from past the end", async () => {
|
|
95
|
+
const { state, scrollTo, scrollView } = await renderScroll();
|
|
96
|
+
// The scroll view is 800px tall, so its content ends at 1200.
|
|
97
|
+
fireEvent.layoutchange(scrollView(), {
|
|
98
|
+
detail: { width: 400, height: 800 },
|
|
99
|
+
});
|
|
100
|
+
scrollTo(1150);
|
|
101
|
+
scrollTo(1250);
|
|
102
|
+
expect(state.minimized()).toBe(true);
|
|
103
|
+
|
|
104
|
+
// Bouncing back from 1250 to the end reads as scrolling up, but isn't.
|
|
105
|
+
scrollTo(1230);
|
|
106
|
+
scrollTo(1210);
|
|
107
|
+
expect(state.minimized()).toBe(true);
|
|
108
|
+
|
|
109
|
+
// Scrolling up from the end is.
|
|
110
|
+
scrollTo(1200);
|
|
111
|
+
scrollTo(1180);
|
|
112
|
+
expect(state.minimized()).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('expands the dock when the scroll view goes, and stops listening', async () => {
|
|
116
|
+
const { host, state, scrollTo, scrollView } = await renderScroll();
|
|
117
|
+
const removed = scrollView();
|
|
118
|
+
scrollTo(200);
|
|
119
|
+
expect(state.minimized()).toBe(true);
|
|
120
|
+
|
|
121
|
+
host.shown.set(false);
|
|
122
|
+
await waitForUpdate();
|
|
123
|
+
expect(state.minimized()).toBe(false);
|
|
124
|
+
|
|
125
|
+
fireEvent.scroll(removed, {
|
|
126
|
+
detail: { scrollTop: 600, scrollHeight: 2000 },
|
|
127
|
+
});
|
|
128
|
+
expect(state.minimized()).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
});
|