@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,3 +1,9 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import {
|
|
3
|
+
fireEvent,
|
|
4
|
+
render,
|
|
5
|
+
waitForUpdate,
|
|
6
|
+
} from '@blotch/angular-lynx-testing-library';
|
|
1
7
|
import { createChartRuntime } from '@tanstack/charts/runtime';
|
|
2
8
|
import type {
|
|
3
9
|
ChartDefinition,
|
|
@@ -5,7 +11,8 @@ import type {
|
|
|
5
11
|
SceneNode,
|
|
6
12
|
} from '@tanstack/charts/types';
|
|
7
13
|
import { describe, expect, it } from 'vitest';
|
|
8
|
-
import {
|
|
14
|
+
import type { XYDatum } from '../chart/chart-presets';
|
|
15
|
+
import { UiScatterChart, scatterChartDefinition } from './scatter-chart';
|
|
9
16
|
|
|
10
17
|
const data = [
|
|
11
18
|
{ x: 150, y: 50 },
|
|
@@ -65,3 +72,69 @@ describe('scatterChartDefinition', () => {
|
|
|
65
72
|
expect(scene.scales['x']!.domain).toEqual([150, 170]);
|
|
66
73
|
});
|
|
67
74
|
});
|
|
75
|
+
|
|
76
|
+
@Component({
|
|
77
|
+
imports: [UiScatterChart],
|
|
78
|
+
template: `
|
|
79
|
+
<ui-scatter-chart
|
|
80
|
+
[data]="data"
|
|
81
|
+
[width]="300"
|
|
82
|
+
[radius]="radius()"
|
|
83
|
+
(pointTap)="tapped.push($event)"
|
|
84
|
+
/>
|
|
85
|
+
`,
|
|
86
|
+
})
|
|
87
|
+
class ScatterChartHost {
|
|
88
|
+
readonly data = data;
|
|
89
|
+
readonly radius = signal(7);
|
|
90
|
+
/** Every `pointTap`, in order. */
|
|
91
|
+
readonly tapped: XYDatum[] = [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The SVG the chart's marks layer draws.
|
|
96
|
+
*/
|
|
97
|
+
const marksSvg = (container: Element) =>
|
|
98
|
+
container.querySelectorAll('svg')[1]!.getAttribute('content')!;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Where the chart drew each dot, and how big.
|
|
102
|
+
*/
|
|
103
|
+
const dotsDrawn = (container: Element) =>
|
|
104
|
+
[
|
|
105
|
+
...marksSvg(container).matchAll(
|
|
106
|
+
/<circle cx="([\d.-]+)" cy="([\d.-]+)" r="([\d.-]+)"/g,
|
|
107
|
+
),
|
|
108
|
+
].map(([, x, y, r]) => ({ x: Number(x), y: Number(y), r: Number(r) }));
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Touches a spot of the chart and lifts the finger right there: a tap.
|
|
112
|
+
*/
|
|
113
|
+
const tap = async (container: Element, at: { x: number; y: number }) => {
|
|
114
|
+
const root = container.querySelector('[accessibility-label]')!;
|
|
115
|
+
fireEvent.touchstart(root, { touches: [at] });
|
|
116
|
+
fireEvent.touchend(root, { touches: [at] });
|
|
117
|
+
await waitForUpdate();
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
describe('UiScatterChart', () => {
|
|
121
|
+
it('draws a dot per datum with its inputs', async () => {
|
|
122
|
+
const { componentRef, container } = await render(ScatterChartHost);
|
|
123
|
+
expect(dotsDrawn(container).map((dot) => dot.r)).toEqual([7, 7, 7]);
|
|
124
|
+
|
|
125
|
+
(componentRef.instance as ScatterChartHost).radius.set(3);
|
|
126
|
+
await waitForUpdate();
|
|
127
|
+
expect(dotsDrawn(container).map((dot) => dot.r)).toEqual([3, 3, 3]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("emits a tapped point's datum, and nothing for a tap off the points", async () => {
|
|
131
|
+
const { componentRef, container } = await render(ScatterChartHost);
|
|
132
|
+
const host = componentRef.instance as ScatterChartHost;
|
|
133
|
+
const last = dotsDrawn(container)[2]!;
|
|
134
|
+
await tap(container, { x: last.x, y: last.y });
|
|
135
|
+
expect(host.tapped).toEqual([data[2]]);
|
|
136
|
+
|
|
137
|
+
await tap(container, { x: -100, y: -100 });
|
|
138
|
+
expect(host.tapped).toEqual([data[2]]);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -1,75 +1,111 @@
|
|
|
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
8
|
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
-
import {
|
|
9
|
+
import { lynxMainThread } from '../../../test-utils/lynx-main-thread';
|
|
6
10
|
import { UiScrollArea } from './scroll-area';
|
|
7
11
|
|
|
8
12
|
@Component({
|
|
9
13
|
imports: [UiScrollArea, LYNX_ELEMENTS],
|
|
10
|
-
template:
|
|
14
|
+
template: `
|
|
15
|
+
<ui-scroll-area
|
|
16
|
+
[orientation]="orientation()"
|
|
17
|
+
[height]="height()"
|
|
18
|
+
[bounces]="bounces()"
|
|
19
|
+
[scrollBar]="scrollBar()"
|
|
20
|
+
[nestedScroll]="nestedScroll()"
|
|
21
|
+
class="bg-card"
|
|
22
|
+
contentClass="gap-2"
|
|
23
|
+
(scrolledToStart)="reached.push('start')"
|
|
24
|
+
(scrolledToEnd)="reached.push('end')"
|
|
25
|
+
>
|
|
26
|
+
<text>Body</text>
|
|
27
|
+
</ui-scroll-area>
|
|
28
|
+
`,
|
|
11
29
|
})
|
|
12
|
-
class ScrollAreaHost {
|
|
30
|
+
class ScrollAreaHost {
|
|
31
|
+
readonly orientation = signal<'vertical' | 'horizontal'>('vertical');
|
|
32
|
+
readonly height = signal('');
|
|
33
|
+
readonly bounces = signal(true);
|
|
34
|
+
readonly scrollBar = signal(true);
|
|
35
|
+
readonly nestedScroll = signal(false);
|
|
36
|
+
readonly reached: string[] = [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const renderScrollArea = async () => {
|
|
40
|
+
const result = await render(ScrollAreaHost);
|
|
41
|
+
return {
|
|
42
|
+
...result,
|
|
43
|
+
host: result.componentRef.instance as ScrollAreaHost,
|
|
44
|
+
scrollView: () => result.container.querySelector('scroll-view')!,
|
|
45
|
+
/** The view inside the scroll view that lays out the content. */
|
|
46
|
+
content: () => result.getByText('Body').parentElement!,
|
|
47
|
+
};
|
|
48
|
+
};
|
|
13
49
|
|
|
14
50
|
/**
|
|
15
51
|
* The values each boolean reached Lynx with, through `__SetAttribute`.
|
|
16
52
|
*/
|
|
17
53
|
const watchAttributes = () => {
|
|
18
|
-
const
|
|
19
|
-
globalThis as unknown as {
|
|
20
|
-
lynxTestingEnv: { mainThread: { globalThis: Record<string, unknown> } };
|
|
21
|
-
}
|
|
22
|
-
).lynxTestingEnv.mainThread.globalThis;
|
|
23
|
-
const setAttribute = vi.spyOn(mainThread, '__SetAttribute' as never);
|
|
54
|
+
const setAttribute = vi.spyOn(lynxMainThread(), '__SetAttribute');
|
|
24
55
|
return {
|
|
25
56
|
last: (name: string): unknown =>
|
|
26
|
-
|
|
27
|
-
.filter((call) => call[1] === name)
|
|
28
|
-
.at(-1)?.[2],
|
|
57
|
+
setAttribute.mock.calls.filter((call) => call[1] === name).at(-1)?.[2],
|
|
29
58
|
restore: () => setAttribute.mockRestore(),
|
|
30
59
|
};
|
|
31
60
|
};
|
|
32
61
|
|
|
33
62
|
describe('UiScrollArea', () => {
|
|
34
|
-
it('renders projected content in
|
|
35
|
-
const {
|
|
36
|
-
expect(
|
|
63
|
+
it('renders projected content in a vertical scroll view', async () => {
|
|
64
|
+
const { scrollView, content } = await renderScrollArea();
|
|
65
|
+
expect(scrollView().getAttribute('scroll-orientation')).toBe('vertical');
|
|
66
|
+
expect(scrollView().classList).toContain('bg-card');
|
|
67
|
+
expect(content().classList).toContain('flex-col');
|
|
68
|
+
expect(content().classList).toContain('gap-2');
|
|
37
69
|
});
|
|
38
70
|
|
|
39
|
-
it('
|
|
40
|
-
const {
|
|
41
|
-
|
|
71
|
+
it('gives the scroll view an explicit height, which it needs to scroll', async () => {
|
|
72
|
+
const { host, scrollView } = await renderScrollArea();
|
|
73
|
+
expect(scrollView().getAttribute('style') ?? '').not.toContain('height');
|
|
74
|
+
|
|
75
|
+
host.height.set('250px');
|
|
42
76
|
await waitForUpdate();
|
|
43
|
-
|
|
44
|
-
expect(scroll?.getAttribute('style')).toContain('height: 250px');
|
|
77
|
+
expect(scrollView().getAttribute('style')).toContain('height: 250px');
|
|
45
78
|
});
|
|
46
79
|
|
|
47
|
-
it('
|
|
48
|
-
const {
|
|
49
|
-
|
|
50
|
-
(componentRef.instance as UiScrollArea).orientation,
|
|
51
|
-
'horizontal',
|
|
52
|
-
);
|
|
80
|
+
it('lays the content out in a row when horizontal', async () => {
|
|
81
|
+
const { host, scrollView, content } = await renderScrollArea();
|
|
82
|
+
host.orientation.set('horizontal');
|
|
53
83
|
await waitForUpdate();
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
expect(rowEl).toBeTruthy();
|
|
84
|
+
expect(scrollView().getAttribute('scroll-orientation')).toBe('horizontal');
|
|
85
|
+
expect(content().classList).toContain('flex-row');
|
|
57
86
|
});
|
|
58
87
|
|
|
59
88
|
it('hands Lynx real booleans, so the defaults that are on can be turned off', async () => {
|
|
60
89
|
// A false used to remove the attribute, which leaves Lynx's default: on.
|
|
61
90
|
const attributes = watchAttributes();
|
|
62
|
-
const {
|
|
63
|
-
const inst = componentRef.instance as UiScrollArea;
|
|
91
|
+
const { host } = await renderScrollArea();
|
|
64
92
|
expect(attributes.last('bounces')).toBe(true);
|
|
65
93
|
expect(attributes.last('scroll-bar-enable')).toBe(true);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
94
|
+
|
|
95
|
+
host.bounces.set(false);
|
|
96
|
+
host.scrollBar.set(false);
|
|
97
|
+
host.nestedScroll.set(true);
|
|
69
98
|
await waitForUpdate();
|
|
70
99
|
expect(attributes.last('bounces')).toBe(false);
|
|
71
100
|
expect(attributes.last('scroll-bar-enable')).toBe(false);
|
|
72
101
|
expect(attributes.last('enable-nested-scroll')).toBe(true);
|
|
73
102
|
attributes.restore();
|
|
74
103
|
});
|
|
104
|
+
|
|
105
|
+
it('emits when scrolled to the start or the end', async () => {
|
|
106
|
+
const { host, scrollView } = await renderScrollArea();
|
|
107
|
+
fireEvent.scrolltolower(scrollView());
|
|
108
|
+
fireEvent.scrolltoupper(scrollView());
|
|
109
|
+
expect(host.reached).toEqual(['end', 'start']);
|
|
110
|
+
});
|
|
75
111
|
});
|
|
@@ -1,107 +1,167 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import {
|
|
3
|
+
fireEvent,
|
|
4
|
+
render,
|
|
5
|
+
waitForUpdate,
|
|
6
|
+
within,
|
|
7
|
+
} from '@blotch/angular-lynx-testing-library';
|
|
8
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
9
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
10
|
import { UiSelect, UiSelectItem } from './select';
|
|
5
11
|
|
|
12
|
+
@Component({
|
|
13
|
+
imports: [UiSelect, UiSelectItem, LYNX_ELEMENTS],
|
|
14
|
+
template: `
|
|
15
|
+
<ui-select
|
|
16
|
+
#select
|
|
17
|
+
[(value)]="value"
|
|
18
|
+
[placeholder]="placeholder()"
|
|
19
|
+
[disabled]="disabled()"
|
|
20
|
+
class="w-48"
|
|
21
|
+
(changed)="changed($event)"
|
|
22
|
+
>
|
|
23
|
+
<ui-select-item value="apple" label="Apple" />
|
|
24
|
+
<ui-select-item value="pear" label="Pear" class="font-bold" />
|
|
25
|
+
</ui-select>
|
|
26
|
+
<text class="open-from-code" (bindtap)="select.open()">Choose</text>
|
|
27
|
+
`,
|
|
28
|
+
})
|
|
29
|
+
class SelectHost {
|
|
30
|
+
readonly value = signal('');
|
|
31
|
+
readonly placeholder = signal('Pick a fruit');
|
|
32
|
+
readonly disabled = signal(false);
|
|
33
|
+
readonly changed = vi.fn((_value: string) => {});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const renderSelect = async () => {
|
|
37
|
+
const result = await render(SelectHost);
|
|
38
|
+
const { container } = result;
|
|
39
|
+
const overlay = container.querySelector<HTMLElement>('overlay')!;
|
|
40
|
+
return {
|
|
41
|
+
...result,
|
|
42
|
+
host: result.componentRef.instance as SelectHost,
|
|
43
|
+
overlay,
|
|
44
|
+
trigger: container.querySelector('.border-input')!,
|
|
45
|
+
triggerText: () => container.querySelector('.border-input text')!,
|
|
46
|
+
// An item's tappable row, found by its label in the sheet.
|
|
47
|
+
item: (label: string) => within(overlay).getByText(label).parentElement!,
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Waits out the sheet's open sequence: the overlay shows on one tick, and the
|
|
53
|
+
* panel slides in on the next.
|
|
54
|
+
*/
|
|
55
|
+
const waitForOpen = async () => {
|
|
56
|
+
for (let i = 0; i < 3; i++) await waitForUpdate();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Waits for the sheet's close sequence to finish: the panel slides out, then
|
|
61
|
+
* the overlay hides. Polled rather than timed, since the sequence only starts
|
|
62
|
+
* on the update after the sheet is told to close.
|
|
63
|
+
*/
|
|
64
|
+
const waitForClose = (overlay: Element) =>
|
|
65
|
+
vi.waitFor(() => expect(overlay.getAttribute('visible')).toBe('false'));
|
|
66
|
+
|
|
6
67
|
describe('UiSelect', () => {
|
|
7
|
-
it('
|
|
8
|
-
const {
|
|
9
|
-
expect(
|
|
68
|
+
it('shows the placeholder, muted, until a value is chosen', async () => {
|
|
69
|
+
const { triggerText, trigger } = await renderSelect();
|
|
70
|
+
expect(triggerText().textContent).toBe('Pick a fruit');
|
|
71
|
+
expect(triggerText().classList).toContain('text-muted-foreground');
|
|
72
|
+
expect(trigger.classList).toContain('w-48');
|
|
10
73
|
});
|
|
11
74
|
|
|
12
|
-
it(
|
|
13
|
-
const {
|
|
14
|
-
|
|
15
|
-
setInputSignal(inst.placeholder, 'Choose...');
|
|
75
|
+
it("shows the chosen item's label", async () => {
|
|
76
|
+
const { host, triggerText } = await renderSelect();
|
|
77
|
+
host.value.set('pear');
|
|
16
78
|
await waitForUpdate();
|
|
17
|
-
expect(
|
|
79
|
+
expect(triggerText().textContent).toBe('Pear');
|
|
80
|
+
expect(triggerText().classList).toContain('text-foreground');
|
|
18
81
|
});
|
|
19
82
|
|
|
20
|
-
it('
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const inst = componentRef.instance as UiSelect;
|
|
24
|
-
inst.value.set('unknown');
|
|
83
|
+
it('shows a value that no item has as it is', async () => {
|
|
84
|
+
const { host, triggerText } = await renderSelect();
|
|
85
|
+
host.value.set('kiwi');
|
|
25
86
|
await waitForUpdate();
|
|
26
|
-
expect(
|
|
87
|
+
expect(triggerText().textContent).toBe('kiwi');
|
|
27
88
|
});
|
|
28
89
|
|
|
29
|
-
it('
|
|
30
|
-
const {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
90
|
+
it('opens its list when the trigger is tapped, and closes it from the sheet', async () => {
|
|
91
|
+
const { overlay, trigger } = await renderSelect();
|
|
92
|
+
expect(overlay.getAttribute('visible')).toBe('false');
|
|
93
|
+
|
|
94
|
+
fireEvent.tap(trigger);
|
|
95
|
+
await waitForOpen();
|
|
96
|
+
expect(overlay.getAttribute('visible')).toBe('true');
|
|
97
|
+
|
|
98
|
+
// Tapping the backdrop closes the sheet, which the select keeps track of:
|
|
99
|
+
// the next tap on the trigger opens it again.
|
|
100
|
+
fireEvent.tap(overlay.querySelector('view')!);
|
|
101
|
+
await waitForClose(overlay);
|
|
102
|
+
expect(overlay.getAttribute('visible')).toBe('false');
|
|
103
|
+
fireEvent.tap(trigger);
|
|
104
|
+
await waitForOpen();
|
|
105
|
+
expect(overlay.getAttribute('visible')).toBe('true');
|
|
38
106
|
});
|
|
39
107
|
|
|
40
|
-
it('
|
|
41
|
-
const {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
inst.open();
|
|
48
|
-
inst.close();
|
|
49
|
-
expect(inst.value()).toBe('');
|
|
108
|
+
it('closes its list when the trigger is tapped again', async () => {
|
|
109
|
+
const { overlay, trigger } = await renderSelect();
|
|
110
|
+
fireEvent.tap(trigger);
|
|
111
|
+
await waitForOpen();
|
|
112
|
+
fireEvent.tap(trigger);
|
|
113
|
+
await waitForClose(overlay);
|
|
114
|
+
expect(overlay.getAttribute('visible')).toBe('false');
|
|
50
115
|
});
|
|
51
116
|
|
|
52
|
-
it('
|
|
53
|
-
const {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// No throw and no exception paths — early-return branch is exercised.
|
|
58
|
-
inst.toggle();
|
|
59
|
-
inst.open();
|
|
60
|
-
inst.close();
|
|
61
|
-
expect(inst.value()).toBe('');
|
|
117
|
+
it('opens from code with open()', async () => {
|
|
118
|
+
const { overlay, getByText } = await renderSelect();
|
|
119
|
+
fireEvent.tap(getByText('Choose'));
|
|
120
|
+
await waitForOpen();
|
|
121
|
+
expect(overlay.getAttribute('visible')).toBe('true');
|
|
62
122
|
});
|
|
63
123
|
|
|
64
|
-
it(
|
|
65
|
-
const {
|
|
66
|
-
|
|
67
|
-
setInputSignal(inst.disabled, true);
|
|
124
|
+
it("dims and doesn't open while disabled", async () => {
|
|
125
|
+
const { host, overlay, trigger, getByText } = await renderSelect();
|
|
126
|
+
host.disabled.set(true);
|
|
68
127
|
await waitForUpdate();
|
|
69
|
-
|
|
70
|
-
|
|
128
|
+
expect(trigger.classList).toContain('opacity-50');
|
|
129
|
+
|
|
130
|
+
fireEvent.tap(trigger);
|
|
131
|
+
fireEvent.tap(getByText('Choose'));
|
|
132
|
+
await waitForOpen();
|
|
133
|
+
expect(overlay.getAttribute('visible')).toBe('false');
|
|
71
134
|
});
|
|
72
135
|
});
|
|
73
136
|
|
|
74
137
|
describe('UiSelectItem', () => {
|
|
75
|
-
it('
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const itemResult = await render(UiSelectItem, {
|
|
80
|
-
providers: [{ provide: UiSelect, useValue: parent }],
|
|
81
|
-
});
|
|
82
|
-
const item = itemResult.componentRef.instance as UiSelectItem;
|
|
83
|
-
setInputSignal(item.itemValue, 'x');
|
|
84
|
-
setInputSignal(item.label, 'X');
|
|
85
|
-
await waitForUpdate();
|
|
138
|
+
it('chooses its value when tapped, and closes the list', async () => {
|
|
139
|
+
const { host, overlay, trigger, triggerText, item } = await renderSelect();
|
|
140
|
+
fireEvent.tap(trigger);
|
|
141
|
+
await waitForOpen();
|
|
86
142
|
|
|
87
|
-
(item
|
|
88
|
-
expect(
|
|
143
|
+
fireEvent.tap(item('Apple'));
|
|
144
|
+
expect(host.value()).toBe('apple');
|
|
145
|
+
expect(host.changed).toHaveBeenCalledWith('apple');
|
|
146
|
+
await waitForUpdate();
|
|
147
|
+
expect(triggerText().textContent).toBe('Apple');
|
|
148
|
+
await waitForClose(overlay);
|
|
149
|
+
expect(overlay.getAttribute('visible')).toBe('false');
|
|
89
150
|
});
|
|
90
151
|
|
|
91
|
-
it('
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
parent.value.set('x');
|
|
95
|
-
|
|
96
|
-
const itemResult = await render(UiSelectItem, {
|
|
97
|
-
providers: [{ provide: UiSelect, useValue: parent }],
|
|
98
|
-
});
|
|
99
|
-
const item = itemResult.componentRef.instance as UiSelectItem;
|
|
100
|
-
setInputSignal(item.itemValue, 'x');
|
|
101
|
-
setInputSignal(item.label, 'X');
|
|
152
|
+
it('marks the chosen item with a highlight and a check', async () => {
|
|
153
|
+
const { host, item } = await renderSelect();
|
|
154
|
+
host.value.set('pear');
|
|
102
155
|
await waitForUpdate();
|
|
156
|
+
expect(item('Pear').classList).toContain('bg-accent');
|
|
157
|
+
expect(item('Pear').querySelector('svg')).not.toBeNull();
|
|
158
|
+
expect(item('Apple').classList).not.toContain('bg-accent');
|
|
159
|
+
expect(item('Apple').querySelector('svg')).toBeNull();
|
|
160
|
+
});
|
|
103
161
|
|
|
104
|
-
|
|
105
|
-
|
|
162
|
+
it("adds the class to the item's own", async () => {
|
|
163
|
+
const { item } = await renderSelect();
|
|
164
|
+
expect(item('Pear').classList).toContain('font-bold');
|
|
165
|
+
expect(item('Pear').classList).toContain('px-4');
|
|
106
166
|
});
|
|
107
167
|
});
|
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
ViewEncapsulation,
|
|
4
4
|
computed,
|
|
5
5
|
contentChildren,
|
|
6
|
-
forwardRef,
|
|
7
6
|
inject,
|
|
8
7
|
input,
|
|
9
8
|
model,
|
|
@@ -18,17 +17,28 @@ import { cn } from '../../utils/cn';
|
|
|
18
17
|
const CHEVRON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>`;
|
|
19
18
|
const CHECK_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>`;
|
|
20
19
|
|
|
20
|
+
/**
|
|
21
|
+
* `UiSelectItem`, for `contentChildren()`. A call rather than the class
|
|
22
|
+
* itself: for a class, Angular's test build keeps an arrow function that
|
|
23
|
+
* nothing calls, which an app's coverage report would show as uncovered.
|
|
24
|
+
*/
|
|
25
|
+
const selectItem = () => UiSelectItem;
|
|
26
|
+
|
|
21
27
|
@Component({
|
|
22
28
|
selector: 'ui-select',
|
|
23
29
|
standalone: true,
|
|
24
30
|
imports: [LYNX_ELEMENTS, UiBottomSheet],
|
|
25
31
|
encapsulation: ViewEncapsulation.None,
|
|
32
|
+
// `sheetOpen` is bound both ways by hand rather than with `[(open)]`: for a
|
|
33
|
+
// two-way binding Angular also compiles a fallback that assigns the field,
|
|
34
|
+
// which never runs for a signal and so shows as an uncovered branch in an
|
|
35
|
+
// app's coverage report.
|
|
26
36
|
template: `
|
|
27
37
|
<view [class]="triggerClass()" (bindtap)="toggle()">
|
|
28
38
|
<text [class]="valueTextClass()">{{ displayText() }}</text>
|
|
29
39
|
<svg [attr.content]="chevronSvg" style="width: 16px; height: 16px;" />
|
|
30
40
|
</view>
|
|
31
|
-
<ui-bottom-sheet [
|
|
41
|
+
<ui-bottom-sheet [open]="sheetOpen()" (openChange)="sheetOpen.set($event)">
|
|
32
42
|
<scroll-view scroll-orientation="vertical" style="max-height: 300px;">
|
|
33
43
|
<view class="flex-col pb-4 flex">
|
|
34
44
|
<ng-content />
|
|
@@ -47,21 +57,17 @@ export class UiSelect {
|
|
|
47
57
|
|
|
48
58
|
protected readonly chevronSvg = CHEVRON_SVG;
|
|
49
59
|
|
|
50
|
-
// Drives the shared bottom-sheet's `open` model
|
|
51
|
-
//
|
|
52
|
-
//
|
|
60
|
+
// Drives the shared bottom-sheet's `open` model. All the overlay/animation/
|
|
61
|
+
// drag behavior lives in ui-bottom-sheet; select only decides when the sheet
|
|
62
|
+
// is open.
|
|
53
63
|
protected readonly sheetOpen = signal(false);
|
|
54
64
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
// time and contentChildren would silently query nothing.
|
|
58
|
-
/* v8 ignore next 1 -- forwardRef lambda; JIT+jsdom doesn't wire required child inputs */
|
|
59
|
-
readonly itemRefs = contentChildren(forwardRef(() => UiSelectItem));
|
|
65
|
+
// The projected items, so the trigger can show the chosen one's label.
|
|
66
|
+
readonly itemRefs = contentChildren(selectItem());
|
|
60
67
|
|
|
61
68
|
protected readonly displayText = computed(() => {
|
|
62
69
|
const val = this.value();
|
|
63
70
|
if (!val) return this.placeholder();
|
|
64
|
-
/* v8 ignore next 1 -- .find predicate: unreachable when no content-projected items */
|
|
65
71
|
const item = this.itemRefs().find((i) => i.itemValue() === val);
|
|
66
72
|
return item?.label() ?? val;
|
|
67
73
|
});
|
|
@@ -1,26 +1,27 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
1
2
|
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
|
|
2
3
|
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { setInputSignal } from '../../../test-utils/set-input-signal';
|
|
4
4
|
import { UiSeparator } from './separator';
|
|
5
5
|
|
|
6
|
+
@Component({
|
|
7
|
+
imports: [UiSeparator],
|
|
8
|
+
template: `<ui-separator [orientation]="orientation()" class="my-2" />`,
|
|
9
|
+
})
|
|
10
|
+
class SeparatorHost {
|
|
11
|
+
readonly orientation = signal<'horizontal' | 'vertical'>('horizontal');
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
describe('UiSeparator', () => {
|
|
7
|
-
it('
|
|
8
|
-
const { componentRef } = await render(
|
|
9
|
-
// The
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
expect(host.getAttribute('class')).toContain('h-px');
|
|
14
|
-
});
|
|
15
|
+
it('draws a horizontal line by default, and a vertical one when asked', async () => {
|
|
16
|
+
const { componentRef, container } = await render(SeparatorHost);
|
|
17
|
+
// The classes are on the host element, the separator's only element.
|
|
18
|
+
const line = container.querySelector('.bg-border')!;
|
|
19
|
+
expect(line.classList).toContain('h-px');
|
|
20
|
+
expect(line.classList).toContain('my-2');
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
const { componentRef } = await render(UiSeparator);
|
|
18
|
-
setInputSignal(
|
|
19
|
-
(componentRef.instance as UiSeparator).orientation,
|
|
20
|
-
'vertical',
|
|
21
|
-
);
|
|
22
|
+
(componentRef.instance as SeparatorHost).orientation.set('vertical');
|
|
22
23
|
await waitForUpdate();
|
|
23
|
-
|
|
24
|
-
expect(
|
|
24
|
+
expect(line.classList).toContain('w-px');
|
|
25
|
+
expect(line.classList).not.toContain('h-px');
|
|
25
26
|
});
|
|
26
27
|
});
|