@blotch/dolan 22.2.3 → 22.2.5
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 +38 -3
- package/dist/commands/add.test.js.map +1 -1
- package/dist/commands/doctor.js +258 -21
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +425 -11
- 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 +105 -2
- 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 +7 -0
- package/dist/commands/update.js +194 -50
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/update.test.js +570 -3
- package/dist/commands/update.test.js.map +1 -1
- package/dist/lockfile.d.ts +17 -0
- package/dist/lockfile.js +56 -1
- package/dist/lockfile.js.map +1 -1
- package/dist/lockfile.test.d.ts +1 -0
- package/dist/lockfile.test.js +65 -0
- package/dist/lockfile.test.js.map +1 -0
- package/dist/registry.d.ts +6 -0
- package/dist/registry.js +119 -11
- package/dist/registry.js.map +1 -1
- package/dist/registry.test.js +61 -6
- package/dist/registry.test.js.map +1 -1
- package/dist/test-utils.d.ts +27 -0
- package/dist/test-utils.js +45 -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/boolean-inputs.test.ts +76 -0
- 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 +637 -183
- package/dist/ui/components/action-sheet/action-sheet.ts +339 -55
- package/dist/ui/components/action-sheet/index.ts +1 -0
- 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 +740 -129
- package/dist/ui/components/alert-dialog/alert-dialog.ts +672 -58
- package/dist/ui/components/alert-dialog/index.ts +5 -0
- package/dist/ui/components/area-chart/area-chart.test.ts +81 -1
- package/dist/ui/components/area-chart/area-chart.ts +3 -2
- 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 +14 -15
- package/dist/ui/components/bar-chart/bar-chart.test.ts +82 -1
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +1314 -273
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +1046 -119
- package/dist/ui/components/bottom-sheet/index.ts +7 -1
- package/dist/ui/components/button/button.test.ts +646 -2
- package/dist/ui/components/button/button.ts +443 -64
- package/dist/ui/components/button/index.ts +2 -0
- package/dist/ui/components/button-group/button-group.test.ts +528 -2
- package/dist/ui/components/button-group/button-group.ts +428 -70
- 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 +97 -61
- package/dist/ui/components/card/card.ts +2 -1
- package/dist/ui/components/chart/chart-presets.test.ts +197 -1
- package/dist/ui/components/chart/chart-presets.ts +14 -7
- package/dist/ui/components/chart/chart-theme.ts +12 -0
- package/dist/ui/components/chart/chart.test.ts +1031 -0
- package/dist/ui/components/chart/chart.ts +66 -46
- package/dist/ui/components/checkbox/checkbox.test.ts +124 -42
- package/dist/ui/components/checkbox/checkbox.ts +25 -24
- package/dist/ui/components/collapsible/collapsible.test.ts +158 -81
- package/dist/ui/components/collapsible/collapsible.ts +21 -15
- 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 +35 -29
- package/dist/ui/components/dock/dock-scroll.test.ts +130 -0
- package/dist/ui/components/dock/dock-scroll.ts +142 -0
- package/dist/ui/components/dock/dock-search.test.ts +594 -0
- package/dist/ui/components/dock/dock.test.ts +752 -0
- package/dist/ui/components/dock/dock.ts +1438 -0
- package/dist/ui/components/dock/index.ts +2 -0
- package/dist/ui/components/dynamic-color/dynamic-color.test.ts +144 -0
- package/dist/ui/components/dynamic-color/dynamic-color.ts +146 -0
- package/dist/ui/components/dynamic-color/index.ts +5 -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 +376 -0
- package/dist/ui/components/glass/glass.ts +430 -0
- package/dist/ui/components/glass/index.ts +19 -0
- package/dist/ui/components/icon/icon-theme.test.ts +64 -0
- package/dist/ui/components/icon/icon-theme.ts +70 -16
- 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 +17 -13
- package/dist/ui/components/label/label.test.ts +11 -24
- package/dist/ui/components/label/label.ts +8 -2
- package/dist/ui/components/line-chart/line-chart.test.ts +76 -1
- package/dist/ui/components/line-chart/line-chart.ts +3 -2
- package/dist/ui/components/list/list.test.ts +80 -49
- package/dist/ui/components/list/list.ts +5 -4
- 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 +636 -0
- package/dist/ui/components/menu/menu-popup.ts +565 -0
- package/dist/ui/components/menu/menu.test.ts +523 -0
- package/dist/ui/components/menu/menu.ts +455 -0
- package/dist/ui/components/nav-drawer/nav-drawer.test.ts +425 -260
- package/dist/ui/components/nav-drawer/nav-drawer.ts +75 -58
- package/dist/ui/components/navigation-bar/index.ts +1 -0
- package/dist/ui/components/navigation-bar/navigation-bar.test.ts +425 -0
- package/dist/ui/components/navigation-bar/navigation-bar.ts +411 -0
- package/dist/ui/components/pagination/pagination.test.ts +136 -86
- package/dist/ui/components/popover/index.ts +9 -0
- package/dist/ui/components/popover/popover-panel.test.ts +735 -0
- package/dist/ui/components/popover/popover-panel.ts +603 -0
- package/dist/ui/components/popover/popover.test.ts +286 -0
- package/dist/ui/components/popover/popover.ts +207 -0
- package/dist/ui/components/progress/progress.test.ts +102 -46
- package/dist/ui/components/progress/progress.ts +54 -55
- package/dist/ui/components/radio-group/radio-group.test.ts +128 -83
- package/dist/ui/components/radio-group/radio-group.ts +26 -25
- 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/scroll-area/scroll-area.ts +4 -3
- package/dist/ui/components/search-field/index.ts +15 -0
- package/dist/ui/components/search-field/keyboard.test.ts +147 -0
- package/dist/ui/components/search-field/keyboard.ts +123 -0
- package/dist/ui/components/search-field/search-field.test.ts +549 -0
- package/dist/ui/components/search-field/search-field.ts +628 -0
- package/dist/ui/components/segmented-control/index.ts +8 -0
- package/dist/ui/components/segmented-control/segmented-control.test.ts +861 -0
- package/dist/ui/components/segmented-control/segmented-control.ts +884 -0
- package/dist/ui/components/select/select.test.ts +138 -78
- package/dist/ui/components/select/select.ts +19 -12
- package/dist/ui/components/separator/separator.test.ts +18 -17
- package/dist/ui/components/sheet/sheet.test.ts +151 -11
- package/dist/ui/components/sheet/sheet.ts +68 -5
- package/dist/ui/components/skeleton/skeleton.test.ts +24 -7
- package/dist/ui/components/skeleton/skeleton.ts +10 -16
- package/dist/ui/components/slider/index.ts +1 -0
- package/dist/ui/components/slider/slider.test.ts +695 -0
- package/dist/ui/components/slider/slider.ts +901 -0
- package/dist/ui/components/spinner/spinner.test.ts +64 -46
- package/dist/ui/components/spinner/spinner.ts +16 -19
- package/dist/ui/components/switch/index.ts +1 -1
- package/dist/ui/components/switch/switch.test.ts +534 -48
- package/dist/ui/components/switch/switch.ts +613 -48
- 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/index.ts +7 -1
- package/dist/ui/components/tabs/tabs.test.ts +484 -150
- package/dist/ui/components/tabs/tabs.ts +344 -149
- package/dist/ui/components/textarea/textarea.test.ts +206 -100
- package/dist/ui/components/textarea/textarea.ts +16 -13
- package/dist/ui/components/toast/toast.test.ts +434 -350
- package/dist/ui/components/toast/toast.ts +42 -51
- package/dist/ui/components/toggle/index.ts +1 -0
- package/dist/ui/components/toggle/toggle.test.ts +559 -146
- package/dist/ui/components/toggle/toggle.ts +241 -26
- package/dist/ui/components/toolbar/bar-scroll.test.ts +310 -0
- package/dist/ui/components/toolbar/bar-scroll.ts +372 -0
- package/dist/ui/components/toolbar/index.ts +12 -0
- package/dist/ui/components/toolbar/toolbar.test.ts +438 -0
- package/dist/ui/components/toolbar/toolbar.ts +535 -0
- 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 +19 -0
- package/dist/utils/analyze.js +16 -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 +25 -1
- package/dist/utils/installed.js +33 -0
- package/dist/utils/installed.js.map +1 -1
- package/dist/utils/installed.test.js +55 -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 +45 -0
- package/dist/utils/tailwind.js +90 -0
- package/dist/utils/tailwind.js.map +1 -0
- package/dist/utils/tailwind.test.d.ts +1 -0
- package/dist/utils/tailwind.test.js +134 -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 { createChartTooltipContent } from '@tanstack/charts/tooltip/model';
|
|
3
9
|
import type {
|
|
@@ -6,7 +12,11 @@ import type {
|
|
|
6
12
|
SceneNode,
|
|
7
13
|
} from '@tanstack/charts/types';
|
|
8
14
|
import { describe, expect, it } from 'vitest';
|
|
9
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
type CandlestickDatum,
|
|
17
|
+
UiCandlestickChart,
|
|
18
|
+
candlestickChartDefinition,
|
|
19
|
+
} from './candlestick-chart';
|
|
10
20
|
|
|
11
21
|
const data = [
|
|
12
22
|
{ x: 0, open: 100, high: 104, low: 98, close: 102 },
|
|
@@ -116,3 +126,74 @@ describe('candlestickChartDefinition', () => {
|
|
|
116
126
|
expect(render2(definition).found.length).toBeGreaterThan(0);
|
|
117
127
|
});
|
|
118
128
|
});
|
|
129
|
+
|
|
130
|
+
@Component({
|
|
131
|
+
imports: [UiCandlestickChart],
|
|
132
|
+
template: `
|
|
133
|
+
<ui-candlestick-chart
|
|
134
|
+
[data]="data"
|
|
135
|
+
[width]="300"
|
|
136
|
+
[upColor]="upColor()"
|
|
137
|
+
downColor="red"
|
|
138
|
+
(candleTap)="tapped.push($event)"
|
|
139
|
+
/>
|
|
140
|
+
`,
|
|
141
|
+
})
|
|
142
|
+
class CandlestickChartHost {
|
|
143
|
+
readonly data = data;
|
|
144
|
+
readonly upColor = signal('green');
|
|
145
|
+
/** Every `candleTap`, in order. */
|
|
146
|
+
readonly tapped: CandlestickDatum[] = [];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The SVG the chart's marks layer draws.
|
|
151
|
+
*/
|
|
152
|
+
const marksSvg = (container: Element) =>
|
|
153
|
+
container.querySelectorAll('svg')[1]!.getAttribute('content')!;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The middle of each candle body the chart drew.
|
|
157
|
+
*/
|
|
158
|
+
const bodiesDrawn = (container: Element) =>
|
|
159
|
+
[
|
|
160
|
+
...marksSvg(container).matchAll(
|
|
161
|
+
/<rect x="([\d.-]+)" y="([\d.-]+)" width="([\d.-]+)" height="([\d.-]+)"[^>]*fill=/g,
|
|
162
|
+
),
|
|
163
|
+
].map(([, x, y, width, height]) => ({
|
|
164
|
+
x: Number(x) + Number(width) / 2,
|
|
165
|
+
y: Number(y) + Number(height) / 2,
|
|
166
|
+
}));
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Touches a spot of the chart and lifts the finger right there: a tap.
|
|
170
|
+
*/
|
|
171
|
+
const tap = async (container: Element, at: { x: number; y: number }) => {
|
|
172
|
+
const root = container.querySelector('[accessibility-label]')!;
|
|
173
|
+
fireEvent.touchstart(root, { touches: [at] });
|
|
174
|
+
fireEvent.touchend(root, { touches: [at] });
|
|
175
|
+
await waitForUpdate();
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
describe('UiCandlestickChart', () => {
|
|
179
|
+
it('colors each candle by direction with its inputs', async () => {
|
|
180
|
+
const { componentRef, container } = await render(CandlestickChartHost);
|
|
181
|
+
expect(bodiesDrawn(container)).toHaveLength(2);
|
|
182
|
+
expect(marksSvg(container)).toContain('fill="green"');
|
|
183
|
+
expect(marksSvg(container)).toContain('fill="red"');
|
|
184
|
+
|
|
185
|
+
(componentRef.instance as CandlestickChartHost).upColor.set('blue');
|
|
186
|
+
await waitForUpdate();
|
|
187
|
+
expect(marksSvg(container)).toContain('fill="blue"');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("emits a tapped candle's datum, and nothing for a tap off the candles", async () => {
|
|
191
|
+
const { componentRef, container } = await render(CandlestickChartHost);
|
|
192
|
+
const host = componentRef.instance as CandlestickChartHost;
|
|
193
|
+
await tap(container, bodiesDrawn(container)[0]!);
|
|
194
|
+
expect(host.tapped).toEqual([data[0]]);
|
|
195
|
+
|
|
196
|
+
await tap(container, { x: -100, y: -100 });
|
|
197
|
+
expect(host.tapped).toEqual([data[0]]);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -1,8 +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
8
|
import { describe, expect, it } from 'vitest';
|
|
5
|
-
import { setInputSignal } from '../../../test-utils/set-input-signal';
|
|
6
9
|
import {
|
|
7
10
|
UiCard,
|
|
8
11
|
UiCardContent,
|
|
@@ -13,7 +16,6 @@ import {
|
|
|
13
16
|
} from './card';
|
|
14
17
|
|
|
15
18
|
@Component({
|
|
16
|
-
standalone: true,
|
|
17
19
|
imports: [
|
|
18
20
|
UiCard,
|
|
19
21
|
UiCardHeader,
|
|
@@ -24,20 +26,53 @@ import {
|
|
|
24
26
|
LYNX_ELEMENTS,
|
|
25
27
|
],
|
|
26
28
|
template: `
|
|
27
|
-
<ui-card>
|
|
28
|
-
<ui-card-header>
|
|
29
|
-
<ui-card-title>Title</ui-card-title>
|
|
30
|
-
<ui-card-description>Desc</ui-card-description>
|
|
29
|
+
<ui-card [pressable]="pressable()" class="w-full">
|
|
30
|
+
<ui-card-header class="gap-1">
|
|
31
|
+
<ui-card-title class="text-lg">Title</ui-card-title>
|
|
32
|
+
<ui-card-description class="italic">Desc</ui-card-description>
|
|
31
33
|
</ui-card-header>
|
|
32
|
-
<ui-card-content><text>Body</text></ui-card-content>
|
|
33
|
-
<ui-card-footer><text>Actions</text></ui-card-footer>
|
|
34
|
+
<ui-card-content class="pt-2"><text>Body</text></ui-card-content>
|
|
35
|
+
<ui-card-footer class="justify-end"><text>Actions</text></ui-card-footer>
|
|
34
36
|
</ui-card>
|
|
35
37
|
`,
|
|
36
38
|
})
|
|
37
|
-
class CardHost {
|
|
39
|
+
class CardHost {
|
|
40
|
+
readonly pressable = signal(false);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Component({
|
|
44
|
+
imports: [UiCard, LYNX_ELEMENTS],
|
|
45
|
+
template: `<ui-card pressable><text>Order</text></ui-card>`,
|
|
46
|
+
})
|
|
47
|
+
class BarePressableHost {}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The card's own view. Each component's host element renders as a view too,
|
|
51
|
+
* so it's found by the card's class rather than by being the first.
|
|
52
|
+
*/
|
|
53
|
+
const cardView = (container: Element) => container.querySelector('.bg-card')!;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Fires a Lynx event `fireEvent` has no shortcut for, as Lynx dispatches it:
|
|
57
|
+
* `mouseleave` is the mouse's `touchcancel`.
|
|
58
|
+
*/
|
|
59
|
+
const fireLynxEvent = (element: Element, name: string) =>
|
|
60
|
+
fireEvent(
|
|
61
|
+
element,
|
|
62
|
+
Object.assign(new Event(`bindEvent:${name}`), {
|
|
63
|
+
eventType: 'bindEvent',
|
|
64
|
+
eventName: name,
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The scale the card's inline style holds, if any.
|
|
70
|
+
*/
|
|
71
|
+
const scaleOf = (view: Element) =>
|
|
72
|
+
(view as HTMLElement).style.getPropertyValue('transform');
|
|
38
73
|
|
|
39
74
|
describe('UiCard', () => {
|
|
40
|
-
it('renders each
|
|
75
|
+
it('renders each part with its projected content', async () => {
|
|
41
76
|
const { getByText } = await render(CardHost);
|
|
42
77
|
expect(getByText('Title')).toBeTruthy();
|
|
43
78
|
expect(getByText('Desc')).toBeTruthy();
|
|
@@ -45,66 +80,67 @@ describe('UiCard', () => {
|
|
|
45
80
|
expect(getByText('Actions')).toBeTruthy();
|
|
46
81
|
});
|
|
47
82
|
|
|
48
|
-
it('
|
|
49
|
-
const { container } = await render(
|
|
50
|
-
expect(container).
|
|
83
|
+
it("merges each part's class with its own", async () => {
|
|
84
|
+
const { container, getByText } = await render(CardHost);
|
|
85
|
+
expect(cardView(container).getAttribute('class')).toContain('bg-card');
|
|
86
|
+
expect(cardView(container).getAttribute('class')).toContain('w-full');
|
|
87
|
+
expect(getByText('Title').getAttribute('class')).toContain('text-lg');
|
|
88
|
+
expect(getByText('Desc').getAttribute('class')).toContain('italic');
|
|
51
89
|
});
|
|
52
90
|
|
|
53
|
-
it('
|
|
54
|
-
const {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
91
|
+
it("doesn't respond to presses unless it's pressable", async () => {
|
|
92
|
+
const { container } = await render(CardHost);
|
|
93
|
+
const view = cardView(container);
|
|
94
|
+
fireEvent.touchstart(view);
|
|
95
|
+
fireEvent.mousedown(view);
|
|
96
|
+
expect(scaleOf(view)).toBe('');
|
|
97
|
+
fireEvent.touchend(view);
|
|
98
|
+
fireEvent.touchcancel(view);
|
|
99
|
+
fireEvent.mouseup(view);
|
|
100
|
+
fireLynxEvent(view, 'mouseleave');
|
|
101
|
+
expect(scaleOf(view)).toBe('');
|
|
63
102
|
});
|
|
64
103
|
|
|
65
|
-
it('
|
|
66
|
-
const { componentRef } = await render(
|
|
67
|
-
|
|
68
|
-
setInputSignal(inst.pressable, true);
|
|
104
|
+
it('scales down while pressed by touch, and back on release', async () => {
|
|
105
|
+
const { container, componentRef } = await render(CardHost);
|
|
106
|
+
(componentRef.instance as CardHost).pressable.set(true);
|
|
69
107
|
await waitForUpdate();
|
|
70
|
-
const
|
|
71
|
-
onPressStart: () => void;
|
|
72
|
-
onPressEnd: () => void;
|
|
73
|
-
onPressCancel: () => void;
|
|
74
|
-
};
|
|
75
|
-
// Run twice so `#pressAnim?.cancel()` gets exercised on the second call.
|
|
76
|
-
cast.onPressStart();
|
|
77
|
-
cast.onPressStart();
|
|
78
|
-
cast.onPressEnd();
|
|
79
|
-
cast.onPressCancel();
|
|
80
|
-
// No assertion — coverage-only paths. Confirm no throw.
|
|
81
|
-
expect(true).toBe(true);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
108
|
+
const view = cardView(container);
|
|
84
109
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
expect(
|
|
89
|
-
});
|
|
110
|
+
fireEvent.touchstart(view);
|
|
111
|
+
expect(scaleOf(view)).toBe('scale(0.985)');
|
|
112
|
+
fireEvent.touchend(view);
|
|
113
|
+
expect(scaleOf(view)).toBe('scale(1)');
|
|
90
114
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
115
|
+
// A scroll that takes the touch over cancels it: the card scales back.
|
|
116
|
+
fireEvent.touchstart(view);
|
|
117
|
+
fireEvent.touchcancel(view);
|
|
118
|
+
expect(scaleOf(view)).toBe('scale(1)');
|
|
94
119
|
});
|
|
95
120
|
|
|
96
|
-
it('
|
|
97
|
-
const { container } = await render(
|
|
98
|
-
|
|
99
|
-
});
|
|
121
|
+
it('is pressable with a bare pressable attribute', async () => {
|
|
122
|
+
const { container } = await render(BarePressableHost);
|
|
123
|
+
const view = cardView(container);
|
|
100
124
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
125
|
+
fireEvent.touchstart(view);
|
|
126
|
+
expect(scaleOf(view)).toBe('scale(0.985)');
|
|
127
|
+
fireEvent.touchend(view);
|
|
128
|
+
expect(scaleOf(view)).toBe('scale(1)');
|
|
104
129
|
});
|
|
105
130
|
|
|
106
|
-
it('
|
|
107
|
-
const { container } = await render(
|
|
108
|
-
|
|
131
|
+
it('scales down while pressed by mouse, and back on release or leave', async () => {
|
|
132
|
+
const { container, componentRef } = await render(CardHost);
|
|
133
|
+
(componentRef.instance as CardHost).pressable.set(true);
|
|
134
|
+
await waitForUpdate();
|
|
135
|
+
const view = cardView(container);
|
|
136
|
+
|
|
137
|
+
fireEvent.mousedown(view);
|
|
138
|
+
expect(scaleOf(view)).toBe('scale(0.985)');
|
|
139
|
+
fireEvent.mouseup(view);
|
|
140
|
+
expect(scaleOf(view)).toBe('scale(1)');
|
|
141
|
+
|
|
142
|
+
fireEvent.mousedown(view);
|
|
143
|
+
fireLynxEvent(view, 'mouseleave');
|
|
144
|
+
expect(scaleOf(view)).toBe('scale(1)');
|
|
109
145
|
});
|
|
110
146
|
});
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
type ElementRef,
|
|
3
3
|
Component,
|
|
4
4
|
ViewEncapsulation,
|
|
5
|
+
booleanAttribute,
|
|
5
6
|
computed,
|
|
6
7
|
input,
|
|
7
8
|
viewChild,
|
|
@@ -40,7 +41,7 @@ export class UiCard {
|
|
|
40
41
|
/**
|
|
41
42
|
* When true, the card responds to touch with a subtle scale animation
|
|
42
43
|
*/
|
|
43
|
-
readonly pressable = input(false);
|
|
44
|
+
readonly pressable = input(false, { transform: booleanAttribute });
|
|
44
45
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
45
46
|
|
|
46
47
|
readonly containerRef = viewChild<ElementRef>('container');
|
|
@@ -1,5 +1,30 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
Injector,
|
|
4
|
+
type Type,
|
|
5
|
+
computed,
|
|
6
|
+
input,
|
|
7
|
+
output,
|
|
8
|
+
signal,
|
|
9
|
+
viewChild,
|
|
10
|
+
} from '@angular/core';
|
|
11
|
+
import {
|
|
12
|
+
fireEvent,
|
|
13
|
+
render,
|
|
14
|
+
waitForUpdate,
|
|
15
|
+
} from '@blotch/angular-lynx-testing-library';
|
|
16
|
+
import { defineChart, dot } from '@tanstack/charts/universal';
|
|
1
17
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
18
|
+
import { UiChart, tooltip } from './chart';
|
|
19
|
+
import {
|
|
20
|
+
type CartesianPresetOptions,
|
|
21
|
+
PRESET_CHART_BINDINGS,
|
|
22
|
+
UiCartesianPreset,
|
|
23
|
+
type XYDatum,
|
|
24
|
+
linearAxis,
|
|
25
|
+
minSpacing,
|
|
26
|
+
presetAxes,
|
|
27
|
+
} from './chart-presets';
|
|
3
28
|
|
|
4
29
|
/**
|
|
5
30
|
* The configured domain of an axis config's scale instance.
|
|
@@ -137,3 +162,174 @@ describe('presetAxes', () => {
|
|
|
137
162
|
expect(domainOf(axes.y)).toEqual([-10, 10]);
|
|
138
163
|
});
|
|
139
164
|
});
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The smallest preset: a dot per datum on the shared axes, built the way the
|
|
168
|
+
* library's presets (`<ui-line-chart>`, …) build theirs.
|
|
169
|
+
*/
|
|
170
|
+
@Component({
|
|
171
|
+
selector: 'test-dot-chart',
|
|
172
|
+
imports: [UiChart],
|
|
173
|
+
template: `
|
|
174
|
+
<ui-chart
|
|
175
|
+
#chart
|
|
176
|
+
[definition]="definition()"
|
|
177
|
+
${PRESET_CHART_BINDINGS}
|
|
178
|
+
(pointSelect)="pointTap.emit(datumOf($event))"
|
|
179
|
+
/>
|
|
180
|
+
`,
|
|
181
|
+
})
|
|
182
|
+
class DotChart extends UiCartesianPreset<XYDatum> {
|
|
183
|
+
readonly data = input.required<readonly XYDatum[]>();
|
|
184
|
+
/** The tapped datum, or `null` for a tap on empty space. */
|
|
185
|
+
readonly pointTap = output<XYDatum | null>();
|
|
186
|
+
|
|
187
|
+
protected readonly definition = computed(() => {
|
|
188
|
+
const options = this.commonOptions();
|
|
189
|
+
const data = this.data();
|
|
190
|
+
return defineChart({
|
|
191
|
+
marks: [dot(data, { x: 'x', y: 'y' })],
|
|
192
|
+
scales: presetAxes(options, {
|
|
193
|
+
x: data.map((d) => d.x),
|
|
194
|
+
y: data.map((d) => d.y),
|
|
195
|
+
}),
|
|
196
|
+
tooltip: options.tooltip === false ? false : tooltip,
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
@Component({
|
|
202
|
+
imports: [DotChart],
|
|
203
|
+
template: `
|
|
204
|
+
<test-dot-chart
|
|
205
|
+
[data]="data"
|
|
206
|
+
[width]="300"
|
|
207
|
+
[height]="160"
|
|
208
|
+
label="Sales"
|
|
209
|
+
class="mt-2"
|
|
210
|
+
[xAxisLabel]="xAxisLabel()"
|
|
211
|
+
yAxisLabel="Units"
|
|
212
|
+
[zoomable]="true"
|
|
213
|
+
zoomAxes="x"
|
|
214
|
+
(pointTap)="tapped.push($event)"
|
|
215
|
+
/>
|
|
216
|
+
`,
|
|
217
|
+
})
|
|
218
|
+
class PresetHost {
|
|
219
|
+
readonly data: readonly XYDatum[] = [
|
|
220
|
+
{ x: 0, y: 100 },
|
|
221
|
+
{ x: 5, y: 300 },
|
|
222
|
+
{ x: 10, y: 500 },
|
|
223
|
+
];
|
|
224
|
+
readonly xAxisLabel = signal('Day');
|
|
225
|
+
/** Every `pointTap`, in order. */
|
|
226
|
+
readonly tapped: (XYDatum | null)[] = [];
|
|
227
|
+
readonly preset = viewChild.required(DotChart);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The chart's own view, which takes every touch.
|
|
232
|
+
*/
|
|
233
|
+
const chartRoot = (container: Element) =>
|
|
234
|
+
container.querySelector('[accessibility-label]')!;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Where the chart drew each dot, read from its marks layer.
|
|
238
|
+
*/
|
|
239
|
+
const dotsDrawn = (container: Element) =>
|
|
240
|
+
[
|
|
241
|
+
...container
|
|
242
|
+
.querySelectorAll('svg')[1]!
|
|
243
|
+
.getAttribute('content')!
|
|
244
|
+
.matchAll(/<circle cx="([\d.-]+)" cy="([\d.-]+)"/g),
|
|
245
|
+
].map(([, x, y]) => ({ x: Number(x), y: Number(y) }));
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Touches a spot and lifts the finger right there: a tap.
|
|
249
|
+
*/
|
|
250
|
+
const tap = async (root: Element, at: { x: number; y: number }) => {
|
|
251
|
+
fireEvent.touchstart(root, { touches: [at] });
|
|
252
|
+
fireEvent.touchend(root, { touches: [at] });
|
|
253
|
+
await waitForUpdate();
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
describe('UiCartesianPreset', () => {
|
|
257
|
+
it('starts every preset from the same defaults', () => {
|
|
258
|
+
// Angular builds a preset through the base's factory, with the preset's
|
|
259
|
+
// own type, so no preset runs that factory's default: build the base alone.
|
|
260
|
+
const base = Injector.create({
|
|
261
|
+
providers: [
|
|
262
|
+
{
|
|
263
|
+
provide: UiCartesianPreset,
|
|
264
|
+
useClass: UiCartesianPreset as unknown as Type<
|
|
265
|
+
UiCartesianPreset<XYDatum>
|
|
266
|
+
>,
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
}).get(UiCartesianPreset);
|
|
270
|
+
const options = (
|
|
271
|
+
base as unknown as { commonOptions(): CartesianPresetOptions }
|
|
272
|
+
).commonOptions();
|
|
273
|
+
expect(options).toEqual({
|
|
274
|
+
tickCount: 5,
|
|
275
|
+
showGrid: true,
|
|
276
|
+
showXGrid: false,
|
|
277
|
+
xAxisLabel: '',
|
|
278
|
+
yAxisLabel: '',
|
|
279
|
+
padding: {},
|
|
280
|
+
xMin: undefined,
|
|
281
|
+
xMax: undefined,
|
|
282
|
+
yMin: undefined,
|
|
283
|
+
yMax: undefined,
|
|
284
|
+
xTickFormat: undefined,
|
|
285
|
+
yTickFormat: undefined,
|
|
286
|
+
tooltip: true,
|
|
287
|
+
});
|
|
288
|
+
expect(base.height()).toBe(200);
|
|
289
|
+
expect(base.label()).toBe('Chart');
|
|
290
|
+
expect(base.zoomable()).toBe(false);
|
|
291
|
+
expect(base.zoomAxes()).toBe('xy');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('passes the shared inputs through to its chart', async () => {
|
|
295
|
+
const { container, componentRef, getByText } = await render(PresetHost);
|
|
296
|
+
const root = chartRoot(container);
|
|
297
|
+
expect(root.getAttribute('accessibility-label')).toBe('Sales');
|
|
298
|
+
expect(root.getAttribute('class')).toContain('mt-2');
|
|
299
|
+
expect(root.getAttribute('style')).toContain('height: 160px');
|
|
300
|
+
expect(getByText('Units')).toBeTruthy();
|
|
301
|
+
expect(getByText('Day')).toBeTruthy();
|
|
302
|
+
|
|
303
|
+
(componentRef.instance as PresetHost).xAxisLabel.set('Week');
|
|
304
|
+
await waitForUpdate();
|
|
305
|
+
expect(getByText('Week')).toBeTruthy();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('hands a tap over as the datum, or null off the points', async () => {
|
|
309
|
+
const { container, componentRef } = await render(PresetHost);
|
|
310
|
+
const host = componentRef.instance as PresetHost;
|
|
311
|
+
const root = chartRoot(container);
|
|
312
|
+
const middle = dotsDrawn(container)[1]!;
|
|
313
|
+
await tap(root, { x: middle.x + 1, y: middle.y + 1 });
|
|
314
|
+
await tap(root, { x: -100, y: -100 });
|
|
315
|
+
expect(host.tapped).toEqual([host.data[1], null]);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('zooms its chart in and out, and resets it', async () => {
|
|
319
|
+
const { componentRef, queryByText } = await render(PresetHost);
|
|
320
|
+
const preset = (componentRef.instance as PresetHost).preset();
|
|
321
|
+
// The x axis spans 0–10; zoomed in around the middle, its last tick goes.
|
|
322
|
+
expect(queryByText('10')).not.toBeNull();
|
|
323
|
+
preset.zoomIn();
|
|
324
|
+
await waitForUpdate();
|
|
325
|
+
expect(queryByText('10')).toBeNull();
|
|
326
|
+
preset.zoomOut();
|
|
327
|
+
await waitForUpdate();
|
|
328
|
+
expect(queryByText('10')).not.toBeNull();
|
|
329
|
+
|
|
330
|
+
preset.zoomIn();
|
|
331
|
+
preset.resetZoom();
|
|
332
|
+
await waitForUpdate();
|
|
333
|
+
expect(queryByText('10')).not.toBeNull();
|
|
334
|
+
});
|
|
335
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Directive, input, viewChild } from '@angular/core';
|
|
1
|
+
import { Directive, booleanAttribute, input, viewChild } from '@angular/core';
|
|
2
2
|
import { scaleLinear } from '@tanstack/charts/scales/linear';
|
|
3
3
|
import type {
|
|
4
4
|
ChartPoint,
|
|
@@ -173,6 +173,13 @@ export const minSpacing = (values: readonly number[]): number => {
|
|
|
173
173
|
return Number.isFinite(spacing) && spacing > 0 ? spacing : 1;
|
|
174
174
|
};
|
|
175
175
|
|
|
176
|
+
/**
|
|
177
|
+
* `UiChart`, for `viewChild()`. A call rather than the class itself: for a
|
|
178
|
+
* class, Angular's test build keeps an arrow function that nothing calls,
|
|
179
|
+
* which an app's coverage report would show as uncovered.
|
|
180
|
+
*/
|
|
181
|
+
const chartType = () => UiChart;
|
|
182
|
+
|
|
176
183
|
/**
|
|
177
184
|
* The inputs and zoom API every cartesian preset shares. A preset extends this
|
|
178
185
|
* and adds its own data and mark inputs, and renders `<ui-chart #chart>`
|
|
@@ -186,9 +193,9 @@ export abstract class UiCartesianPreset<TDatum> {
|
|
|
186
193
|
/** Target number of ticks per axis; the axis picks nice values near it. */
|
|
187
194
|
readonly tickCount = input(5);
|
|
188
195
|
/** Horizontal gridlines at the y ticks. */
|
|
189
|
-
readonly showGrid = input(true);
|
|
196
|
+
readonly showGrid = input(true, { transform: booleanAttribute });
|
|
190
197
|
/** Vertical gridlines at the x ticks. */
|
|
191
|
-
readonly showXGrid = input(false);
|
|
198
|
+
readonly showXGrid = input(false, { transform: booleanAttribute });
|
|
192
199
|
readonly xAxisLabel = input('');
|
|
193
200
|
readonly yAxisLabel = input('');
|
|
194
201
|
readonly padding = input<ChartPadding>({});
|
|
@@ -199,20 +206,20 @@ export abstract class UiCartesianPreset<TDatum> {
|
|
|
199
206
|
readonly xTickFormat = input<(value: number) => string>();
|
|
200
207
|
readonly yTickFormat = input<(value: number) => string>();
|
|
201
208
|
/** Shows a tooltip for the touched point; a tap pins it. */
|
|
202
|
-
readonly tooltip = input(true);
|
|
209
|
+
readonly tooltip = input(true, { transform: booleanAttribute });
|
|
203
210
|
/** What a screen reader announces for the chart. */
|
|
204
211
|
readonly label = input('Chart');
|
|
205
212
|
/** Light, dark or `'system'` colors; the chart theme's setting when unset. */
|
|
206
213
|
readonly colorScheme = input<Exclude<ChartColorSchemeOption, Function>>();
|
|
207
|
-
readonly zoomable = input(false);
|
|
214
|
+
readonly zoomable = input(false, { transform: booleanAttribute });
|
|
208
215
|
readonly zoomAxes = input<ChartZoomAxes>('xy');
|
|
209
|
-
readonly showZoomControls = input(true);
|
|
216
|
+
readonly showZoomControls = input(true, { transform: booleanAttribute });
|
|
210
217
|
readonly minZoom = input(1);
|
|
211
218
|
readonly maxZoom = input(8);
|
|
212
219
|
readonly zoomStep = input(1.4);
|
|
213
220
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
214
221
|
|
|
215
|
-
protected readonly chart = viewChild.required(
|
|
222
|
+
protected readonly chart = viewChild.required(chartType());
|
|
216
223
|
|
|
217
224
|
/**
|
|
218
225
|
* The shared inputs, as the options every preset's definition takes.
|
|
@@ -79,6 +79,12 @@ export const CHART_LIGHT_COLORS: ChartColorTokens = {
|
|
|
79
79
|
'chart-3': 'rgba(39, 71, 84, 1)',
|
|
80
80
|
'chart-4': 'rgba(232, 196, 104, 1)',
|
|
81
81
|
'chart-5': 'rgba(244, 164, 98, 1)',
|
|
82
|
+
'glass-tint': 'rgba(24, 24, 27, 1)',
|
|
83
|
+
'glass-tint-foreground': 'rgba(250, 250, 250, 1)',
|
|
84
|
+
'glass-foreground': 'rgba(9, 9, 11, 1)',
|
|
85
|
+
'glass-muted-foreground': 'rgba(63, 63, 70, 1)',
|
|
86
|
+
'glass-clear-foreground': 'rgba(255, 255, 255, 1)',
|
|
87
|
+
'glass-dim': 'rgba(0, 0, 0, 0.35)',
|
|
82
88
|
};
|
|
83
89
|
|
|
84
90
|
export const CHART_DARK_COLORS: ChartColorTokens = {
|
|
@@ -106,6 +112,12 @@ export const CHART_DARK_COLORS: ChartColorTokens = {
|
|
|
106
112
|
'chart-3': 'rgba(232, 140, 48, 1)',
|
|
107
113
|
'chart-4': 'rgba(175, 87, 219, 1)',
|
|
108
114
|
'chart-5': 'rgba(226, 54, 112, 1)',
|
|
115
|
+
'glass-tint': 'rgba(250, 250, 250, 1)',
|
|
116
|
+
'glass-tint-foreground': 'rgba(24, 24, 27, 1)',
|
|
117
|
+
'glass-foreground': 'rgba(250, 250, 250, 1)',
|
|
118
|
+
'glass-muted-foreground': 'rgba(212, 212, 216, 1)',
|
|
119
|
+
'glass-clear-foreground': 'rgba(255, 255, 255, 1)',
|
|
120
|
+
'glass-dim': 'rgba(0, 0, 0, 0.35)',
|
|
109
121
|
};
|
|
110
122
|
|
|
111
123
|
/**
|