@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,7 +1,11 @@
|
|
|
1
|
-
import { Component } from '@angular/core';
|
|
2
|
-
import {
|
|
1
|
+
import { Component, signal } 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 { describe, expect, it } from 'vitest';
|
|
8
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
9
|
import {
|
|
6
10
|
UiSheet,
|
|
7
11
|
UiSheetDescription,
|
|
@@ -11,7 +15,6 @@ import {
|
|
|
11
15
|
} from './sheet';
|
|
12
16
|
|
|
13
17
|
@Component({
|
|
14
|
-
standalone: true,
|
|
15
18
|
imports: [
|
|
16
19
|
UiSheet,
|
|
17
20
|
UiSheetHeader,
|
|
@@ -21,24 +24,74 @@ import {
|
|
|
21
24
|
LYNX_ELEMENTS,
|
|
22
25
|
],
|
|
23
26
|
template: `
|
|
24
|
-
<ui-sheet [(open)]="open">
|
|
25
|
-
<ui-sheet-header>
|
|
26
|
-
<ui-sheet-title>Title</ui-sheet-title>
|
|
27
|
-
<ui-sheet-description>Body</ui-sheet-description>
|
|
27
|
+
<ui-sheet [(open)]="open" class="gap-4" (closed)="closed()">
|
|
28
|
+
<ui-sheet-header class="items-center">
|
|
29
|
+
<ui-sheet-title class="text-xl">Title</ui-sheet-title>
|
|
30
|
+
<ui-sheet-description class="italic">Body</ui-sheet-description>
|
|
28
31
|
</ui-sheet-header>
|
|
29
|
-
<ui-sheet-footer><text>Buttons</text></ui-sheet-footer>
|
|
32
|
+
<ui-sheet-footer class="flex-row"><text>Buttons</text></ui-sheet-footer>
|
|
30
33
|
</ui-sheet>
|
|
31
34
|
`,
|
|
32
35
|
})
|
|
33
36
|
class SheetHost {
|
|
34
|
-
open =
|
|
37
|
+
readonly open = signal(false);
|
|
38
|
+
readonly closed = vi.fn();
|
|
35
39
|
}
|
|
36
40
|
|
|
41
|
+
const renderSheet = async () => {
|
|
42
|
+
const result = await render(SheetHost);
|
|
43
|
+
return {
|
|
44
|
+
...result,
|
|
45
|
+
host: result.componentRef.instance as SheetHost,
|
|
46
|
+
overlay: result.container.querySelector('overlay')!,
|
|
47
|
+
backdrop: result.container.querySelector('overlay > view')!,
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Opens the sheet and waits out its open sequence: the overlay shows on one
|
|
53
|
+
* tick, and the panel slides in on the next.
|
|
54
|
+
*/
|
|
55
|
+
const openSheet = async (host: SheetHost) => {
|
|
56
|
+
host.open.set(true);
|
|
57
|
+
for (let i = 0; i < 3; i++) await waitForUpdate();
|
|
58
|
+
};
|
|
59
|
+
|
|
37
60
|
describe('UiSheet', () => {
|
|
38
61
|
it('renders header, title, description, and footer', async () => {
|
|
39
|
-
const { getByText } = await
|
|
62
|
+
const { getByText } = await renderSheet();
|
|
40
63
|
expect(getByText('Title')).toBeTruthy();
|
|
41
64
|
expect(getByText('Body')).toBeTruthy();
|
|
42
65
|
expect(getByText('Buttons')).toBeTruthy();
|
|
43
66
|
});
|
|
67
|
+
|
|
68
|
+
it("merges each part's class with its own", async () => {
|
|
69
|
+
const { container, getByText } = await renderSheet();
|
|
70
|
+
// The sheet's class goes on the padded content wrapper, not the panel.
|
|
71
|
+
expect(container.querySelector('.px-6')?.classList).toContain('gap-4');
|
|
72
|
+
expect(container.querySelector('.mb-4')?.classList).toContain(
|
|
73
|
+
'items-center',
|
|
74
|
+
);
|
|
75
|
+
expect(getByText('Title').classList).toContain('text-xl');
|
|
76
|
+
expect(getByText('Title').classList).toContain('font-semibold');
|
|
77
|
+
expect(getByText('Body').classList).toContain('italic');
|
|
78
|
+
expect(getByText('Body').classList).toContain('text-muted-foreground');
|
|
79
|
+
expect(container.querySelector('.pt-4')?.classList).toContain('flex-row');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('opens with `open`, and reports a dismissal through it and `closed`', async () => {
|
|
83
|
+
const { host, overlay, backdrop } = await renderSheet();
|
|
84
|
+
expect(overlay.getAttribute('visible')).toBe('false');
|
|
85
|
+
|
|
86
|
+
await openSheet(host);
|
|
87
|
+
expect(overlay.getAttribute('visible')).toBe('true');
|
|
88
|
+
|
|
89
|
+
fireEvent.tap(backdrop);
|
|
90
|
+
expect(host.open()).toBe(false);
|
|
91
|
+
// Once the panel has slid out, the overlay hides and `closed` fires.
|
|
92
|
+
await vi.waitFor(() => expect(host.closed).toHaveBeenCalled());
|
|
93
|
+
await waitForUpdate();
|
|
94
|
+
expect(overlay.getAttribute('visible')).toBe('false');
|
|
95
|
+
expect(host.closed).toHaveBeenCalledOnce();
|
|
96
|
+
});
|
|
44
97
|
});
|
|
@@ -26,8 +26,16 @@ import { cn } from '../../utils/cn';
|
|
|
26
26
|
standalone: true,
|
|
27
27
|
imports: [LYNX_ELEMENTS, UiBottomSheet],
|
|
28
28
|
encapsulation: ViewEncapsulation.None,
|
|
29
|
+
// `open` is bound both ways by hand rather than with `[(open)]`: for a
|
|
30
|
+
// two-way binding Angular also compiles a fallback that assigns the field,
|
|
31
|
+
// which never runs for a signal and so shows as an uncovered branch in an
|
|
32
|
+
// app's coverage report.
|
|
29
33
|
template: `
|
|
30
|
-
<ui-bottom-sheet
|
|
34
|
+
<ui-bottom-sheet
|
|
35
|
+
[open]="open()"
|
|
36
|
+
(openChange)="open.set($event)"
|
|
37
|
+
(closed)="closed.emit()"
|
|
38
|
+
>
|
|
31
39
|
<view [class]="contentClass()">
|
|
32
40
|
<ng-content />
|
|
33
41
|
</view>
|
|
@@ -1,13 +1,30 @@
|
|
|
1
|
+
import { Component } from '@angular/core';
|
|
1
2
|
import { render } from '@blotch/angular-lynx-testing-library';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { lynxMainThread } from '../../../test-utils/lynx-main-thread';
|
|
3
5
|
import { UiSkeleton } from './skeleton';
|
|
4
6
|
|
|
7
|
+
@Component({
|
|
8
|
+
imports: [UiSkeleton],
|
|
9
|
+
template: `<ui-skeleton class="h-4 w-32" />`,
|
|
10
|
+
})
|
|
11
|
+
class SkeletonHost {}
|
|
12
|
+
|
|
5
13
|
describe('UiSkeleton', () => {
|
|
6
|
-
it('renders
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
it('renders a muted placeholder with the given class', async () => {
|
|
15
|
+
const { container } = await render(SkeletonHost);
|
|
16
|
+
const skeleton = container.querySelector('.bg-muted');
|
|
17
|
+
expect(skeleton?.classList).toContain('h-4');
|
|
18
|
+
expect(skeleton?.classList).toContain('w-32');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('pulses', async () => {
|
|
22
|
+
const animate = vi.spyOn(lynxMainThread(), '__ElementAnimate');
|
|
23
|
+
const { container } = await render(SkeletonHost);
|
|
24
|
+
expect(animate).toHaveBeenCalledWith(
|
|
25
|
+
container.querySelector('.bg-muted'),
|
|
26
|
+
expect.any(Array),
|
|
27
|
+
);
|
|
28
|
+
animate.mockRestore();
|
|
12
29
|
});
|
|
13
30
|
});
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from '@angular/core';
|
|
10
10
|
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { pulse } from '../../utils/animate';
|
|
13
13
|
import { cn } from '../../utils/cn';
|
|
14
14
|
|
|
15
15
|
@Component({
|
|
@@ -22,22 +22,16 @@ import { cn } from '../../utils/cn';
|
|
|
22
22
|
export class UiSkeleton {
|
|
23
23
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
24
24
|
|
|
25
|
-
readonly skeletonRef = viewChild<ElementRef>('skeleton');
|
|
26
|
-
#pulseAnim?: AnimationHandle;
|
|
25
|
+
readonly skeletonRef = viewChild.required<ElementRef>('skeleton');
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
this.#pulseAnim?.cancel();
|
|
37
|
-
this.#pulseAnim = pulse(el);
|
|
38
|
-
/* v8 ignore stop */
|
|
39
|
-
});
|
|
40
|
-
}
|
|
27
|
+
/**
|
|
28
|
+
* Pulses the skeleton. A field rather than a constructor: Angular's test
|
|
29
|
+
* build keeps a function for a constructor that nothing calls, which an
|
|
30
|
+
* app's coverage report would show as uncovered.
|
|
31
|
+
*/
|
|
32
|
+
protected readonly pulsing = effect(() => {
|
|
33
|
+
pulse(this.skeletonRef().nativeElement);
|
|
34
|
+
});
|
|
41
35
|
|
|
42
36
|
protected readonly skeletonClass = computed(() =>
|
|
43
37
|
cn('rounded-md bg-muted', this.userClass()),
|
|
@@ -1,66 +1,84 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
1
2
|
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import {
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { lynxMainThread } from '../../../test-utils/lynx-main-thread';
|
|
4
5
|
import { ICON_LIGHT_COLORS } from '../icon/icon-theme';
|
|
5
6
|
import { UiSpinner } from './spinner';
|
|
6
7
|
|
|
8
|
+
@Component({
|
|
9
|
+
imports: [UiSpinner],
|
|
10
|
+
template: `
|
|
11
|
+
<ui-spinner [size]="size()" [color]="color()" [class]="className()" />
|
|
12
|
+
`,
|
|
13
|
+
})
|
|
14
|
+
class SpinnerHost {
|
|
15
|
+
readonly size = signal<'xs' | 'sm' | 'md' | 'lg'>('md');
|
|
16
|
+
readonly color = signal<string | undefined>(undefined);
|
|
17
|
+
readonly className = signal('');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const renderSpinner = async () => {
|
|
21
|
+
const result = await render(SpinnerHost);
|
|
22
|
+
return {
|
|
23
|
+
...result,
|
|
24
|
+
host: result.componentRef.instance as SpinnerHost,
|
|
25
|
+
// The spinner's own view: the one that holds the svg.
|
|
26
|
+
spinner: () => result.container.querySelector('svg')!.parentElement!,
|
|
27
|
+
svgContent: () =>
|
|
28
|
+
result.container.querySelector('svg')!.getAttribute('content'),
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
7
32
|
describe('UiSpinner', () => {
|
|
8
|
-
it('renders the
|
|
9
|
-
const {
|
|
10
|
-
expect(
|
|
11
|
-
|
|
12
|
-
const view = container.querySelector('view');
|
|
13
|
-
expect(view?.getAttribute('style')).toContain('24px');
|
|
33
|
+
it('renders the loader at the default size', async () => {
|
|
34
|
+
const { spinner } = await renderSpinner();
|
|
35
|
+
expect(spinner().getAttribute('style')).toContain('width: 24px');
|
|
36
|
+
expect(spinner().getAttribute('style')).toContain('height: 24px');
|
|
14
37
|
});
|
|
15
38
|
|
|
16
|
-
it('
|
|
17
|
-
const {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
39
|
+
it('resizes for each supported size', async () => {
|
|
40
|
+
const { host, spinner } = await renderSpinner();
|
|
41
|
+
for (const [size, px] of [
|
|
42
|
+
['xs', 16],
|
|
43
|
+
['sm', 20],
|
|
44
|
+
['md', 24],
|
|
45
|
+
['lg', 32],
|
|
46
|
+
] as const) {
|
|
47
|
+
host.size.set(size);
|
|
48
|
+
await waitForUpdate();
|
|
49
|
+
expect(spinner().getAttribute('style')).toContain(`width: ${px}px`);
|
|
50
|
+
}
|
|
24
51
|
});
|
|
25
52
|
|
|
26
53
|
it("draws in the theme's foreground when no color is set", async () => {
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
expect(
|
|
30
|
-
expect(
|
|
54
|
+
// Lynx's SVG parser reads neither `currentColor` nor CSS variables.
|
|
55
|
+
const { svgContent } = await renderSpinner();
|
|
56
|
+
expect(svgContent()).toContain(ICON_LIGHT_COLORS['foreground']);
|
|
57
|
+
expect(svgContent()).not.toContain('currentColor');
|
|
31
58
|
});
|
|
32
59
|
|
|
33
|
-
it('
|
|
34
|
-
const {
|
|
35
|
-
|
|
36
|
-
(componentRef.instance as UiSpinner).color,
|
|
37
|
-
'var(--primary-foreground)',
|
|
38
|
-
);
|
|
60
|
+
it('draws in a literal color or a theme token', async () => {
|
|
61
|
+
const { host, svgContent } = await renderSpinner();
|
|
62
|
+
host.color.set('rgb(0, 0, 0)');
|
|
39
63
|
await waitForUpdate();
|
|
40
|
-
expect(
|
|
41
|
-
|
|
42
|
-
);
|
|
64
|
+
expect(svgContent()).toContain('rgb(0, 0, 0)');
|
|
65
|
+
|
|
66
|
+
host.color.set('var(--primary-foreground)');
|
|
67
|
+
await waitForUpdate();
|
|
68
|
+
expect(svgContent()).toContain(ICON_LIGHT_COLORS['primary-foreground']);
|
|
43
69
|
});
|
|
44
70
|
|
|
45
|
-
it('
|
|
46
|
-
const {
|
|
47
|
-
|
|
71
|
+
it('takes the class input', async () => {
|
|
72
|
+
const { host, spinner } = await renderSpinner();
|
|
73
|
+
host.className.set('ml-2');
|
|
48
74
|
await waitForUpdate();
|
|
49
|
-
expect(
|
|
75
|
+
expect(spinner().getAttribute('class')).toBe('ml-2');
|
|
50
76
|
});
|
|
51
77
|
|
|
52
|
-
it('
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
['md', 24],
|
|
58
|
-
['lg', 32],
|
|
59
|
-
] as const) {
|
|
60
|
-
setInputSignal((componentRef.instance as UiSpinner).size, size);
|
|
61
|
-
await waitForUpdate();
|
|
62
|
-
const view = container.querySelector('view');
|
|
63
|
-
expect(view?.getAttribute('style')).toContain(`${px}px`);
|
|
64
|
-
}
|
|
78
|
+
it('turns continuously', async () => {
|
|
79
|
+
const animate = vi.spyOn(lynxMainThread(), '__ElementAnimate');
|
|
80
|
+
const { spinner } = await renderSpinner();
|
|
81
|
+
expect(animate).toHaveBeenCalledWith(spinner(), expect.any(Array));
|
|
82
|
+
animate.mockRestore();
|
|
65
83
|
});
|
|
66
84
|
});
|
|
@@ -43,26 +43,23 @@ export class UiSpinner {
|
|
|
43
43
|
|
|
44
44
|
readonly #resolveColor = injectIconColor();
|
|
45
45
|
|
|
46
|
-
readonly spinnerRef = viewChild<ElementRef>('spinner');
|
|
46
|
+
readonly spinnerRef = viewChild.required<ElementRef>('spinner');
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
/* v8 ignore stop */
|
|
64
|
-
});
|
|
65
|
-
}
|
|
48
|
+
/**
|
|
49
|
+
* Turns the spinner for as long as it's shown. A field rather than a
|
|
50
|
+
* constructor: Angular's test build keeps a function for a constructor that
|
|
51
|
+
* nothing calls, which an app's coverage report would show as uncovered.
|
|
52
|
+
*/
|
|
53
|
+
protected readonly rotation = effect(() => {
|
|
54
|
+
this.spinnerRef().nativeElement.animate(
|
|
55
|
+
[{ transform: 'rotate(0deg)' }, { transform: 'rotate(360deg)' }],
|
|
56
|
+
{
|
|
57
|
+
duration: 1600,
|
|
58
|
+
iterations: Infinity,
|
|
59
|
+
easing: 'linear',
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
});
|
|
66
63
|
|
|
67
64
|
// Lynx SVG elements don't inherit the CSS `color` property into their stroke
|
|
68
65
|
// attributes, so we substitute the literal `currentColor` in the SVG string
|
|
@@ -1,63 +1,133 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import {
|
|
3
|
+
fireEvent,
|
|
4
|
+
render,
|
|
5
|
+
waitForUpdate,
|
|
6
|
+
} from '@blotch/angular-lynx-testing-library';
|
|
7
|
+
import {
|
|
8
|
+
type MockInstance,
|
|
9
|
+
afterEach,
|
|
10
|
+
beforeEach,
|
|
11
|
+
describe,
|
|
12
|
+
expect,
|
|
13
|
+
it,
|
|
14
|
+
vi,
|
|
15
|
+
} from 'vitest';
|
|
16
|
+
import {
|
|
17
|
+
type LynxMainThread,
|
|
18
|
+
lynxMainThread,
|
|
19
|
+
} from '../../../test-utils/lynx-main-thread';
|
|
4
20
|
import { UiSwitch } from './switch';
|
|
5
21
|
|
|
22
|
+
@Component({
|
|
23
|
+
imports: [UiSwitch],
|
|
24
|
+
template: `
|
|
25
|
+
<ui-switch
|
|
26
|
+
[(checked)]="checked"
|
|
27
|
+
[disabled]="disabled()"
|
|
28
|
+
class="ml-2"
|
|
29
|
+
(changed)="changes.push($event)"
|
|
30
|
+
/>
|
|
31
|
+
`,
|
|
32
|
+
})
|
|
33
|
+
class SwitchHost {
|
|
34
|
+
readonly checked = signal(false);
|
|
35
|
+
readonly disabled = signal(false);
|
|
36
|
+
readonly changes: boolean[] = [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const renderSwitch = async () => {
|
|
40
|
+
const result = await render(SwitchHost);
|
|
41
|
+
return {
|
|
42
|
+
...result,
|
|
43
|
+
host: result.componentRef.instance as SwitchHost,
|
|
44
|
+
track: () => result.container.querySelector('.flex-row')!,
|
|
45
|
+
thumb: () => result.container.querySelector('.bg-background')!,
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** `__ElementAnimate`'s first argument for starting an animation. */
|
|
50
|
+
const START = 0;
|
|
51
|
+
/** `__ElementAnimate`'s first argument for canceling one. */
|
|
52
|
+
const CANCEL = 3;
|
|
53
|
+
|
|
6
54
|
describe('UiSwitch', () => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
55
|
+
// Watched per test: the testing library resets the main thread after each.
|
|
56
|
+
let animate: MockInstance<LynxMainThread['__ElementAnimate']>;
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
animate = vi.spyOn(lynxMainThread(), '__ElementAnimate');
|
|
10
59
|
});
|
|
60
|
+
afterEach(() => animate.mockRestore());
|
|
11
61
|
|
|
12
|
-
it('
|
|
13
|
-
const {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
inst.toggle();
|
|
19
|
-
expect(inst.checked()).toBe(true);
|
|
20
|
-
inst.toggle();
|
|
21
|
-
expect(inst.checked()).toBe(false);
|
|
22
|
-
expect(emitted).toEqual([true, false]);
|
|
62
|
+
it('renders off, with the thumb at the left and the given class', async () => {
|
|
63
|
+
const { track, thumb } = await renderSwitch();
|
|
64
|
+
expect(track().classList).toContain('bg-input');
|
|
65
|
+
expect(track().classList).toContain('ml-2');
|
|
66
|
+
expect(thumb().getAttribute('style')).toContain('translateX(2px)');
|
|
23
67
|
});
|
|
24
68
|
|
|
25
|
-
it('
|
|
26
|
-
const {
|
|
27
|
-
const inst = componentRef.instance as UiSwitch;
|
|
28
|
-
setInputSignal(inst.disabled, true);
|
|
29
|
-
const listener = vi.fn();
|
|
30
|
-
inst.changed.subscribe(listener);
|
|
69
|
+
it('turns on and off on tap, emitting `changed`', async () => {
|
|
70
|
+
const { host, track, thumb } = await renderSwitch();
|
|
31
71
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
expect(
|
|
72
|
+
fireEvent.tap(track());
|
|
73
|
+
await waitForUpdate();
|
|
74
|
+
expect(host.checked()).toBe(true);
|
|
75
|
+
expect(track().classList).toContain('bg-primary');
|
|
76
|
+
expect(thumb().getAttribute('style')).toContain('translateX(22px)');
|
|
77
|
+
|
|
78
|
+
fireEvent.tap(track());
|
|
79
|
+
await waitForUpdate();
|
|
80
|
+
expect(host.checked()).toBe(false);
|
|
81
|
+
expect(host.changes).toEqual([true, false]);
|
|
35
82
|
});
|
|
36
83
|
|
|
37
|
-
it('
|
|
38
|
-
const {
|
|
39
|
-
const inst = componentRef.instance as UiSwitch;
|
|
40
|
-
let thumb = container.querySelectorAll('view')[1];
|
|
41
|
-
expect(thumb?.getAttribute('style')).toContain('translateX(2px)');
|
|
84
|
+
it('springs the thumb across the track, squishing it halfway', async () => {
|
|
85
|
+
const { track, thumb } = await renderSwitch();
|
|
42
86
|
|
|
43
|
-
|
|
87
|
+
fireEvent.tap(track());
|
|
88
|
+
expect(animate).toHaveBeenCalledWith(thumb(), [
|
|
89
|
+
START,
|
|
90
|
+
expect.any(String),
|
|
91
|
+
[
|
|
92
|
+
{ transform: 'translateX(2px) scale(1)' },
|
|
93
|
+
{ transform: 'translateX(12px) scale(1.1)' },
|
|
94
|
+
{ transform: 'translateX(22px) scale(1)' },
|
|
95
|
+
],
|
|
96
|
+
expect.objectContaining({ fill: 'forwards' }),
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
// Turning it back off cuts the first spring short and springs it back.
|
|
100
|
+
fireEvent.tap(track());
|
|
101
|
+
expect(animate).toHaveBeenCalledWith(thumb(), [CANCEL, expect.any(String)]);
|
|
102
|
+
expect(animate).toHaveBeenLastCalledWith(thumb(), [
|
|
103
|
+
START,
|
|
104
|
+
expect.any(String),
|
|
105
|
+
[
|
|
106
|
+
{ transform: 'translateX(22px) scale(1)' },
|
|
107
|
+
{ transform: 'translateX(12px) scale(1.1)' },
|
|
108
|
+
{ transform: 'translateX(2px) scale(1)' },
|
|
109
|
+
],
|
|
110
|
+
expect.objectContaining({ fill: 'forwards' }),
|
|
111
|
+
]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('follows its checked binding', async () => {
|
|
115
|
+
const { host, thumb } = await renderSwitch();
|
|
116
|
+
host.checked.set(true);
|
|
44
117
|
await waitForUpdate();
|
|
45
|
-
thumb
|
|
46
|
-
expect(thumb?.getAttribute('style')).toContain('translateX(22px)');
|
|
118
|
+
expect(thumb().getAttribute('style')).toContain('translateX(22px)');
|
|
47
119
|
});
|
|
48
120
|
|
|
49
|
-
it('
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
setInputSignal(inst.userClass, 'custom-cls');
|
|
121
|
+
it('dims and ignores taps while disabled', async () => {
|
|
122
|
+
const { host, track } = await renderSwitch();
|
|
123
|
+
host.disabled.set(true);
|
|
124
|
+
await waitForUpdate();
|
|
125
|
+
expect(track().classList).toContain('opacity-50');
|
|
126
|
+
|
|
127
|
+
fireEvent.tap(track());
|
|
57
128
|
await waitForUpdate();
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
expect(
|
|
61
|
-
expect(cls).toContain('custom-cls');
|
|
129
|
+
expect(host.checked()).toBe(false);
|
|
130
|
+
expect(host.changes).toEqual([]);
|
|
131
|
+
expect(animate).not.toHaveBeenCalled();
|
|
62
132
|
});
|
|
63
133
|
});
|
|
@@ -52,7 +52,8 @@ export class UiSwitch {
|
|
|
52
52
|
|
|
53
53
|
readonly changed = output<boolean>();
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
/** The thumb is always in the template, so toggle() can always slide it. */
|
|
56
|
+
readonly thumbRef = viewChild.required<ElementRef>('thumb');
|
|
56
57
|
#activeAnimation?: AnimationHandle;
|
|
57
58
|
|
|
58
59
|
protected readonly trackClass = computed(() =>
|
|
@@ -76,21 +77,15 @@ export class UiSwitch {
|
|
|
76
77
|
toggle(): void {
|
|
77
78
|
if (this.disabled()) return;
|
|
78
79
|
|
|
79
|
-
const thumb = this.thumbRef()
|
|
80
|
+
const thumb = this.thumbRef().nativeElement;
|
|
80
81
|
const wasChecked = this.checked();
|
|
81
82
|
const next = !wasChecked;
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const from = wasChecked ? THUMB_OFFSET_ON : THUMB_OFFSET_OFF;
|
|
89
|
-
const to = next ? THUMB_OFFSET_ON : THUMB_OFFSET_OFF;
|
|
90
|
-
// Spring translate with squish effect at midpoint
|
|
91
|
-
this.#activeAnimation = springTranslateX(thumb, from, to);
|
|
92
|
-
}
|
|
93
|
-
/* v8 ignore stop */
|
|
84
|
+
this.#activeAnimation?.cancel();
|
|
85
|
+
const from = wasChecked ? THUMB_OFFSET_ON : THUMB_OFFSET_OFF;
|
|
86
|
+
const to = next ? THUMB_OFFSET_ON : THUMB_OFFSET_OFF;
|
|
87
|
+
// Spring translate with squish effect at midpoint
|
|
88
|
+
this.#activeAnimation = springTranslateX(thumb, from, to);
|
|
94
89
|
|
|
95
90
|
this.checked.set(next);
|
|
96
91
|
this.changed.emit(next);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiTabBar, UiTabBarItem } from './tab-bar';
|