@blotch/dolan 22.2.1 → 22.2.3
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 +32 -10
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/add.test.js +129 -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 +41 -27
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +93 -9
- 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 +149 -40
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/init.test.js +285 -26
- 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/update.d.ts +6 -0
- package/dist/commands/update.js +59 -24
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/update.test.js +273 -31
- package/dist/commands/update.test.js.map +1 -1
- package/dist/index.js +10 -3
- package/dist/index.js.map +1 -1
- package/dist/registry.d.ts +8 -0
- package/dist/registry.js +32 -24
- package/dist/registry.js.map +1 -1
- package/dist/registry.test.js +76 -37
- package/dist/registry.test.js.map +1 -1
- package/dist/testing/set-input-signal.d.ts +13 -0
- package/dist/testing/set-input-signal.js +20 -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/action-sheet/action-sheet.test.ts +48 -1
- package/dist/ui/components/action-sheet/action-sheet.ts +37 -9
- package/dist/ui/components/alert-dialog/alert-dialog.test.ts +49 -2
- package/dist/ui/components/alert-dialog/alert-dialog.ts +43 -12
- package/dist/ui/components/area-chart/area-chart-render.test.ts +52 -110
- package/dist/ui/components/area-chart/area-chart.test.ts +59 -146
- 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/bar-chart/bar-chart-render.test.ts +47 -163
- package/dist/ui/components/bar-chart/bar-chart.test.ts +82 -58
- 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 +191 -9
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +136 -20
- package/dist/ui/components/button/button-render.test.ts +19 -0
- package/dist/ui/components/button/button.test.ts +2 -15
- package/dist/ui/components/button/button.ts +18 -2
- package/dist/ui/components/button-group/button-group.test.ts +2 -21
- package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +48 -163
- package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +107 -85
- 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/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 +139 -0
- package/dist/ui/components/chart/chart-presets.ts +271 -0
- package/dist/ui/components/chart/chart-render.test.ts +730 -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 +198 -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.ts +921 -0
- package/dist/ui/components/chart/index.ts +24 -0
- package/dist/ui/components/dialog/dialog.test.ts +49 -2
- package/dist/ui/components/dialog/dialog.ts +43 -12
- package/dist/ui/components/icon/icon-theme.test.ts +133 -0
- package/dist/ui/components/icon/icon-theme.ts +204 -0
- package/dist/ui/components/icon/icon.test.ts +51 -2
- package/dist/ui/components/icon/icon.ts +38 -21
- package/dist/ui/components/line-chart/index.ts +2 -3
- package/dist/ui/components/line-chart/line-chart-render.test.ts +69 -107
- package/dist/ui/components/line-chart/line-chart.test.ts +63 -67
- package/dist/ui/components/line-chart/line-chart.ts +83 -241
- package/dist/ui/components/list/list.test.ts +69 -2
- package/dist/ui/components/list/list.ts +21 -15
- package/dist/ui/components/nav-drawer/nav-drawer.test.ts +80 -1
- package/dist/ui/components/nav-drawer/nav-drawer.ts +66 -11
- package/dist/ui/components/pagination/pagination.test.ts +62 -0
- package/dist/ui/components/pagination/pagination.ts +55 -8
- package/dist/ui/components/scatter-chart/index.ts +2 -3
- package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +51 -144
- package/dist/ui/components/scatter-chart/scatter-chart.test.ts +57 -47
- package/dist/ui/components/scatter-chart/scatter-chart.ts +104 -250
- package/dist/ui/components/scroll-area/scroll-area.test.ts +37 -2
- package/dist/ui/components/scroll-area/scroll-area.ts +8 -8
- package/dist/ui/components/spinner/spinner.test.ts +27 -0
- package/dist/ui/components/spinner/spinner.ts +33 -23
- package/dist/ui/components/toast/toast.test.ts +37 -0
- package/dist/ui/components/toast/toast.ts +20 -4
- package/dist/ui/theme/chart-colors.test.ts +30 -0
- package/dist/ui/theme/dark.css +8 -0
- package/dist/ui/theme/default.css +8 -0
- package/dist/ui/theme/icon-colors.test.ts +30 -0
- package/dist/ui/theme/tailwind-plugin.ts +10 -1
- package/dist/utils/installed.d.ts +17 -0
- package/dist/utils/installed.js +27 -0
- package/dist/utils/installed.js.map +1 -0
- package/dist/utils/installed.test.d.ts +1 -0
- package/dist/utils/installed.test.js +41 -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 +23 -3
- package/dist/utils/resolve-paths.js.map +1 -1
- package/dist/utils/resolve-paths.test.js +34 -2
- package/dist/utils/resolve-paths.test.js.map +1 -1
- package/package.json +5 -1
- package/dist/ui/components/cartesian-chart/cartesian-chart-render.test.ts +0 -188
- package/dist/ui/components/cartesian-chart/cartesian-chart.test.ts +0 -549
- package/dist/ui/components/cartesian-chart/cartesian-chart.ts +0 -1140
- package/dist/ui/components/cartesian-chart/index.ts +0 -18
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Component, signal } from '@angular/core';
|
|
2
2
|
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
|
|
3
3
|
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
4
|
-
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { DURATION } from '../../utils/animate';
|
|
5
6
|
import { setInputSignal } from '../../../test-utils/set-input-signal';
|
|
6
7
|
import { UiBottomSheet } from './bottom-sheet';
|
|
7
8
|
|
|
8
9
|
@Component({
|
|
9
|
-
standalone: true,
|
|
10
10
|
imports: [UiBottomSheet, LYNX_ELEMENTS],
|
|
11
11
|
template: `<ui-bottom-sheet [(open)]="open"
|
|
12
12
|
><text>Panel</text></ui-bottom-sheet
|
|
@@ -17,6 +17,47 @@ class BottomSheetHost {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
describe('UiBottomSheet', () => {
|
|
20
|
+
it('stops its close timers when it is destroyed while closing', async () => {
|
|
21
|
+
const warn = vi.spyOn(console, 'warn');
|
|
22
|
+
const { componentRef, unmount } = await render(UiBottomSheet);
|
|
23
|
+
const inst = componentRef.instance as UiBottomSheet;
|
|
24
|
+
inst.open.set(true);
|
|
25
|
+
for (let i = 0; i < 5; i++) await waitForUpdate();
|
|
26
|
+
inst.open.set(false);
|
|
27
|
+
await waitForUpdate();
|
|
28
|
+
// Destroyed during the close animation, as an `@if` around the sheet
|
|
29
|
+
// would do. Its close timer (DURATION.normal + 20) used to fire anyway
|
|
30
|
+
// and emit `closed` from the destroyed output.
|
|
31
|
+
unmount();
|
|
32
|
+
await new Promise((r) => setTimeout(r, DURATION.normal + 50));
|
|
33
|
+
const warnings = warn.mock.calls.map(([message]) => String(message));
|
|
34
|
+
warn.mockRestore();
|
|
35
|
+
expect(warnings.filter((message) => message.includes('NG0953'))).toEqual(
|
|
36
|
+
[],
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('moves a screen reader into the panel and keeps it there while open', async () => {
|
|
41
|
+
const { componentRef, container } = await render(UiBottomSheet);
|
|
42
|
+
const inst = componentRef.instance as UiBottomSheet;
|
|
43
|
+
const panel = container.querySelector(
|
|
44
|
+
'[accessibility-exclusive-focus]',
|
|
45
|
+
) as Element & { invoke?: unknown };
|
|
46
|
+
expect(panel.getAttribute('accessibility-exclusive-focus')).toBe('false');
|
|
47
|
+
const invoke = vi.fn();
|
|
48
|
+
panel.invoke = invoke;
|
|
49
|
+
(inst as unknown as { backdropRef: unknown }).backdropRef = signal({
|
|
50
|
+
nativeElement: panel,
|
|
51
|
+
});
|
|
52
|
+
(inst as unknown as { panelRef: unknown }).panelRef = signal({
|
|
53
|
+
nativeElement: panel,
|
|
54
|
+
});
|
|
55
|
+
inst.open.set(true);
|
|
56
|
+
for (let i = 0; i < 5; i++) await waitForUpdate();
|
|
57
|
+
expect(invoke).toHaveBeenCalledWith('requestAccessibilityFocus');
|
|
58
|
+
expect(panel.getAttribute('accessibility-exclusive-focus')).toBe('true');
|
|
59
|
+
});
|
|
60
|
+
|
|
20
61
|
const patchRefs = (inst: UiBottomSheet, view: Element) => {
|
|
21
62
|
(inst as unknown as { backdropRef: unknown }).backdropRef = signal({
|
|
22
63
|
nativeElement: view,
|
|
@@ -68,12 +109,18 @@ describe('UiBottomSheet', () => {
|
|
|
68
109
|
it('open effect bails when backdrop/panel refs are undefined', async () => {
|
|
69
110
|
const { componentRef } = await render(UiBottomSheet);
|
|
70
111
|
const inst = componentRef.instance as UiBottomSheet;
|
|
112
|
+
const closed = vi.fn();
|
|
113
|
+
inst.closed.subscribe(closed);
|
|
71
114
|
inst.open.set(true);
|
|
72
115
|
await waitForUpdate();
|
|
73
116
|
await waitForUpdate();
|
|
74
117
|
inst.open.set(false);
|
|
75
118
|
await waitForUpdate();
|
|
119
|
+
// Wait out the close sequence, so #animateOut runs (and bails) while the
|
|
120
|
+
// component is still alive. Destroying it clears the sequence's timers.
|
|
121
|
+
await new Promise((r) => setTimeout(r, DURATION.normal + 50));
|
|
76
122
|
expect(inst.open()).toBe(false);
|
|
123
|
+
expect(closed).toHaveBeenCalledOnce();
|
|
77
124
|
});
|
|
78
125
|
|
|
79
126
|
it('drag beyond the dismiss threshold closes the sheet', async () => {
|
|
@@ -81,19 +128,32 @@ describe('UiBottomSheet', () => {
|
|
|
81
128
|
const inst = componentRef.instance as UiBottomSheet;
|
|
82
129
|
const view = container.querySelector('view') as Element;
|
|
83
130
|
patchRefs(inst, view);
|
|
131
|
+
const animate = vi.spyOn(
|
|
132
|
+
view as Element & { animate: () => unknown },
|
|
133
|
+
'animate',
|
|
134
|
+
);
|
|
84
135
|
inst.open.set(true);
|
|
85
136
|
await waitForUpdate();
|
|
86
137
|
|
|
87
138
|
const cast = inst as unknown as {
|
|
88
139
|
onHandleTouchStart: (e: { touches: { clientY: number }[] }) => void;
|
|
89
140
|
onHandleTouchMove: (e: { touches: { clientY: number }[] }) => void;
|
|
90
|
-
|
|
141
|
+
onDragEnd: () => void;
|
|
91
142
|
};
|
|
92
143
|
cast.onHandleTouchStart({ touches: [{ clientY: 100 }] });
|
|
93
144
|
cast.onHandleTouchMove({ touches: [{ clientY: 300 }] });
|
|
94
|
-
cast.
|
|
145
|
+
cast.onDragEnd();
|
|
95
146
|
await waitForUpdate();
|
|
96
147
|
expect(inst.open()).toBe(false);
|
|
148
|
+
// The exit slide starts the next tick, from where the finger let go
|
|
149
|
+
// rather than from the docked position.
|
|
150
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
151
|
+
expect(
|
|
152
|
+
animate.mock.calls.some(
|
|
153
|
+
([keyframes]) =>
|
|
154
|
+
(keyframes as Keyframe[])[0]?.transform === 'translateY(200px)',
|
|
155
|
+
),
|
|
156
|
+
).toBe(true);
|
|
97
157
|
});
|
|
98
158
|
|
|
99
159
|
it('drag below the dismiss threshold snaps back to open', async () => {
|
|
@@ -107,11 +167,11 @@ describe('UiBottomSheet', () => {
|
|
|
107
167
|
const cast = inst as unknown as {
|
|
108
168
|
onHandleTouchStart: (e: { touches: { clientY: number }[] }) => void;
|
|
109
169
|
onHandleTouchMove: (e: { touches: { clientY: number }[] }) => void;
|
|
110
|
-
|
|
170
|
+
onDragEnd: () => void;
|
|
111
171
|
};
|
|
112
172
|
cast.onHandleTouchStart({ touches: [{ clientY: 100 }] });
|
|
113
173
|
cast.onHandleTouchMove({ touches: [{ clientY: 130 }] });
|
|
114
|
-
cast.
|
|
174
|
+
cast.onDragEnd();
|
|
115
175
|
await waitForUpdate();
|
|
116
176
|
expect(inst.open()).toBe(true);
|
|
117
177
|
});
|
|
@@ -122,7 +182,7 @@ describe('UiBottomSheet', () => {
|
|
|
122
182
|
const cast = inst as unknown as {
|
|
123
183
|
onHandleTouchStart: (e: { touches?: { clientY: number }[] }) => void;
|
|
124
184
|
onHandleTouchMove: (e: { touches?: { clientY: number }[] }) => void;
|
|
125
|
-
|
|
185
|
+
onDragEnd: () => void;
|
|
126
186
|
};
|
|
127
187
|
// touchmove without prior touchstart is a no-op (guards `#isDragging`).
|
|
128
188
|
cast.onHandleTouchMove({ touches: [{ clientY: 200 }] });
|
|
@@ -131,8 +191,130 @@ describe('UiBottomSheet', () => {
|
|
|
131
191
|
cast.onHandleTouchStart({ touches: [{ clientY: 100 }] });
|
|
132
192
|
cast.onHandleTouchMove({ touches: [] });
|
|
133
193
|
// touchend without an active drag is also a no-op.
|
|
134
|
-
cast.
|
|
135
|
-
cast.
|
|
194
|
+
cast.onDragEnd();
|
|
195
|
+
cast.onDragEnd();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
type MouseDrag = {
|
|
199
|
+
onHandleMouseDown: (e: { y: number }) => void;
|
|
200
|
+
onMouseDragMove: (e: { y: number; buttons: number }) => void;
|
|
201
|
+
onDragEnd: () => void;
|
|
202
|
+
onBackdropMouseDown: () => void;
|
|
203
|
+
onBackdropTap: () => void;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
it('mouse drag beyond the dismiss threshold closes the sheet', async () => {
|
|
207
|
+
const { componentRef, container } = await render(UiBottomSheet);
|
|
208
|
+
const inst = componentRef.instance as UiBottomSheet;
|
|
209
|
+
patchRefs(inst, container.querySelector('view') as Element);
|
|
210
|
+
inst.open.set(true);
|
|
211
|
+
await waitForUpdate();
|
|
212
|
+
|
|
213
|
+
const cast = inst as unknown as MouseDrag;
|
|
214
|
+
cast.onHandleMouseDown({ y: 100 });
|
|
215
|
+
cast.onMouseDragMove({ y: 300, buttons: 1 });
|
|
216
|
+
cast.onDragEnd();
|
|
217
|
+
await waitForUpdate();
|
|
218
|
+
expect(inst.open()).toBe(false);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('mouse drag below the dismiss threshold snaps back to open', async () => {
|
|
222
|
+
const { componentRef, container } = await render(UiBottomSheet);
|
|
223
|
+
const inst = componentRef.instance as UiBottomSheet;
|
|
224
|
+
patchRefs(inst, container.querySelector('view') as Element);
|
|
225
|
+
inst.open.set(true);
|
|
226
|
+
await waitForUpdate();
|
|
227
|
+
|
|
228
|
+
const cast = inst as unknown as MouseDrag;
|
|
229
|
+
cast.onHandleMouseDown({ y: 100 });
|
|
230
|
+
cast.onMouseDragMove({ y: 130, buttons: 1 });
|
|
231
|
+
cast.onDragEnd();
|
|
232
|
+
await waitForUpdate();
|
|
233
|
+
expect(inst.open()).toBe(true);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('ignores the backdrop click that follows a moved mouse drag', async () => {
|
|
237
|
+
const { componentRef } = await render(UiBottomSheet);
|
|
238
|
+
const inst = componentRef.instance as UiBottomSheet;
|
|
239
|
+
inst.open.set(true);
|
|
240
|
+
await waitForUpdate();
|
|
241
|
+
|
|
242
|
+
const cast = inst as unknown as MouseDrag;
|
|
243
|
+
// A drag released above the panel: the browser's click lands on the
|
|
244
|
+
// backdrop, and must not close the sheet that just snapped back.
|
|
245
|
+
cast.onHandleMouseDown({ y: 100 });
|
|
246
|
+
cast.onMouseDragMove({ y: 40, buttons: 1 });
|
|
247
|
+
cast.onDragEnd();
|
|
248
|
+
cast.onBackdropTap();
|
|
249
|
+
expect(inst.open()).toBe(true);
|
|
250
|
+
|
|
251
|
+
// Only that one click is ignored: the next backdrop tap closes it.
|
|
252
|
+
cast.onBackdropTap();
|
|
253
|
+
expect(inst.open()).toBe(false);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('a new press clears an ignore no click consumed', async () => {
|
|
257
|
+
const { componentRef } = await render(UiBottomSheet);
|
|
258
|
+
const inst = componentRef.instance as UiBottomSheet;
|
|
259
|
+
inst.open.set(true);
|
|
260
|
+
await waitForUpdate();
|
|
261
|
+
|
|
262
|
+
const cast = inst as unknown as MouseDrag;
|
|
263
|
+
// Released over the panel, so its catchtap ate the click.
|
|
264
|
+
cast.onHandleMouseDown({ y: 100 });
|
|
265
|
+
cast.onMouseDragMove({ y: 120, buttons: 1 });
|
|
266
|
+
cast.onDragEnd();
|
|
267
|
+
// A real backdrop click starts with a mousedown on the backdrop.
|
|
268
|
+
cast.onBackdropMouseDown();
|
|
269
|
+
cast.onBackdropTap();
|
|
270
|
+
expect(inst.open()).toBe(false);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('a mouse press without a move, or a touch drag, leaves the backdrop tap alone', async () => {
|
|
274
|
+
const { componentRef } = await render(UiBottomSheet);
|
|
275
|
+
const inst = componentRef.instance as UiBottomSheet;
|
|
276
|
+
inst.open.set(true);
|
|
277
|
+
await waitForUpdate();
|
|
278
|
+
|
|
279
|
+
const cast = inst as unknown as MouseDrag & {
|
|
280
|
+
onHandleTouchStart: (e: { touches: { clientY: number }[] }) => void;
|
|
281
|
+
onHandleTouchMove: (e: { touches: { clientY: number }[] }) => void;
|
|
282
|
+
};
|
|
283
|
+
cast.onHandleMouseDown({ y: 100 });
|
|
284
|
+
cast.onDragEnd();
|
|
285
|
+
// A moved touch is never followed by a tap, so nothing is ignored.
|
|
286
|
+
cast.onHandleTouchStart({ touches: [{ clientY: 100 }] });
|
|
287
|
+
cast.onHandleTouchMove({ touches: [{ clientY: 120 }] });
|
|
288
|
+
cast.onDragEnd();
|
|
289
|
+
cast.onBackdropTap();
|
|
290
|
+
expect(inst.open()).toBe(false);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('ends a mouse drag whose release it never saw', async () => {
|
|
294
|
+
const { componentRef, container } = await render(UiBottomSheet);
|
|
295
|
+
const inst = componentRef.instance as UiBottomSheet;
|
|
296
|
+
patchRefs(inst, container.querySelector('view') as Element);
|
|
297
|
+
inst.open.set(true);
|
|
298
|
+
await waitForUpdate();
|
|
299
|
+
|
|
300
|
+
const cast = inst as unknown as MouseDrag;
|
|
301
|
+
cast.onHandleMouseDown({ y: 100 });
|
|
302
|
+
cast.onMouseDragMove({ y: 300, buttons: 1 });
|
|
303
|
+
// The button came up outside the window; the cursor is back without it.
|
|
304
|
+
cast.onMouseDragMove({ y: 120, buttons: 0 });
|
|
305
|
+
await waitForUpdate();
|
|
306
|
+
// Released 200px down, past the threshold, so it closes.
|
|
307
|
+
expect(inst.open()).toBe(false);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('a mouse move outside a drag leaves the sheet in place', async () => {
|
|
311
|
+
const { componentRef } = await render(UiBottomSheet);
|
|
312
|
+
const inst = componentRef.instance as UiBottomSheet;
|
|
313
|
+
// The cursor moving over an open sheet with no button pressed.
|
|
314
|
+
(inst as unknown as MouseDrag).onMouseDragMove({ y: 300, buttons: 1 });
|
|
315
|
+
expect((inst as unknown as { dragOffset: () => number }).dragOffset()).toBe(
|
|
316
|
+
0,
|
|
317
|
+
);
|
|
136
318
|
});
|
|
137
319
|
|
|
138
320
|
it('applies a user class on the panel', async () => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// cspell:words WAAPI tweens
|
|
2
2
|
import {
|
|
3
3
|
type ElementRef,
|
|
4
|
+
type OnDestroy,
|
|
4
5
|
Component,
|
|
5
6
|
ViewEncapsulation,
|
|
6
7
|
computed,
|
|
@@ -59,29 +60,44 @@ const BACKDROP_CLEAR_DISTANCE = 500;
|
|
|
59
60
|
*/
|
|
60
61
|
@Component({
|
|
61
62
|
selector: 'ui-bottom-sheet',
|
|
62
|
-
standalone: true,
|
|
63
63
|
imports: [LYNX_ELEMENTS],
|
|
64
64
|
encapsulation: ViewEncapsulation.None,
|
|
65
65
|
template: `
|
|
66
66
|
<overlay [attr.visible]="overlayVisible()" [style]="overlayStyle()">
|
|
67
|
+
<!--
|
|
68
|
+
The mouse drag's move/up listeners sit on the full-screen backdrop,
|
|
69
|
+
not the handle: a touch keeps targeting the element it started on, but
|
|
70
|
+
a mouse event targets whatever is under the cursor, so a cursor that
|
|
71
|
+
outruns the thin handle strip (or drags up off the panel) would stop
|
|
72
|
+
driving the drag. Mouse events bubble here from anywhere in the sheet,
|
|
73
|
+
including the handle and the panel. There's no mouseleave: like a
|
|
74
|
+
touch, the drag carries on until release, and Chrome doesn't deliver
|
|
75
|
+
mouseleave to Lynx web anyway (see onMouseDragMove for a release
|
|
76
|
+
outside the view).
|
|
77
|
+
-->
|
|
67
78
|
<view
|
|
68
79
|
#backdrop
|
|
69
80
|
class="h-full w-full"
|
|
70
81
|
[style]="backdropStyle()"
|
|
71
82
|
(bindtap)="onBackdropTap()"
|
|
83
|
+
(bindmousedown)="onBackdropMouseDown()"
|
|
84
|
+
(bindmousemove)="onMouseDragMove($event)"
|
|
85
|
+
(bindmouseup)="onDragEnd()"
|
|
72
86
|
>
|
|
73
87
|
<view
|
|
74
88
|
#panel
|
|
75
89
|
[class]="panelClass()"
|
|
76
90
|
[style]="panelPositionStyle()"
|
|
91
|
+
[accessibility-exclusive-focus]="overlayVisible()"
|
|
77
92
|
(catchtap)="onPanelTap()"
|
|
78
93
|
>
|
|
79
94
|
<view
|
|
80
95
|
class="items-center pb-3 pt-2 flex justify-center"
|
|
81
96
|
(catchtouchstart)="onHandleTouchStart($event)"
|
|
82
97
|
(catchtouchmove)="onHandleTouchMove($event)"
|
|
83
|
-
(catchtouchend)="
|
|
84
|
-
(catchtouchcancel)="
|
|
98
|
+
(catchtouchend)="onDragEnd()"
|
|
99
|
+
(catchtouchcancel)="onDragEnd()"
|
|
100
|
+
(catchmousedown)="onHandleMouseDown($event)"
|
|
85
101
|
>
|
|
86
102
|
<view class="h-1 w-10 rounded-full bg-muted" />
|
|
87
103
|
</view>
|
|
@@ -91,7 +107,7 @@ const BACKDROP_CLEAR_DISTANCE = 500;
|
|
|
91
107
|
</overlay>
|
|
92
108
|
`,
|
|
93
109
|
})
|
|
94
|
-
export class UiBottomSheet {
|
|
110
|
+
export class UiBottomSheet implements OnDestroy {
|
|
95
111
|
readonly open = model(false);
|
|
96
112
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
97
113
|
|
|
@@ -115,6 +131,9 @@ export class UiBottomSheet {
|
|
|
115
131
|
#backdropAnim?: AnimationHandle;
|
|
116
132
|
#panelAnim?: AnimationHandle;
|
|
117
133
|
#hasBeenOpen = false;
|
|
134
|
+
// The open and close sequences' pending timers, so ngOnDestroy can stop
|
|
135
|
+
// them.
|
|
136
|
+
readonly #timers = new Set<ReturnType<typeof setTimeout>>();
|
|
118
137
|
|
|
119
138
|
// --- Drag-to-dismiss state ---
|
|
120
139
|
// Live downward drag distance (px), applied to the panel as a translateY via
|
|
@@ -125,6 +144,15 @@ export class UiBottomSheet {
|
|
|
125
144
|
// True only between touchstart and touchend on the handle, so unrelated move
|
|
126
145
|
// events can't shift the sheet.
|
|
127
146
|
#isDragging = false;
|
|
147
|
+
// Set when a mouse drag that actually moved is released. The browser follows
|
|
148
|
+
// that mouseup with a click on the nearest element under both the mousedown
|
|
149
|
+
// and the mouseup, which is the backdrop whenever the cursor ended up off the
|
|
150
|
+
// panel, and Lynx web maps that click to the backdrop's tap. Without this, a
|
|
151
|
+
// drag released above the panel (where the sheet snaps back) would close it.
|
|
152
|
+
#ignoreNextBackdropTap = false;
|
|
153
|
+
// Whether the current drag is a mouse drag that has moved: only those are
|
|
154
|
+
// followed by a click. A moved touch produces no tap.
|
|
155
|
+
#mouseDragMoved = false;
|
|
128
156
|
|
|
129
157
|
constructor() {
|
|
130
158
|
// `#hasBeenOpen` prevents the close animation from running on the initial
|
|
@@ -184,9 +212,24 @@ export class UiBottomSheet {
|
|
|
184
212
|
});
|
|
185
213
|
|
|
186
214
|
protected onBackdropTap(): void {
|
|
215
|
+
if (this.#ignoreNextBackdropTap) {
|
|
216
|
+
this.#ignoreNextBackdropTap = false;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
187
219
|
this.open.set(false);
|
|
188
220
|
}
|
|
189
221
|
|
|
222
|
+
/**
|
|
223
|
+
* A fresh press starts a fresh click, so drop any ignore left over from a
|
|
224
|
+
* drag whose release didn't produce a backdrop click (one released over the
|
|
225
|
+
* panel, whose `catchtap` swallows it). Otherwise the next real backdrop tap
|
|
226
|
+
* would be ignored. The handle catches its own mousedown, which is fine: it
|
|
227
|
+
* starts a new drag that resets the flag on release.
|
|
228
|
+
*/
|
|
229
|
+
protected onBackdropMouseDown(): void {
|
|
230
|
+
this.#ignoreNextBackdropTap = false;
|
|
231
|
+
}
|
|
232
|
+
|
|
190
233
|
/**
|
|
191
234
|
* No-op tap handler for the panel. `catchtap` (vs `bindtap`) already stops
|
|
192
235
|
* the tap from bubbling to the backdrop — Lynx controls propagation via the
|
|
@@ -196,19 +239,14 @@ export class UiBottomSheet {
|
|
|
196
239
|
protected onPanelTap(): void {}
|
|
197
240
|
|
|
198
241
|
/**
|
|
199
|
-
* Begin a drag on the handle strip.
|
|
200
|
-
* translateY(0%) via fill:'forwards', which would override our inline
|
|
201
|
-
* transform — cancel it so `dragOffset` (applied through panelPositionStyle)
|
|
202
|
-
* actually moves the panel from here on.
|
|
242
|
+
* Begin a drag on the handle strip.
|
|
203
243
|
*/
|
|
204
244
|
protected onHandleTouchStart(e: {
|
|
205
245
|
touches?: ReadonlyArray<{ clientY: number }>;
|
|
206
246
|
}): void {
|
|
207
247
|
const y = e.touches?.[0]?.clientY;
|
|
208
248
|
if (y == null) return;
|
|
209
|
-
this.#
|
|
210
|
-
this.#isDragging = true;
|
|
211
|
-
this.#dragStartY = y;
|
|
249
|
+
this.#startDrag(y);
|
|
212
250
|
}
|
|
213
251
|
|
|
214
252
|
protected onHandleTouchMove(e: {
|
|
@@ -217,20 +255,53 @@ export class UiBottomSheet {
|
|
|
217
255
|
if (!this.#isDragging) return;
|
|
218
256
|
const y = e.touches?.[0]?.clientY;
|
|
219
257
|
if (y == null) return;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
258
|
+
this.#moveDrag(y);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The mouse twin of onHandleTouchStart, so the handle drags on web. A
|
|
263
|
+
* desktop browser fires no touch events for a mouse, and Lynx web passes
|
|
264
|
+
* DOM events straight through rather than turning mouse input into touches.
|
|
265
|
+
* On a touch-only device no mouse events fire, so touch keeps driving it.
|
|
266
|
+
*
|
|
267
|
+
* Reads `y`, not `clientY`: Lynx PC reports a mouse event's clientY relative
|
|
268
|
+
* to the element under the cursor, which changes as the cursor moves across
|
|
269
|
+
* the sheet, while `y` is relative to the Lynx view on PC and web alike.
|
|
270
|
+
*/
|
|
271
|
+
protected onHandleMouseDown(e: { y: number }): void {
|
|
272
|
+
this.#startDrag(e.y);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Bound on the backdrop (see the template), so it also sees every move the
|
|
277
|
+
* cursor makes over an open sheet. Only those during a drag count.
|
|
278
|
+
*/
|
|
279
|
+
protected onMouseDragMove(e: { y: number; buttons: number }): void {
|
|
280
|
+
if (!this.#isDragging) return;
|
|
281
|
+
// No button held means the mouseup happened where the sheet couldn't see
|
|
282
|
+
// it: a release outside the Lynx view (past the edge of a docs preview, or
|
|
283
|
+
// outside the browser window) goes to the page, and web-core only listens
|
|
284
|
+
// inside the view. Treat the first move back as the release, or the panel
|
|
285
|
+
// would stay mid-drag.
|
|
286
|
+
if (e.buttons === 0) {
|
|
287
|
+
this.onDragEnd();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
this.#mouseDragMoved = true;
|
|
291
|
+
this.#moveDrag(e.y);
|
|
223
292
|
}
|
|
224
293
|
|
|
225
294
|
/**
|
|
226
|
-
* Release the drag
|
|
295
|
+
* Release the drag, from a touchend, touchcancel or mouseup, or a mouse
|
|
296
|
+
* move with no button held. Past the threshold we set `open` to false and let the
|
|
227
297
|
* effect drive teardown (#doClose → drag-aware #animateOut), which keeps the
|
|
228
298
|
* two-way `open` binding in sync and reuses the `closed` emit. Otherwise the
|
|
229
299
|
* sheet springs back to fully open.
|
|
230
300
|
*/
|
|
231
|
-
protected
|
|
301
|
+
protected onDragEnd(): void {
|
|
232
302
|
if (!this.#isDragging) return;
|
|
233
303
|
this.#isDragging = false;
|
|
304
|
+
this.#ignoreNextBackdropTap = this.#mouseDragMoved;
|
|
234
305
|
if (this.dragOffset() > DISMISS_THRESHOLD) {
|
|
235
306
|
this.open.set(false);
|
|
236
307
|
} else {
|
|
@@ -238,15 +309,55 @@ export class UiBottomSheet {
|
|
|
238
309
|
}
|
|
239
310
|
}
|
|
240
311
|
|
|
312
|
+
/**
|
|
313
|
+
* The open slideIn holds the panel at translateY(0%) via fill:'forwards',
|
|
314
|
+
* which would override our inline transform — cancel it so `dragOffset`
|
|
315
|
+
* (applied through panelPositionStyle) actually moves the panel from here on.
|
|
316
|
+
*/
|
|
317
|
+
#startDrag(y: number): void {
|
|
318
|
+
this.#panelAnim?.cancel();
|
|
319
|
+
this.#isDragging = true;
|
|
320
|
+
this.#mouseDragMoved = false;
|
|
321
|
+
this.#dragStartY = y;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
#moveDrag(y: number): void {
|
|
325
|
+
// Clamp to >= 0: the sheet is already docked at the bottom, so only a
|
|
326
|
+
// downward (dismiss-direction) drag should move it.
|
|
327
|
+
this.dragOffset.set(Math.max(0, y - this.#dragStartY));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* The sheet can be destroyed while its open or close sequence is still
|
|
332
|
+
* running, for example by an `@if` around it or a route change right after
|
|
333
|
+
* closing it. The rest of the sequence then ran on the destroyed component:
|
|
334
|
+
* it animated detached elements and emitted `closed` from a destroyed
|
|
335
|
+
* output, which Angular warns about in dev builds (NG0953).
|
|
336
|
+
*/
|
|
337
|
+
ngOnDestroy(): void {
|
|
338
|
+
for (const timer of this.#timers) clearTimeout(timer);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* `setTimeout`, tracked so that ngOnDestroy can cancel it.
|
|
343
|
+
*/
|
|
344
|
+
#later(callback: () => void, ms: number): void {
|
|
345
|
+
const timer = setTimeout(() => {
|
|
346
|
+
this.#timers.delete(timer);
|
|
347
|
+
callback();
|
|
348
|
+
}, ms);
|
|
349
|
+
this.#timers.add(timer);
|
|
350
|
+
}
|
|
351
|
+
|
|
241
352
|
/**
|
|
242
353
|
* Two-phase open: make the overlay visible first so native elements exist
|
|
243
354
|
* in the tree, then animate on the next frame. Without this, animate()
|
|
244
355
|
* targets elements that haven't been flushed to native yet and silently fails.
|
|
245
356
|
*/
|
|
246
357
|
#doOpen(): void {
|
|
247
|
-
|
|
358
|
+
this.#later(() => {
|
|
248
359
|
this.overlayVisible.set(true);
|
|
249
|
-
|
|
360
|
+
this.#later(() => this.#animateIn(), 0);
|
|
250
361
|
}, 0);
|
|
251
362
|
}
|
|
252
363
|
|
|
@@ -257,9 +368,9 @@ export class UiBottomSheet {
|
|
|
257
368
|
* before the fade finishes.
|
|
258
369
|
*/
|
|
259
370
|
#doClose(): void {
|
|
260
|
-
|
|
371
|
+
this.#later(() => {
|
|
261
372
|
this.#animateOut();
|
|
262
|
-
|
|
373
|
+
this.#later(() => {
|
|
263
374
|
this.overlayVisible.set(false);
|
|
264
375
|
this.closed.emit();
|
|
265
376
|
// Reset for the next open: the sheet returns to its docked position
|
|
@@ -278,6 +389,11 @@ export class UiBottomSheet {
|
|
|
278
389
|
this.#panelAnim?.cancel();
|
|
279
390
|
|
|
280
391
|
this.#backdropAnim = fadeIn(backdrop, { duration: DURATION.normal });
|
|
392
|
+
// Move a screen reader into the panel. It would otherwise stay on whatever
|
|
393
|
+
// opened it, which is now behind the overlay. With the panel's
|
|
394
|
+
// accessibility-exclusive-focus (see the template), it then can't leave
|
|
395
|
+
// the panel until it closes.
|
|
396
|
+
panel.invoke?.('requestAccessibilityFocus');
|
|
281
397
|
this.#panelAnim = slideIn(panel, 'up', {
|
|
282
398
|
duration: DURATION.slow,
|
|
283
399
|
easing: EASING.sheet,
|
|
@@ -91,6 +91,25 @@ describe('UiButton (render)', () => {
|
|
|
91
91
|
expect(inst.spinnerSize()).toBe('sm');
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
it("colors the spinner like each variant's label", async () => {
|
|
95
|
+
// The template binding doesn't reach the spinner's input under JIT (see
|
|
96
|
+
// setInputSignal), so check the value the button passes it.
|
|
97
|
+
const { componentRef } = await render(UiButton);
|
|
98
|
+
const inst = componentRef.instance as unknown as UiButton & {
|
|
99
|
+
spinnerColor: () => string;
|
|
100
|
+
};
|
|
101
|
+
for (const [variant, color] of [
|
|
102
|
+
['default', 'var(--primary-foreground)'],
|
|
103
|
+
['destructive', 'var(--destructive-foreground)'],
|
|
104
|
+
['outline', 'var(--foreground)'],
|
|
105
|
+
['secondary', 'var(--secondary-foreground)'],
|
|
106
|
+
['ghost', 'var(--foreground)'],
|
|
107
|
+
] as const) {
|
|
108
|
+
setInputSignal(inst.variant, variant);
|
|
109
|
+
expect(inst.spinnerColor()).toBe(color);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
94
113
|
it('press handlers run cleanly (guarded by disabled/loading state)', async () => {
|
|
95
114
|
// onPressStart/End are no-ops when the button is disabled/loading; the
|
|
96
115
|
// active-press path relies on the ref being wired up on-device — assert
|
|
@@ -1,18 +1,5 @@
|
|
|
1
|
-
import { describe, expect, it
|
|
2
|
-
|
|
3
|
-
vi.mock('@angular/core', () => ({
|
|
4
|
-
Component: () => () => {},
|
|
5
|
-
ViewEncapsulation: { None: 0 },
|
|
6
|
-
computed: () => () => {},
|
|
7
|
-
input: () => () => {},
|
|
8
|
-
output: () => () => {},
|
|
9
|
-
}));
|
|
10
|
-
|
|
11
|
-
vi.mock('@blotch/angular-lynx', () => ({
|
|
12
|
-
LYNX_ELEMENTS: [],
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
const { buttonVariants } = await import('./button');
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { buttonVariants } from './button';
|
|
16
3
|
|
|
17
4
|
describe('buttonVariants', () => {
|
|
18
5
|
it('returns default variant and size classes', () => {
|
|
@@ -65,6 +65,19 @@ const buttonTextVariants = cva('font-medium', {
|
|
|
65
65
|
},
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* The spinner's color for each variant: the same token as the label's text
|
|
70
|
+
* class above. The spinner draws with SVG, which can't read the text classes,
|
|
71
|
+
* so it gets the token and resolves it itself (see icon-theme.ts).
|
|
72
|
+
*/
|
|
73
|
+
const SPINNER_COLORS = {
|
|
74
|
+
default: 'var(--primary-foreground)',
|
|
75
|
+
destructive: 'var(--destructive-foreground)',
|
|
76
|
+
outline: 'var(--foreground)',
|
|
77
|
+
secondary: 'var(--secondary-foreground)',
|
|
78
|
+
ghost: 'var(--foreground)',
|
|
79
|
+
} as const;
|
|
80
|
+
|
|
68
81
|
export type ButtonVariant = NonNullable<
|
|
69
82
|
VariantProps<typeof buttonVariants>['variant']
|
|
70
83
|
>;
|
|
@@ -74,7 +87,6 @@ export type ButtonSize = NonNullable<
|
|
|
74
87
|
|
|
75
88
|
@Component({
|
|
76
89
|
selector: 'ui-button',
|
|
77
|
-
standalone: true,
|
|
78
90
|
imports: [LYNX_ELEMENTS, UiSpinner],
|
|
79
91
|
encapsulation: ViewEncapsulation.None,
|
|
80
92
|
template: `
|
|
@@ -100,7 +112,7 @@ export type ButtonSize = NonNullable<
|
|
|
100
112
|
(bindtap)="onTap()"
|
|
101
113
|
>
|
|
102
114
|
@if (loading()) {
|
|
103
|
-
<ui-spinner [size]="spinnerSize()" />
|
|
115
|
+
<ui-spinner [size]="spinnerSize()" [color]="spinnerColor()" />
|
|
104
116
|
} @else {
|
|
105
117
|
<text [class]="labelClass()"><ng-content /></text>
|
|
106
118
|
}
|
|
@@ -136,6 +148,10 @@ export class UiButton {
|
|
|
136
148
|
return s === 'sm' ? ('xs' as const) : ('sm' as const);
|
|
137
149
|
});
|
|
138
150
|
|
|
151
|
+
protected readonly spinnerColor = computed(
|
|
152
|
+
() => SPINNER_COLORS[this.variant()],
|
|
153
|
+
);
|
|
154
|
+
|
|
139
155
|
protected onPressStart(): void {
|
|
140
156
|
if (this.disabled() || this.loading()) return;
|
|
141
157
|
// pressDown/pressRelease drive el.setStyle() based CSS transitions on the
|
|
@@ -1,24 +1,5 @@
|
|
|
1
|
-
import { describe, expect, it
|
|
2
|
-
|
|
3
|
-
// Mock Angular so importing the component module doesn't require the framework
|
|
4
|
-
// runtime. Only the exported `cva` variant fn is exercised here; the class field
|
|
5
|
-
// initializers (input/output/viewChild/inject) never run because we never
|
|
6
|
-
// construct the components.
|
|
7
|
-
vi.mock('@angular/core', () => ({
|
|
8
|
-
Component: () => () => {},
|
|
9
|
-
ViewEncapsulation: { None: 0 },
|
|
10
|
-
computed: () => () => {},
|
|
11
|
-
inject: () => ({}),
|
|
12
|
-
input: () => () => {},
|
|
13
|
-
output: () => () => {},
|
|
14
|
-
viewChild: () => () => {},
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
vi.mock('@blotch/angular-lynx', () => ({
|
|
18
|
-
LYNX_ELEMENTS: [],
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
|
-
const { buttonGroupItemVariants } = await import('./button-group');
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { buttonGroupItemVariants } from './button-group';
|
|
22
3
|
|
|
23
4
|
describe('buttonGroupItemVariants', () => {
|
|
24
5
|
it('returns default variant and size classes', () => {
|