@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,71 +1,127 @@
|
|
|
1
|
-
import { Component } from '@angular/core';
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
2
|
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import {
|
|
4
|
+
type MockInstance,
|
|
5
|
+
afterEach,
|
|
6
|
+
beforeEach,
|
|
7
|
+
describe,
|
|
8
|
+
expect,
|
|
9
|
+
it,
|
|
10
|
+
vi,
|
|
11
|
+
} from 'vitest';
|
|
12
|
+
import {
|
|
13
|
+
type LynxMainThread,
|
|
14
|
+
lynxMainThread,
|
|
15
|
+
} from '../../../test-utils/lynx-main-thread';
|
|
6
16
|
import { UiProgress } from './progress';
|
|
7
17
|
|
|
8
18
|
@Component({
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
19
|
+
imports: [UiProgress],
|
|
20
|
+
template: `
|
|
21
|
+
<ui-progress
|
|
22
|
+
[value]="value()"
|
|
23
|
+
[max]="max()"
|
|
24
|
+
[indeterminate]="indeterminate()"
|
|
25
|
+
class="h-3"
|
|
26
|
+
/>
|
|
27
|
+
`,
|
|
12
28
|
})
|
|
13
29
|
class ProgressHost {
|
|
14
|
-
value = 40;
|
|
15
|
-
|
|
30
|
+
readonly value = signal(40);
|
|
31
|
+
readonly max = signal(100);
|
|
32
|
+
readonly indeterminate = signal(false);
|
|
16
33
|
}
|
|
17
34
|
|
|
35
|
+
const renderProgress = async () => {
|
|
36
|
+
const result = await render(ProgressHost);
|
|
37
|
+
return {
|
|
38
|
+
...result,
|
|
39
|
+
host: result.componentRef.instance as ProgressHost,
|
|
40
|
+
track: () => result.container.querySelector('.bg-muted')!,
|
|
41
|
+
fill: () => result.container.querySelector('.bg-primary')!,
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** `__ElementAnimate`'s first argument for starting an animation. */
|
|
46
|
+
const START = 0;
|
|
47
|
+
|
|
18
48
|
describe('UiProgress', () => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
49
|
+
// Watched per test: the testing library resets the main thread after each.
|
|
50
|
+
let animate: MockInstance<LynxMainThread['__ElementAnimate']>;
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
animate = vi.spyOn(lynxMainThread(), '__ElementAnimate');
|
|
22
53
|
});
|
|
54
|
+
afterEach(() => animate.mockRestore());
|
|
23
55
|
|
|
24
|
-
it('
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
template: `<ui-progress [indeterminate]="true" />`,
|
|
29
|
-
})
|
|
30
|
-
class IndeterminateHost {}
|
|
31
|
-
const { container } = await render(IndeterminateHost);
|
|
32
|
-
expect(container).toBeTruthy();
|
|
56
|
+
it('fills the track by the value out of max', async () => {
|
|
57
|
+
const { fill, track } = await renderProgress();
|
|
58
|
+
expect(fill().getAttribute('style')).toContain('width: 40%');
|
|
59
|
+
expect(track().classList).toContain('h-3');
|
|
33
60
|
});
|
|
34
61
|
|
|
35
|
-
it('
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
62
|
+
it('keeps the fill between 0% and 100%', async () => {
|
|
63
|
+
const { host, fill } = await renderProgress();
|
|
64
|
+
host.value.set(500);
|
|
65
|
+
await waitForUpdate();
|
|
66
|
+
expect(fill().getAttribute('style')).toContain('width: 100%');
|
|
67
|
+
|
|
68
|
+
host.value.set(-5);
|
|
69
|
+
await waitForUpdate();
|
|
70
|
+
expect(fill().getAttribute('style')).toContain('width: 0%');
|
|
71
|
+
|
|
72
|
+
// A max of 0 would divide by zero.
|
|
73
|
+
host.value.set(50);
|
|
74
|
+
host.max.set(0);
|
|
41
75
|
await waitForUpdate();
|
|
42
|
-
|
|
43
|
-
expect(fill?.getAttribute('style')).toContain('width: 25%');
|
|
76
|
+
expect(fill().getAttribute('style')).toContain('width: 0%');
|
|
44
77
|
});
|
|
45
78
|
|
|
46
|
-
it('
|
|
47
|
-
const {
|
|
48
|
-
|
|
49
|
-
|
|
79
|
+
it("doesn't animate the first value, then animates each change", async () => {
|
|
80
|
+
const { host, fill } = await renderProgress();
|
|
81
|
+
expect(animate).not.toHaveBeenCalled();
|
|
82
|
+
|
|
83
|
+
host.value.set(80);
|
|
50
84
|
await waitForUpdate();
|
|
51
|
-
|
|
52
|
-
|
|
85
|
+
expect(animate).toHaveBeenCalledWith(fill(), [
|
|
86
|
+
START,
|
|
87
|
+
expect.any(String),
|
|
88
|
+
[{ width: '40%' }, { width: '80%' }],
|
|
89
|
+
expect.objectContaining({ fill: 'forwards' }),
|
|
90
|
+
]);
|
|
53
91
|
});
|
|
54
92
|
|
|
55
|
-
it('
|
|
56
|
-
const {
|
|
57
|
-
|
|
58
|
-
|
|
93
|
+
it('slides a 40% fill across the track while indeterminate', async () => {
|
|
94
|
+
const { host, fill } = await renderProgress();
|
|
95
|
+
host.value.set(60);
|
|
96
|
+
await waitForUpdate();
|
|
97
|
+
animate.mockClear();
|
|
98
|
+
|
|
99
|
+
host.indeterminate.set(true);
|
|
59
100
|
await waitForUpdate();
|
|
60
|
-
|
|
61
|
-
expect(fill
|
|
101
|
+
expect(fill().getAttribute('style')).toContain('width: 40%');
|
|
102
|
+
expect(animate).toHaveBeenCalledWith(fill(), [
|
|
103
|
+
START,
|
|
104
|
+
expect.any(String),
|
|
105
|
+
[
|
|
106
|
+
{ transform: 'translateX(-100%)', width: '40%' },
|
|
107
|
+
{ transform: 'translateX(250%)', width: '40%' },
|
|
108
|
+
],
|
|
109
|
+
expect.objectContaining({ iterations: Infinity }),
|
|
110
|
+
]);
|
|
62
111
|
});
|
|
63
112
|
|
|
64
|
-
it(
|
|
65
|
-
const {
|
|
66
|
-
|
|
113
|
+
it("returns to the value without animating when it hasn't changed", async () => {
|
|
114
|
+
const { host, fill } = await renderProgress();
|
|
115
|
+
host.indeterminate.set(true);
|
|
116
|
+
await waitForUpdate();
|
|
117
|
+
animate.mockClear();
|
|
118
|
+
|
|
119
|
+
host.indeterminate.set(false);
|
|
67
120
|
await waitForUpdate();
|
|
68
|
-
|
|
69
|
-
|
|
121
|
+
expect(fill().getAttribute('style')).toContain('width: 40%');
|
|
122
|
+
// Only the sliding animation is canceled; nothing new starts.
|
|
123
|
+
expect(
|
|
124
|
+
animate.mock.calls.map(([, [operation]]) => operation),
|
|
125
|
+
).not.toContain(START);
|
|
70
126
|
});
|
|
71
127
|
});
|
|
@@ -32,65 +32,63 @@ export class UiProgress {
|
|
|
32
32
|
readonly indeterminate = input(false);
|
|
33
33
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
34
34
|
|
|
35
|
-
readonly fillRef = viewChild<ElementRef>('fill');
|
|
35
|
+
readonly fillRef = viewChild.required<ElementRef>('fill');
|
|
36
36
|
#fillAnim?: AnimationHandle;
|
|
37
37
|
#indeterminateAnim?: AnimationHandle;
|
|
38
38
|
#previousPercent: number | null = null;
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// Handle indeterminate mode with infinite sliding animation.
|
|
50
|
-
// The fill element is 40% of the track width. `translateX` percentages
|
|
51
|
-
// are relative to the element's OWN width, so:
|
|
52
|
-
// translateX(-100%) = left edge at -40% of track = starts off-screen left
|
|
53
|
-
// translateX(250%) = left edge at +100% of track = exits off-screen right
|
|
54
|
-
// 250% is the minimum that pushes the 40%-wide fill fully past the right edge.
|
|
55
|
-
if (this.indeterminate()) {
|
|
56
|
-
this.#fillAnim?.cancel();
|
|
57
|
-
this.#indeterminateAnim?.cancel();
|
|
58
|
-
this.#indeterminateAnim = el.animate(
|
|
59
|
-
[
|
|
60
|
-
{ transform: 'translateX(-100%)', width: '40%' },
|
|
61
|
-
{ transform: 'translateX(250%)', width: '40%' },
|
|
62
|
-
],
|
|
63
|
-
{
|
|
64
|
-
duration: 1200,
|
|
65
|
-
easing: EASING.standard,
|
|
66
|
-
iterations: Infinity,
|
|
67
|
-
},
|
|
68
|
-
);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Determinate mode — animate width changes smoothly.
|
|
73
|
-
// `#previousPercent === null` skips the very first effect run so
|
|
74
|
-
// the bar doesn't animate from 0% → initial value on mount.
|
|
75
|
-
// The equality guard prevents redundant animations if the effect
|
|
76
|
-
// re-runs without an actual percent change.
|
|
77
|
-
this.#indeterminateAnim?.cancel();
|
|
78
|
-
const percent = this.percent();
|
|
79
|
-
if (this.#previousPercent === null) {
|
|
80
|
-
this.#previousPercent = percent;
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
if (percent === this.#previousPercent) return;
|
|
40
|
+
/**
|
|
41
|
+
* Slides the fill while indeterminate, and animates its width as the value
|
|
42
|
+
* changes. A field rather than a constructor: Angular's test build keeps a
|
|
43
|
+
* function for a constructor that nothing calls, which an app's coverage
|
|
44
|
+
* report would show as uncovered.
|
|
45
|
+
*/
|
|
46
|
+
protected readonly fillAnimation = effect(() => {
|
|
47
|
+
const el = this.fillRef().nativeElement;
|
|
84
48
|
|
|
49
|
+
// Handle indeterminate mode with infinite sliding animation.
|
|
50
|
+
// The fill element is 40% of the track width. `translateX` percentages
|
|
51
|
+
// are relative to the element's OWN width, so:
|
|
52
|
+
// translateX(-100%) = left edge at -40% of track = starts off-screen left
|
|
53
|
+
// translateX(250%) = left edge at +100% of track = exits off-screen right
|
|
54
|
+
// 250% is the minimum that pushes the 40%-wide fill fully past the right edge.
|
|
55
|
+
if (this.indeterminate()) {
|
|
85
56
|
this.#fillAnim?.cancel();
|
|
86
|
-
this.#
|
|
87
|
-
|
|
88
|
-
|
|
57
|
+
this.#indeterminateAnim?.cancel();
|
|
58
|
+
this.#indeterminateAnim = el.animate(
|
|
59
|
+
[
|
|
60
|
+
{ transform: 'translateX(-100%)', width: '40%' },
|
|
61
|
+
{ transform: 'translateX(250%)', width: '40%' },
|
|
62
|
+
],
|
|
63
|
+
{
|
|
64
|
+
duration: 1200,
|
|
65
|
+
easing: EASING.standard,
|
|
66
|
+
iterations: Infinity,
|
|
67
|
+
},
|
|
89
68
|
);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Determinate mode — animate width changes smoothly.
|
|
73
|
+
// `#previousPercent === null` skips the very first effect run so
|
|
74
|
+
// the bar doesn't animate from 0% → initial value on mount.
|
|
75
|
+
// The equality guard skips the animation when the effect re-runs without
|
|
76
|
+
// a percent change: leaving indeterminate mode at the same value.
|
|
77
|
+
this.#indeterminateAnim?.cancel();
|
|
78
|
+
const percent = this.percent();
|
|
79
|
+
if (this.#previousPercent === null) {
|
|
90
80
|
this.#previousPercent = percent;
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (percent === this.#previousPercent) return;
|
|
84
|
+
|
|
85
|
+
this.#fillAnim?.cancel();
|
|
86
|
+
this.#fillAnim = el.animate(
|
|
87
|
+
[{ width: `${this.#previousPercent}%` }, { width: `${percent}%` }],
|
|
88
|
+
{ duration: DURATION.slow, easing: EASING.standard, fill: 'forwards' },
|
|
89
|
+
);
|
|
90
|
+
this.#previousPercent = percent;
|
|
91
|
+
});
|
|
94
92
|
|
|
95
93
|
protected readonly percent = computed(() => {
|
|
96
94
|
const m = this.max();
|
|
@@ -100,9 +98,9 @@ export class UiProgress {
|
|
|
100
98
|
|
|
101
99
|
protected readonly trackClass = computed(() =>
|
|
102
100
|
cn(
|
|
103
|
-
// bg-muted (solid, theme-aware) instead of bg-primary/20:
|
|
104
|
-
//
|
|
105
|
-
//
|
|
101
|
+
// bg-muted (solid, theme-aware) instead of bg-primary/20: a modifier
|
|
102
|
+
// needs the theme's --primary-rgb, and in a theme copied before it
|
|
103
|
+
// existed the track would disappear.
|
|
106
104
|
'h-2 w-full rounded-full bg-muted overflow-hidden',
|
|
107
105
|
this.userClass(),
|
|
108
106
|
),
|
|
@@ -1,115 +1,160 @@
|
|
|
1
1
|
import { Component, signal } from '@angular/core';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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';
|
|
6
20
|
import { UiRadioGroup, UiRadioGroupItem } from './radio-group';
|
|
7
21
|
|
|
8
22
|
@Component({
|
|
9
|
-
|
|
10
|
-
imports: [UiRadioGroup, UiRadioGroupItem, LYNX_ELEMENTS],
|
|
23
|
+
imports: [UiRadioGroup, UiRadioGroupItem],
|
|
11
24
|
template: `
|
|
12
|
-
<ui-radio-group
|
|
13
|
-
|
|
14
|
-
|
|
25
|
+
<ui-radio-group
|
|
26
|
+
[(value)]="value"
|
|
27
|
+
[disabled]="disabled()"
|
|
28
|
+
class="mt-2"
|
|
29
|
+
(changed)="changes.push($event)"
|
|
30
|
+
>
|
|
31
|
+
<ui-radio-group-item value="a" class="pl-1">Apple</ui-radio-group-item>
|
|
32
|
+
<ui-radio-group-item value="b">Banana</ui-radio-group-item>
|
|
15
33
|
</ui-radio-group>
|
|
16
34
|
`,
|
|
17
35
|
})
|
|
18
36
|
class RadioGroupHost {
|
|
19
|
-
value = 'a';
|
|
37
|
+
readonly value = signal('a');
|
|
38
|
+
readonly disabled = signal(false);
|
|
39
|
+
readonly changes: string[] = [];
|
|
20
40
|
}
|
|
21
41
|
|
|
42
|
+
const renderGroup = async () => {
|
|
43
|
+
const result = await render(RadioGroupHost);
|
|
44
|
+
/**
|
|
45
|
+
* An item's row, which takes the tap: its label's parent.
|
|
46
|
+
*/
|
|
47
|
+
const item = (label: string) => result.getByText(label).parentElement!;
|
|
48
|
+
/**
|
|
49
|
+
* An item's dot, inside the circle that's the row's first child.
|
|
50
|
+
*/
|
|
51
|
+
const dot = (label: string) =>
|
|
52
|
+
item(label).firstElementChild!.firstElementChild!;
|
|
53
|
+
return {
|
|
54
|
+
...result,
|
|
55
|
+
host: result.componentRef.instance as RadioGroupHost,
|
|
56
|
+
item,
|
|
57
|
+
dot,
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** `__ElementAnimate`'s first argument for starting an animation. */
|
|
62
|
+
const START = 0;
|
|
63
|
+
/** `__ElementAnimate`'s first argument for canceling one. */
|
|
64
|
+
const CANCEL = 3;
|
|
65
|
+
|
|
22
66
|
describe('UiRadioGroup', () => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
67
|
+
// Watched per test: the testing library resets the main thread after each.
|
|
68
|
+
let animate: MockInstance<LynxMainThread['__ElementAnimate']>;
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
animate = vi.spyOn(lynxMainThread(), '__ElementAnimate');
|
|
27
71
|
});
|
|
72
|
+
afterEach(() => animate.mockRestore());
|
|
28
73
|
|
|
29
|
-
it('
|
|
30
|
-
const {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
inst.select('a');
|
|
35
|
-
expect(inst.value()).toBe('a');
|
|
36
|
-
inst.select('b');
|
|
37
|
-
expect(inst.value()).toBe('b');
|
|
38
|
-
expect(emitted).toEqual(['a', 'b']);
|
|
74
|
+
it('renders each item with its label and class', async () => {
|
|
75
|
+
const { container, item } = await renderGroup();
|
|
76
|
+
expect(container.querySelector('.flex-col')?.classList).toContain('mt-2');
|
|
77
|
+
expect(item('Apple').classList).toContain('pl-1');
|
|
78
|
+
expect(item('Banana').classList).toContain('h-11');
|
|
39
79
|
});
|
|
40
80
|
|
|
41
|
-
it('
|
|
42
|
-
const {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const emitted = vi.fn();
|
|
46
|
-
inst.changed.subscribe(emitted);
|
|
47
|
-
inst.select('x');
|
|
48
|
-
expect(inst.value()).toBe('');
|
|
49
|
-
expect(emitted).not.toHaveBeenCalled();
|
|
81
|
+
it('shows the dot of the selected item only', async () => {
|
|
82
|
+
const { dot } = await renderGroup();
|
|
83
|
+
expect(dot('Apple').getAttribute('style')).toContain('opacity: 1');
|
|
84
|
+
expect(dot('Banana').getAttribute('style')).toContain('opacity: 0');
|
|
50
85
|
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe('UiRadioGroupItem', () => {
|
|
54
|
-
it('select() delegates to the parent group', async () => {
|
|
55
|
-
const parentResult = await render(UiRadioGroup);
|
|
56
|
-
const parent = parentResult.componentRef.instance as UiRadioGroup;
|
|
57
86
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const item = itemResult.componentRef.instance as UiRadioGroupItem;
|
|
62
|
-
setInputSignal(item.itemValue, 'a');
|
|
87
|
+
it('selects the tapped item, updating the value and emitting `changed`', async () => {
|
|
88
|
+
const { host, item, dot } = await renderGroup();
|
|
89
|
+
fireEvent.tap(item('Banana'));
|
|
63
90
|
await waitForUpdate();
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
expect(
|
|
91
|
+
expect(host.value()).toBe('b');
|
|
92
|
+
expect(host.changes).toEqual(['b']);
|
|
93
|
+
expect(dot('Banana').getAttribute('style')).toContain('opacity: 1');
|
|
94
|
+
expect(dot('Apple').getAttribute('style')).toContain('opacity: 0');
|
|
67
95
|
});
|
|
68
96
|
|
|
69
|
-
it('
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
97
|
+
it("doesn't animate the dot on mount, then pops it in and out as the selection changes", async () => {
|
|
98
|
+
const { host, dot } = await renderGroup();
|
|
99
|
+
// A pre-selected item shows its dot without animating it in.
|
|
100
|
+
expect(animate).not.toHaveBeenCalled();
|
|
73
101
|
|
|
74
|
-
|
|
75
|
-
providers: [{ provide: UiRadioGroup, useValue: parent }],
|
|
76
|
-
});
|
|
77
|
-
const item = itemResult.componentRef.instance as UiRadioGroupItem;
|
|
78
|
-
setInputSignal(item.itemValue, 'a');
|
|
102
|
+
host.value.set('b');
|
|
79
103
|
await waitForUpdate();
|
|
104
|
+
// The new selection's dot pops in from a smaller scale...
|
|
105
|
+
expect(animate).toHaveBeenCalledWith(dot('Banana'), [
|
|
106
|
+
START,
|
|
107
|
+
expect.any(String),
|
|
108
|
+
[
|
|
109
|
+
{ transform: 'scale(0.6)', opacity: 0 },
|
|
110
|
+
{ transform: 'scale(1)', opacity: 1 },
|
|
111
|
+
],
|
|
112
|
+
expect.objectContaining({ fill: 'forwards' }),
|
|
113
|
+
]);
|
|
114
|
+
// ...and the old one pops out.
|
|
115
|
+
expect(animate).toHaveBeenCalledWith(dot('Apple'), [
|
|
116
|
+
START,
|
|
117
|
+
expect.any(String),
|
|
118
|
+
[
|
|
119
|
+
{ transform: 'scale(1)', opacity: 1 },
|
|
120
|
+
{ transform: 'scale(0.8)', opacity: 0 },
|
|
121
|
+
],
|
|
122
|
+
expect.objectContaining({ fill: 'forwards' }),
|
|
123
|
+
]);
|
|
80
124
|
|
|
81
|
-
|
|
82
|
-
|
|
125
|
+
// Selecting it again cuts its pop-out short.
|
|
126
|
+
host.value.set('a');
|
|
127
|
+
await waitForUpdate();
|
|
128
|
+
expect(animate).toHaveBeenCalledWith(dot('Apple'), [
|
|
129
|
+
CANCEL,
|
|
130
|
+
expect.any(String),
|
|
131
|
+
]);
|
|
83
132
|
});
|
|
84
133
|
|
|
85
|
-
it('
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// parent's value across two mounted items.
|
|
89
|
-
// JIT + jsdom does not populate the `#dot` viewChild query, so
|
|
90
|
-
// monkey-patch dotRef with a signal returning a fake ElementRef so the
|
|
91
|
-
// `if (!el || previousSelected === undefined)` guard doesn't short-circuit.
|
|
92
|
-
const parentResult = await render(UiRadioGroup);
|
|
93
|
-
const parent = parentResult.componentRef.instance as UiRadioGroup;
|
|
94
|
-
|
|
95
|
-
const itemResult = await render(UiRadioGroupItem, {
|
|
96
|
-
providers: [{ provide: UiRadioGroup, useValue: parent }],
|
|
97
|
-
});
|
|
98
|
-
const item = itemResult.componentRef.instance as UiRadioGroupItem;
|
|
99
|
-
setInputSignal(item.itemValue, 'a');
|
|
100
|
-
const view = itemResult.container.querySelector('view') as Element;
|
|
101
|
-
(item as unknown as { dotRef: unknown }).dotRef = signal({
|
|
102
|
-
nativeElement: view,
|
|
103
|
-
});
|
|
134
|
+
it('ignores taps while disabled, and dims its items', async () => {
|
|
135
|
+
const { host, item } = await renderGroup();
|
|
136
|
+
host.disabled.set(true);
|
|
104
137
|
await waitForUpdate();
|
|
138
|
+
expect(item('Apple').classList).toContain('opacity-50');
|
|
105
139
|
|
|
106
|
-
|
|
107
|
-
// Select => popIn branch.
|
|
108
|
-
parent.value.set('a');
|
|
140
|
+
fireEvent.tap(item('Banana'));
|
|
109
141
|
await waitForUpdate();
|
|
110
|
-
|
|
111
|
-
|
|
142
|
+
expect(host.value()).toBe('a');
|
|
143
|
+
expect(host.changes).toEqual([]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("can't be selected through its own select() while disabled", async () => {
|
|
147
|
+
// An item checks the group first, but select() is public: a caller that
|
|
148
|
+
// goes straight to the group gets the same rule.
|
|
149
|
+
const { componentRef } = await render(UiRadioGroup);
|
|
150
|
+
componentRef.setInput('disabled', true);
|
|
112
151
|
await waitForUpdate();
|
|
113
|
-
|
|
152
|
+
const group = componentRef.instance as UiRadioGroup;
|
|
153
|
+
const changed = vi.fn();
|
|
154
|
+
group.changed.subscribe(changed);
|
|
155
|
+
|
|
156
|
+
group.select('b');
|
|
157
|
+
expect(group.value()).toBe('');
|
|
158
|
+
expect(changed).not.toHaveBeenCalled();
|
|
114
159
|
});
|
|
115
160
|
});
|
|
@@ -70,36 +70,36 @@ export class UiRadioGroupItem {
|
|
|
70
70
|
readonly itemValue = input.required<string>({ alias: 'value' });
|
|
71
71
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
72
72
|
|
|
73
|
-
readonly dotRef = viewChild<ElementRef>('dot');
|
|
73
|
+
readonly dotRef = viewChild.required<ElementRef>('dot');
|
|
74
74
|
#dotAnim?: AnimationHandle;
|
|
75
|
-
#
|
|
75
|
+
#mounted = false;
|
|
76
76
|
|
|
77
77
|
protected readonly isSelected = computed(
|
|
78
78
|
() => this.#group.value() === this.itemValue(),
|
|
79
79
|
);
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
81
|
+
/**
|
|
82
|
+
* Pops the dot in when the item is selected and out when it's deselected.
|
|
83
|
+
* A field rather than a constructor: Angular's test build keeps a function
|
|
84
|
+
* for a constructor that nothing calls, which an app's coverage report
|
|
85
|
+
* would show as uncovered.
|
|
86
|
+
*
|
|
87
|
+
* The first run is the mount, skipped so a pre-selected item doesn't
|
|
88
|
+
* animate in. Every later run is a change of selection: `isSelected` is a
|
|
89
|
+
* computed, which only notifies when its value changes, and the dot is
|
|
90
|
+
* always in the template, so its query never changes.
|
|
91
|
+
*/
|
|
92
|
+
protected readonly dotAnimation = effect(() => {
|
|
93
|
+
const selected = this.isSelected();
|
|
94
|
+
const el = this.dotRef().nativeElement;
|
|
95
|
+
if (!this.#mounted) {
|
|
96
|
+
this.#mounted = true;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.#dotAnim?.cancel();
|
|
101
|
+
this.#dotAnim = selected ? popIn(el) : popOut(el);
|
|
102
|
+
});
|
|
103
103
|
|
|
104
104
|
protected readonly itemClass = computed(() =>
|
|
105
105
|
cn(
|