@blotch/dolan 22.2.2 → 22.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/add.d.ts +6 -2
- package/dist/commands/add.js +32 -10
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/add.test.js +129 -9
- package/dist/commands/add.test.js.map +1 -1
- package/dist/commands/diff.d.ts +2 -1
- package/dist/commands/diff.js +38 -25
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/diff.test.js +72 -11
- package/dist/commands/diff.test.js.map +1 -1
- package/dist/commands/doctor.js +41 -27
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +93 -9
- package/dist/commands/doctor.test.js.map +1 -1
- package/dist/commands/eject.js +7 -0
- package/dist/commands/eject.js.map +1 -1
- package/dist/commands/eject.test.js +39 -0
- package/dist/commands/eject.test.js.map +1 -1
- package/dist/commands/info.js +3 -2
- package/dist/commands/info.js.map +1 -1
- package/dist/commands/init.d.ts +37 -1
- package/dist/commands/init.js +149 -40
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/init.test.js +285 -26
- package/dist/commands/init.test.js.map +1 -1
- package/dist/commands/install-packages.d.ts +6 -0
- package/dist/commands/install-packages.js +20 -0
- package/dist/commands/install-packages.js.map +1 -0
- package/dist/commands/install-packages.test.d.ts +1 -0
- package/dist/commands/install-packages.test.js +55 -0
- package/dist/commands/install-packages.test.js.map +1 -0
- package/dist/commands/list.js +21 -14
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/list.test.js +62 -5
- package/dist/commands/list.test.js.map +1 -1
- package/dist/commands/outdated.js +11 -10
- package/dist/commands/outdated.js.map +1 -1
- package/dist/commands/outdated.test.js +20 -5
- package/dist/commands/outdated.test.js.map +1 -1
- package/dist/commands/remove.d.ts +4 -0
- package/dist/commands/remove.js +17 -4
- package/dist/commands/remove.js.map +1 -1
- package/dist/commands/remove.test.js +53 -2
- package/dist/commands/remove.test.js.map +1 -1
- package/dist/commands/update.d.ts +6 -0
- package/dist/commands/update.js +59 -24
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/update.test.js +273 -31
- package/dist/commands/update.test.js.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/registry.d.ts +8 -0
- package/dist/registry.js +32 -24
- package/dist/registry.js.map +1 -1
- package/dist/registry.test.js +76 -37
- package/dist/registry.test.js.map +1 -1
- package/dist/testing/set-input-signal.d.ts +13 -0
- package/dist/testing/set-input-signal.js +20 -0
- package/dist/testing/set-input-signal.js.map +1 -0
- package/dist/testing/set-input-signal.test.d.ts +1 -0
- package/dist/testing/set-input-signal.test.js +20 -0
- package/dist/testing/set-input-signal.test.js.map +1 -0
- package/dist/ui/components/action-sheet/action-sheet.test.ts +48 -1
- package/dist/ui/components/action-sheet/action-sheet.ts +37 -9
- package/dist/ui/components/alert-dialog/alert-dialog.test.ts +49 -2
- package/dist/ui/components/alert-dialog/alert-dialog.ts +43 -12
- package/dist/ui/components/area-chart/area-chart-render.test.ts +52 -110
- package/dist/ui/components/area-chart/area-chart.test.ts +58 -123
- package/dist/ui/components/area-chart/area-chart.ts +100 -315
- package/dist/ui/components/area-chart/index.ts +2 -3
- package/dist/ui/components/bar-chart/bar-chart-render.test.ts +47 -163
- package/dist/ui/components/bar-chart/bar-chart.test.ts +83 -37
- package/dist/ui/components/bar-chart/bar-chart.ts +140 -294
- package/dist/ui/components/bar-chart/index.ts +3 -3
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +191 -9
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +136 -20
- package/dist/ui/components/button/button-render.test.ts +19 -0
- package/dist/ui/components/button/button.ts +18 -2
- package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +48 -163
- package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +106 -62
- package/dist/ui/components/candlestick-chart/candlestick-chart.ts +150 -323
- package/dist/ui/components/candlestick-chart/index.ts +3 -4
- package/dist/ui/components/chart/chart-focus.test.ts +132 -0
- package/dist/ui/components/chart/chart-focus.ts +115 -0
- package/dist/ui/components/chart/chart-presets.test.ts +139 -0
- package/dist/ui/components/chart/chart-presets.ts +271 -0
- package/dist/ui/components/chart/chart-render.test.ts +730 -0
- package/dist/ui/components/chart/chart-scene.test.ts +589 -0
- package/dist/ui/components/chart/chart-scene.ts +483 -0
- package/dist/ui/components/chart/chart-theme.test.ts +85 -0
- package/dist/ui/components/chart/chart-theme.ts +198 -0
- package/dist/ui/components/chart/chart-zoom.test.ts +159 -0
- package/dist/ui/components/chart/chart-zoom.ts +162 -0
- package/dist/ui/components/chart/chart.ts +921 -0
- package/dist/ui/components/chart/index.ts +24 -0
- package/dist/ui/components/dialog/dialog.test.ts +49 -2
- package/dist/ui/components/dialog/dialog.ts +43 -12
- package/dist/ui/components/icon/icon-theme.test.ts +133 -0
- package/dist/ui/components/icon/icon-theme.ts +204 -0
- package/dist/ui/components/icon/icon.test.ts +51 -2
- package/dist/ui/components/icon/icon.ts +38 -21
- package/dist/ui/components/line-chart/index.ts +2 -3
- package/dist/ui/components/line-chart/line-chart-render.test.ts +69 -107
- package/dist/ui/components/line-chart/line-chart.test.ts +63 -45
- package/dist/ui/components/line-chart/line-chart.ts +83 -241
- package/dist/ui/components/list/list.test.ts +69 -2
- package/dist/ui/components/list/list.ts +21 -15
- package/dist/ui/components/nav-drawer/nav-drawer.test.ts +80 -1
- package/dist/ui/components/nav-drawer/nav-drawer.ts +66 -11
- package/dist/ui/components/pagination/pagination.test.ts +62 -0
- package/dist/ui/components/pagination/pagination.ts +55 -8
- package/dist/ui/components/scatter-chart/index.ts +2 -3
- package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +51 -144
- package/dist/ui/components/scatter-chart/scatter-chart.test.ts +57 -25
- package/dist/ui/components/scatter-chart/scatter-chart.ts +104 -250
- package/dist/ui/components/scroll-area/scroll-area.test.ts +37 -2
- package/dist/ui/components/scroll-area/scroll-area.ts +8 -8
- package/dist/ui/components/spinner/spinner.test.ts +27 -0
- package/dist/ui/components/spinner/spinner.ts +33 -23
- package/dist/ui/components/toast/toast.test.ts +37 -0
- package/dist/ui/components/toast/toast.ts +20 -4
- package/dist/ui/theme/chart-colors.test.ts +30 -0
- package/dist/ui/theme/dark.css +8 -0
- package/dist/ui/theme/default.css +8 -0
- package/dist/ui/theme/icon-colors.test.ts +30 -0
- package/dist/ui/theme/tailwind-plugin.ts +10 -1
- package/dist/utils/installed.d.ts +17 -0
- package/dist/utils/installed.js +27 -0
- package/dist/utils/installed.js.map +1 -0
- package/dist/utils/installed.test.d.ts +1 -0
- package/dist/utils/installed.test.js +41 -0
- package/dist/utils/installed.test.js.map +1 -0
- package/dist/utils/interactive.d.ts +9 -0
- package/dist/utils/interactive.js +10 -0
- package/dist/utils/interactive.js.map +1 -0
- package/dist/utils/interactive.test.d.ts +1 -0
- package/dist/utils/interactive.test.js +24 -0
- package/dist/utils/interactive.test.js.map +1 -0
- package/dist/utils/packages.d.ts +36 -0
- package/dist/utils/packages.js +70 -0
- package/dist/utils/packages.js.map +1 -0
- package/dist/utils/packages.test.d.ts +1 -0
- package/dist/utils/packages.test.js +89 -0
- package/dist/utils/packages.test.js.map +1 -0
- package/dist/utils/resolve-paths.d.ts +17 -1
- package/dist/utils/resolve-paths.js +23 -3
- package/dist/utils/resolve-paths.js.map +1 -1
- package/dist/utils/resolve-paths.test.js +34 -2
- package/dist/utils/resolve-paths.test.js.map +1 -1
- package/package.json +5 -1
- package/dist/ui/components/cartesian-chart/cartesian-chart-render.test.ts +0 -188
- package/dist/ui/components/cartesian-chart/cartesian-chart.test.ts +0 -525
- package/dist/ui/components/cartesian-chart/cartesian-chart.ts +0 -1140
- package/dist/ui/components/cartesian-chart/index.ts +0 -18
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type ElementRef,
|
|
3
|
+
type OnDestroy,
|
|
3
4
|
Component,
|
|
4
5
|
ViewEncapsulation,
|
|
5
6
|
computed,
|
|
@@ -28,10 +29,10 @@ import {
|
|
|
28
29
|
} from '../../utils/animate';
|
|
29
30
|
import { cn } from '../../utils/cn';
|
|
30
31
|
import { UiIcon } from '../icon';
|
|
32
|
+
import { UI_ICON_COLOR } from '../icon/icon-theme';
|
|
31
33
|
|
|
32
34
|
@Component({
|
|
33
35
|
selector: 'ui-nav-drawer',
|
|
34
|
-
standalone: true,
|
|
35
36
|
imports: [LYNX_ELEMENTS],
|
|
36
37
|
encapsulation: ViewEncapsulation.None,
|
|
37
38
|
template: `
|
|
@@ -52,6 +53,7 @@ import { UiIcon } from '../icon';
|
|
|
52
53
|
#panel
|
|
53
54
|
[class]="panelClass()"
|
|
54
55
|
[style]="panelPositionStyle()"
|
|
56
|
+
[accessibility-exclusive-focus]="overlayVisible()"
|
|
55
57
|
(catchtap)="onPanelTap()"
|
|
56
58
|
>
|
|
57
59
|
<ng-content />
|
|
@@ -60,7 +62,7 @@ import { UiIcon } from '../icon';
|
|
|
60
62
|
</overlay>
|
|
61
63
|
`,
|
|
62
64
|
})
|
|
63
|
-
export class UiNavDrawer {
|
|
65
|
+
export class UiNavDrawer implements OnDestroy {
|
|
64
66
|
readonly open = model(false);
|
|
65
67
|
readonly side = input<'left' | 'right'>('left');
|
|
66
68
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
@@ -85,6 +87,9 @@ export class UiNavDrawer {
|
|
|
85
87
|
#backdropAnim?: AnimationHandle;
|
|
86
88
|
#panelAnim?: AnimationHandle;
|
|
87
89
|
#hasBeenOpen = false;
|
|
90
|
+
// The open and close sequences' pending timers, so ngOnDestroy can stop
|
|
91
|
+
// them.
|
|
92
|
+
readonly #timers = new Set<ReturnType<typeof setTimeout>>();
|
|
88
93
|
|
|
89
94
|
constructor() {
|
|
90
95
|
// `#hasBeenOpen` prevents the close animation from running on the initial
|
|
@@ -135,6 +140,28 @@ export class UiNavDrawer {
|
|
|
135
140
|
this.open.set(false);
|
|
136
141
|
}
|
|
137
142
|
|
|
143
|
+
/**
|
|
144
|
+
* The drawer can be destroyed while its open or close sequence is still
|
|
145
|
+
* running, for example by an `@if` around it or a route change right after
|
|
146
|
+
* closing it. The rest of the sequence then ran on the destroyed component:
|
|
147
|
+
* it animated detached elements and emitted `closed` from a destroyed
|
|
148
|
+
* output, which Angular warns about in dev builds (NG0953).
|
|
149
|
+
*/
|
|
150
|
+
ngOnDestroy(): void {
|
|
151
|
+
for (const timer of this.#timers) clearTimeout(timer);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* `setTimeout`, tracked so that ngOnDestroy can cancel it.
|
|
156
|
+
*/
|
|
157
|
+
#later(callback: () => void, ms: number): void {
|
|
158
|
+
const timer = setTimeout(() => {
|
|
159
|
+
this.#timers.delete(timer);
|
|
160
|
+
callback();
|
|
161
|
+
}, ms);
|
|
162
|
+
this.#timers.add(timer);
|
|
163
|
+
}
|
|
164
|
+
|
|
138
165
|
/**
|
|
139
166
|
* Two-phase open: first make the overlay visible (so native elements exist),
|
|
140
167
|
* then animate in on the next frame. Without the nested setTimeout, the
|
|
@@ -142,9 +169,9 @@ export class UiNavDrawer {
|
|
|
142
169
|
* yet — Lynx element.animate() requires the element to be in the tree.
|
|
143
170
|
*/
|
|
144
171
|
#doOpen(): void {
|
|
145
|
-
|
|
172
|
+
this.#later(() => {
|
|
146
173
|
this.overlayVisible.set(true);
|
|
147
|
-
|
|
174
|
+
this.#later(() => this.#animateIn(), 0);
|
|
148
175
|
}, 0);
|
|
149
176
|
}
|
|
150
177
|
|
|
@@ -155,9 +182,9 @@ export class UiNavDrawer {
|
|
|
155
182
|
* cause a visual glitch (elements disappear before fade completes).
|
|
156
183
|
*/
|
|
157
184
|
#doClose(): void {
|
|
158
|
-
|
|
185
|
+
this.#later(() => {
|
|
159
186
|
this.#animateOut();
|
|
160
|
-
|
|
187
|
+
this.#later(() => {
|
|
161
188
|
this.overlayVisible.set(false);
|
|
162
189
|
this.closed.emit();
|
|
163
190
|
}, DURATION.normal + 20);
|
|
@@ -178,6 +205,11 @@ export class UiNavDrawer {
|
|
|
178
205
|
duration: DURATION.slow,
|
|
179
206
|
easing: EASING.sheet,
|
|
180
207
|
});
|
|
208
|
+
// Move a screen reader into the panel. It would otherwise stay on whatever
|
|
209
|
+
// opened it, which is now behind the overlay. With the panel's
|
|
210
|
+
// accessibility-exclusive-focus (see the template), it then can't leave
|
|
211
|
+
// the panel until it closes.
|
|
212
|
+
panel.invoke?.('requestAccessibilityFocus');
|
|
181
213
|
}
|
|
182
214
|
|
|
183
215
|
#animateOut(): void {
|
|
@@ -199,7 +231,6 @@ export class UiNavDrawer {
|
|
|
199
231
|
|
|
200
232
|
@Component({
|
|
201
233
|
selector: 'ui-nav-drawer-header',
|
|
202
|
-
standalone: true,
|
|
203
234
|
imports: [LYNX_ELEMENTS],
|
|
204
235
|
encapsulation: ViewEncapsulation.None,
|
|
205
236
|
template: `
|
|
@@ -229,7 +260,6 @@ export class UiNavDrawerHeader {
|
|
|
229
260
|
|
|
230
261
|
@Component({
|
|
231
262
|
selector: 'ui-nav-drawer-content',
|
|
232
|
-
standalone: true,
|
|
233
263
|
imports: [LYNX_ELEMENTS],
|
|
234
264
|
encapsulation: ViewEncapsulation.None,
|
|
235
265
|
// Fixes: a long item list scrolled nowhere and spilled over the header and
|
|
@@ -326,9 +356,21 @@ export class UiNavDrawerContent {
|
|
|
326
356
|
|
|
327
357
|
@Component({
|
|
328
358
|
selector: 'ui-nav-drawer-item',
|
|
329
|
-
standalone: true,
|
|
330
359
|
imports: [LYNX_ELEMENTS],
|
|
331
360
|
encapsulation: ViewEncapsulation.None,
|
|
361
|
+
// Icons projected into the item (see the variant below) inject this through
|
|
362
|
+
// the item's element injector, which content projection keeps them under.
|
|
363
|
+
providers: [
|
|
364
|
+
{
|
|
365
|
+
provide: UI_ICON_COLOR,
|
|
366
|
+
useFactory: () => {
|
|
367
|
+
const item = inject(UiNavDrawerItem);
|
|
368
|
+
return computed(() =>
|
|
369
|
+
item.variant() === 'destructive' ? 'var(--destructive)' : undefined,
|
|
370
|
+
);
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
],
|
|
332
374
|
template: `
|
|
333
375
|
<view
|
|
334
376
|
#container
|
|
@@ -358,10 +400,18 @@ export class UiNavDrawerItem {
|
|
|
358
400
|
readonly containerRef = viewChild<ElementRef>('container');
|
|
359
401
|
#pressAnim?: AnimationHandle;
|
|
360
402
|
|
|
403
|
+
// `destructive` recolors the item's content, which the item doesn't render
|
|
404
|
+
// itself: the label and icon are projected. Lynx doesn't inherit `color`, so
|
|
405
|
+
// a text class here wouldn't reach the label. CSS variables do inherit, so
|
|
406
|
+
// the item points `--foreground` at `--destructive` for everything inside
|
|
407
|
+
// it: a label styled `text-foreground` (as items are) turns destructive.
|
|
408
|
+
// Icons draw with SVG, which can't read CSS, so they get the same color
|
|
409
|
+
// through UI_ICON_COLOR (see providers above).
|
|
361
410
|
protected readonly containerClass = computed(() =>
|
|
362
411
|
cn(
|
|
363
412
|
'flex-row items-center gap-3 px-4 py-3 flex',
|
|
364
413
|
this.active() ? 'bg-accent' : 'bg-transparent',
|
|
414
|
+
this.variant() === 'destructive' && '[--foreground:var(--destructive)]',
|
|
365
415
|
this.userClass(),
|
|
366
416
|
),
|
|
367
417
|
);
|
|
@@ -421,7 +471,6 @@ export class UiNavDrawerItem {
|
|
|
421
471
|
|
|
422
472
|
@Component({
|
|
423
473
|
selector: 'ui-nav-drawer-footer',
|
|
424
|
-
standalone: true,
|
|
425
474
|
imports: [LYNX_ELEMENTS],
|
|
426
475
|
encapsulation: ViewEncapsulation.None,
|
|
427
476
|
template: `
|
|
@@ -449,13 +498,17 @@ export class UiNavDrawerFooter {
|
|
|
449
498
|
|
|
450
499
|
@Component({
|
|
451
500
|
selector: 'ui-nav-drawer-trigger',
|
|
452
|
-
standalone: true,
|
|
453
501
|
imports: [LYNX_ELEMENTS, UiIcon],
|
|
454
502
|
encapsulation: ViewEncapsulation.None,
|
|
455
503
|
template: `
|
|
504
|
+
<!-- The trigger is only an icon, so it carries the words a screen reader
|
|
505
|
+
says for it. -->
|
|
456
506
|
<view
|
|
457
507
|
#container
|
|
458
508
|
[class]="containerClass()"
|
|
509
|
+
[accessibility-element]="true"
|
|
510
|
+
accessibility-traits="button"
|
|
511
|
+
[accessibility-label]="label()"
|
|
459
512
|
(bindtouchstart)="onPressStart()"
|
|
460
513
|
(bindtouchend)="onPressEnd()"
|
|
461
514
|
(bindtouchcancel)="onPressCancel()"
|
|
@@ -469,6 +522,8 @@ export class UiNavDrawerFooter {
|
|
|
469
522
|
`,
|
|
470
523
|
})
|
|
471
524
|
export class UiNavDrawerTrigger {
|
|
525
|
+
/** What a screen reader says for the trigger. */
|
|
526
|
+
readonly label = input('Open navigation menu');
|
|
472
527
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
473
528
|
|
|
474
529
|
readonly pressed = output<void>();
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
3
|
import { setInputSignal } from '../../../test-utils/set-input-signal';
|
|
4
|
+
import { ICON_LIGHT_COLORS } from '../icon/icon-theme';
|
|
4
5
|
import { UiPagination } from './pagination';
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* The screen reader labels of the row's buttons, in order.
|
|
9
|
+
*/
|
|
10
|
+
const labels = (container: Element): (string | null)[] =>
|
|
11
|
+
Array.from(container.querySelectorAll('[accessibility-element]')).map((el) =>
|
|
12
|
+
el.getAttribute('accessibility-label'),
|
|
13
|
+
);
|
|
14
|
+
|
|
6
15
|
describe('UiPagination', () => {
|
|
7
16
|
const setup = async (totalPages: number, page = 1, siblingCount = 1) => {
|
|
8
17
|
const { componentRef, container } = await render(UiPagination);
|
|
@@ -93,4 +102,57 @@ describe('UiPagination', () => {
|
|
|
93
102
|
);
|
|
94
103
|
expect(primary).toBeTruthy();
|
|
95
104
|
});
|
|
105
|
+
|
|
106
|
+
it('labels every button for screen readers', async () => {
|
|
107
|
+
const { container } = await setup(3, 2);
|
|
108
|
+
expect(labels(container)).toEqual([
|
|
109
|
+
'Previous page',
|
|
110
|
+
'Page 1',
|
|
111
|
+
'Page 2, current page',
|
|
112
|
+
'Page 3',
|
|
113
|
+
'Next page',
|
|
114
|
+
]);
|
|
115
|
+
const buttons = container.querySelectorAll('[accessibility-element]');
|
|
116
|
+
for (const button of Array.from(buttons)) {
|
|
117
|
+
expect(button.getAttribute('accessibility-traits')).toBe('button');
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('takes its own labels', async () => {
|
|
122
|
+
const { inst, container } = await setup(2, 1);
|
|
123
|
+
setInputSignal(inst.previousLabel, 'Back');
|
|
124
|
+
setInputSignal(inst.nextLabel, 'Forward');
|
|
125
|
+
setInputSignal(inst.pageLabel, (page: number, current: boolean) =>
|
|
126
|
+
current ? `Results ${page}, showing` : `Results ${page}`,
|
|
127
|
+
);
|
|
128
|
+
await waitForUpdate();
|
|
129
|
+
expect(labels(container)).toEqual([
|
|
130
|
+
'Back',
|
|
131
|
+
'Results 1, showing',
|
|
132
|
+
'Results 2',
|
|
133
|
+
'Forward',
|
|
134
|
+
]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('hides the gaps from screen readers', async () => {
|
|
138
|
+
const { container } = await setup(10, 5);
|
|
139
|
+
const gaps = Array.from(container.querySelectorAll('text'))
|
|
140
|
+
.filter((el) => el.textContent === '...')
|
|
141
|
+
.map((el) =>
|
|
142
|
+
el.parentElement?.getAttribute('accessibility-elements-hidden'),
|
|
143
|
+
);
|
|
144
|
+
expect(gaps).toEqual(['true', 'true']);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("draws the arrows in the theme's foreground", async () => {
|
|
148
|
+
const { container } = await setup(3, 2);
|
|
149
|
+
const arrows = Array.from(container.querySelectorAll('svg')).map((el) =>
|
|
150
|
+
el.getAttribute('content'),
|
|
151
|
+
);
|
|
152
|
+
expect(arrows).toHaveLength(2);
|
|
153
|
+
for (const arrow of arrows) {
|
|
154
|
+
expect(arrow).toContain(ICON_LIGHT_COLORS['foreground']);
|
|
155
|
+
expect(arrow).not.toContain('currentColor');
|
|
156
|
+
}
|
|
157
|
+
});
|
|
96
158
|
});
|
|
@@ -8,20 +8,40 @@ import {
|
|
|
8
8
|
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
9
9
|
|
|
10
10
|
import { cn } from '../../utils/cn';
|
|
11
|
+
import { injectIconColor } from '../icon/icon-theme';
|
|
11
12
|
|
|
12
13
|
const CHEVRON_LEFT = `<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="m15 18-6-6 6-6"/></svg>`;
|
|
13
14
|
const CHEVRON_RIGHT = `<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="m9 18 6-6-6-6"/></svg>`;
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* What a screen reader says for a page button, unless `pageLabel` is set.
|
|
18
|
+
*/
|
|
19
|
+
const defaultPageLabel = (page: number, current: boolean): string =>
|
|
20
|
+
current ? `Page ${page}, current page` : `Page ${page}`;
|
|
21
|
+
|
|
15
22
|
@Component({
|
|
16
23
|
selector: 'ui-pagination',
|
|
17
|
-
standalone: true,
|
|
18
24
|
imports: [LYNX_ELEMENTS],
|
|
19
25
|
encapsulation: ViewEncapsulation.None,
|
|
26
|
+
// Each button is one accessibility element with a label: without them a
|
|
27
|
+
// screen reader found the arrows unlabeled (they're only an SVG) and read
|
|
28
|
+
// each number as plain text, with nothing saying it's a button or which page
|
|
29
|
+
// is current. The "..." gaps are hidden, since they aren't controls.
|
|
30
|
+
//
|
|
31
|
+
// The chevrons are drawn in the theme's foreground, like the page numbers
|
|
32
|
+
// next to them. An SVG can't read the text color classes, so the color is
|
|
33
|
+
// resolved and written into the markup (see icon-theme.ts).
|
|
20
34
|
template: `
|
|
21
35
|
<view [class]="containerClass()">
|
|
22
|
-
<view
|
|
36
|
+
<view
|
|
37
|
+
[class]="navButtonClass(page() <= 1)"
|
|
38
|
+
[accessibility-element]="true"
|
|
39
|
+
accessibility-traits="button"
|
|
40
|
+
[accessibility-label]="previousLabel()"
|
|
41
|
+
(bindtap)="prev()"
|
|
42
|
+
>
|
|
23
43
|
<svg
|
|
24
|
-
[attr.content]="chevronLeftSvg"
|
|
44
|
+
[attr.content]="chevronLeftSvg()"
|
|
25
45
|
style="width: 16px; height: 16px;"
|
|
26
46
|
/>
|
|
27
47
|
</view>
|
|
@@ -30,6 +50,11 @@ const CHEVRON_RIGHT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 2
|
|
|
30
50
|
@if (item.type === 'page') {
|
|
31
51
|
<view
|
|
32
52
|
[class]="pageButtonClass(item.value === page())"
|
|
53
|
+
[accessibility-element]="true"
|
|
54
|
+
accessibility-traits="button"
|
|
55
|
+
[accessibility-label]="
|
|
56
|
+
pageLabel()(item.value, item.value === page())
|
|
57
|
+
"
|
|
33
58
|
(bindtap)="goTo(item.value)"
|
|
34
59
|
>
|
|
35
60
|
<text [class]="pageTextClass(item.value === page())">{{
|
|
@@ -37,15 +62,24 @@ const CHEVRON_RIGHT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 2
|
|
|
37
62
|
}}</text>
|
|
38
63
|
</view>
|
|
39
64
|
} @else {
|
|
40
|
-
<view
|
|
65
|
+
<view
|
|
66
|
+
class="h-8 w-8 items-center flex justify-center"
|
|
67
|
+
[accessibility-elements-hidden]="true"
|
|
68
|
+
>
|
|
41
69
|
<text class="text-xs text-muted-foreground">...</text>
|
|
42
70
|
</view>
|
|
43
71
|
}
|
|
44
72
|
}
|
|
45
73
|
|
|
46
|
-
<view
|
|
74
|
+
<view
|
|
75
|
+
[class]="navButtonClass(page() >= totalPages())"
|
|
76
|
+
[accessibility-element]="true"
|
|
77
|
+
accessibility-traits="button"
|
|
78
|
+
[accessibility-label]="nextLabel()"
|
|
79
|
+
(bindtap)="next()"
|
|
80
|
+
>
|
|
47
81
|
<svg
|
|
48
|
-
[attr.content]="chevronRightSvg"
|
|
82
|
+
[attr.content]="chevronRightSvg()"
|
|
49
83
|
style="width: 16px; height: 16px;"
|
|
50
84
|
/>
|
|
51
85
|
</view>
|
|
@@ -56,10 +90,23 @@ export class UiPagination {
|
|
|
56
90
|
readonly page = model(1);
|
|
57
91
|
readonly totalPages = input.required<number>();
|
|
58
92
|
readonly siblingCount = input(1);
|
|
93
|
+
/** Screen reader label for the previous arrow. */
|
|
94
|
+
readonly previousLabel = input('Previous page');
|
|
95
|
+
/** Screen reader label for the next arrow. */
|
|
96
|
+
readonly nextLabel = input('Next page');
|
|
97
|
+
/** Screen reader label for a page button, given its page and whether it's current. */
|
|
98
|
+
readonly pageLabel =
|
|
99
|
+
input<(page: number, current: boolean) => string>(defaultPageLabel);
|
|
59
100
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
60
101
|
|
|
61
|
-
|
|
62
|
-
|
|
102
|
+
readonly #resolveColor = injectIconColor();
|
|
103
|
+
// split/join, not replaceAll(): Lynx's main thread (PrimJS, ES2019) lacks it.
|
|
104
|
+
protected readonly chevronLeftSvg = computed(() =>
|
|
105
|
+
CHEVRON_LEFT.split('currentColor').join(this.#resolveColor(undefined)),
|
|
106
|
+
);
|
|
107
|
+
protected readonly chevronRightSvg = computed(() =>
|
|
108
|
+
CHEVRON_RIGHT.split('currentColor').join(this.#resolveColor(undefined)),
|
|
109
|
+
);
|
|
63
110
|
|
|
64
111
|
// Sliding-window pagination algorithm:
|
|
65
112
|
// Always shows page 1 and the last page. Around the current page, shows
|
|
@@ -1,157 +1,64 @@
|
|
|
1
|
-
import { render
|
|
1
|
+
import { render } from '@blotch/angular-lynx-testing-library';
|
|
2
|
+
import { createChartRuntime } from '@tanstack/charts/runtime';
|
|
3
|
+
import type {
|
|
4
|
+
ChartDefinition,
|
|
5
|
+
ChartPoint,
|
|
6
|
+
SceneDot,
|
|
7
|
+
SceneNode,
|
|
8
|
+
} from '@tanstack/charts/types';
|
|
2
9
|
import { describe, expect, it, vi } from 'vitest';
|
|
3
10
|
import { setInputSignal } from '../../../test-utils/set-input-signal';
|
|
4
|
-
import {
|
|
5
|
-
import { UiScatterChart, UiScatterSeries } from './scatter-chart';
|
|
11
|
+
import { UiScatterChart } from './scatter-chart';
|
|
6
12
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
const data = [
|
|
14
|
+
{ x: 150, y: 50 },
|
|
15
|
+
{ x: 160, y: 65 },
|
|
16
|
+
{ x: 170, y: 70 },
|
|
17
|
+
];
|
|
12
18
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
]);
|
|
21
|
-
await waitForUpdate();
|
|
22
|
-
const cast = inst as unknown as {
|
|
23
|
-
xAxis: () => { domain: readonly [number, number] };
|
|
24
|
-
yAxis: () => { domain: readonly [number, number] };
|
|
25
|
-
effectivePadding: () => {
|
|
26
|
-
top?: number;
|
|
27
|
-
right?: number;
|
|
28
|
-
bottom?: number;
|
|
29
|
-
left?: number;
|
|
30
|
-
};
|
|
31
|
-
};
|
|
32
|
-
expect(cast.xAxis().domain[0]).toBeLessThanOrEqual(0);
|
|
33
|
-
expect(cast.xAxis().domain[1]).toBeGreaterThanOrEqual(10);
|
|
34
|
-
expect(cast.yAxis().domain[0]).toBeLessThanOrEqual(0);
|
|
35
|
-
expect(cast.yAxis().domain[1]).toBeGreaterThanOrEqual(8);
|
|
36
|
-
// Default SCATTER_INSET on every side.
|
|
37
|
-
expect(cast.effectivePadding().top).toBeCloseTo(0.04);
|
|
38
|
-
expect(cast.effectivePadding().right).toBeCloseTo(0.04);
|
|
39
|
-
expect(cast.effectivePadding().bottom).toBeCloseTo(0.04);
|
|
40
|
-
expect(cast.effectivePadding().left).toBeCloseTo(0.04);
|
|
19
|
+
/**
|
|
20
|
+
* Every dot in a definition's scene.
|
|
21
|
+
*/
|
|
22
|
+
const dots = (definition: ChartDefinition<any, any, any>) => {
|
|
23
|
+
const scene = createChartRuntime().render(definition, {
|
|
24
|
+
width: 300,
|
|
25
|
+
height: 200,
|
|
41
26
|
});
|
|
27
|
+
const found: SceneDot[] = [];
|
|
28
|
+
const walk = (nodes: readonly SceneNode[]) => {
|
|
29
|
+
for (const node of nodes) {
|
|
30
|
+
if (node.kind === 'group') {
|
|
31
|
+
if (!node.focus) walk(node.children);
|
|
32
|
+
} else if (node.kind === 'dot') found.push(node);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
walk(scene.nodes);
|
|
36
|
+
return { scene, found };
|
|
37
|
+
};
|
|
42
38
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
{ x: 10, y: 10 },
|
|
49
|
-
]);
|
|
50
|
-
setInputSignal(inst.xMin, -5);
|
|
51
|
-
setInputSignal(inst.xMax, 20);
|
|
52
|
-
setInputSignal(inst.yMin, -1);
|
|
53
|
-
setInputSignal(inst.yMax, 15);
|
|
54
|
-
await waitForUpdate();
|
|
55
|
-
const cast = inst as unknown as {
|
|
56
|
-
xAxis: () => { domain: readonly [number, number] };
|
|
57
|
-
yAxis: () => { domain: readonly [number, number] };
|
|
58
|
-
};
|
|
59
|
-
expect(cast.xAxis().domain[0]).toBeLessThanOrEqual(-5);
|
|
60
|
-
expect(cast.xAxis().domain[1]).toBeGreaterThanOrEqual(20);
|
|
61
|
-
expect(cast.yAxis().domain[0]).toBeLessThanOrEqual(-1);
|
|
62
|
-
expect(cast.yAxis().domain[1]).toBeGreaterThanOrEqual(15);
|
|
63
|
-
});
|
|
39
|
+
describe('UiScatterChart', () => {
|
|
40
|
+
type Internals = {
|
|
41
|
+
definition: () => ChartDefinition<any, any, any>;
|
|
42
|
+
onSelect(point: ChartPoint | null): void;
|
|
43
|
+
};
|
|
64
44
|
|
|
65
|
-
it('
|
|
45
|
+
it('builds its definition from its inputs', async () => {
|
|
66
46
|
const { componentRef } = await render(UiScatterChart);
|
|
67
|
-
const
|
|
68
|
-
setInputSignal(
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
top?: number;
|
|
73
|
-
right?: number;
|
|
74
|
-
bottom?: number;
|
|
75
|
-
left?: number;
|
|
76
|
-
};
|
|
77
|
-
};
|
|
78
|
-
expect(cast.effectivePadding().top).toBeCloseTo(0.2);
|
|
79
|
-
expect(cast.effectivePadding().left).toBeCloseTo(0.04);
|
|
47
|
+
const chart = componentRef.instance as UiScatterChart;
|
|
48
|
+
setInputSignal(chart.data, data);
|
|
49
|
+
setInputSignal(chart.radius, 7);
|
|
50
|
+
const { found } = dots((chart as unknown as Internals).definition());
|
|
51
|
+
expect(found[0]!.radius).toBe(7);
|
|
80
52
|
});
|
|
81
53
|
|
|
82
|
-
it('
|
|
54
|
+
it('emits the tapped datum', async () => {
|
|
83
55
|
const { componentRef } = await render(UiScatterChart);
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const renderSeries = async () => {
|
|
92
|
-
const parentResult = await render(UiCartesianChart);
|
|
93
|
-
const parent = parentResult.componentRef.instance as UiCartesianChart;
|
|
94
|
-
setInputSignal(parent.xDomain, [0, 10]);
|
|
95
|
-
setInputSignal(parent.yDomain, [0, 10]);
|
|
96
|
-
await waitForUpdate();
|
|
97
|
-
const seriesResult = await render(UiScatterSeries, {
|
|
98
|
-
providers: [{ provide: UiCartesianChart, useValue: parent }],
|
|
99
|
-
});
|
|
100
|
-
const series = seriesResult.componentRef.instance as UiScatterSeries;
|
|
101
|
-
setInputSignal(series.data, [
|
|
102
|
-
{ x: 0, y: 0 },
|
|
103
|
-
{ x: 5, y: 5 },
|
|
104
|
-
{ x: 10, y: 10 },
|
|
105
|
-
]);
|
|
106
|
-
await waitForUpdate();
|
|
107
|
-
return { parent, series };
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
it('renders a circular dot per datum with the fallback color', async () => {
|
|
111
|
-
const { series } = await renderSeries();
|
|
112
|
-
const cast = series as unknown as {
|
|
113
|
-
dots: () => { style: string; point: { x: number; y: number } }[];
|
|
114
|
-
};
|
|
115
|
-
const dots = cast.dots();
|
|
116
|
-
expect(dots.length).toBe(3);
|
|
117
|
-
for (const d of dots) {
|
|
118
|
-
expect(d.style).toContain('background-color: var(--primary)');
|
|
119
|
-
expect(d.style).toContain('border-radius');
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('cycles through the per-dot palette when colors are provided', async () => {
|
|
124
|
-
const { series } = await renderSeries();
|
|
125
|
-
setInputSignal(series.colors, ['#f00', '#0f0']);
|
|
126
|
-
await waitForUpdate();
|
|
127
|
-
const cast = series as unknown as { dots: () => { style: string }[] };
|
|
128
|
-
const dots = cast.dots();
|
|
129
|
-
expect(dots[0].style).toContain('#f00');
|
|
130
|
-
expect(dots[1].style).toContain('#0f0');
|
|
131
|
-
expect(dots[2].style).toContain('#f00');
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('per-dot sizes drive per-dot radii for a bubble chart', async () => {
|
|
135
|
-
const { series } = await renderSeries();
|
|
136
|
-
setInputSignal(series.sizes, [10, 6]);
|
|
137
|
-
await waitForUpdate();
|
|
138
|
-
const cast = series as unknown as { dots: () => { style: string }[] };
|
|
139
|
-
const dots = cast.dots();
|
|
140
|
-
// radius 10 → width 20px
|
|
141
|
-
expect(dots[0].style).toContain('width: 20.00px');
|
|
142
|
-
// radius 6 → width 12px
|
|
143
|
-
expect(dots[1].style).toContain('width: 12.00px');
|
|
144
|
-
// No entry → falls back to uniform radius (default 4) → width 8px
|
|
145
|
-
expect(dots[2].style).toContain('width: 8.00px');
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('emits pointTap with the source point when a dot is tapped', async () => {
|
|
149
|
-
const { series } = await renderSeries();
|
|
150
|
-
const cb = vi.fn();
|
|
151
|
-
series.pointTap.subscribe(cb);
|
|
152
|
-
(
|
|
153
|
-
series as unknown as { onPointTap: (p: { x: number; y: number }) => void }
|
|
154
|
-
).onPointTap({ x: 3, y: 4 });
|
|
155
|
-
expect(cb).toHaveBeenCalledWith({ x: 3, y: 4 });
|
|
56
|
+
const chart = componentRef.instance as UiScatterChart;
|
|
57
|
+
const tapped = vi.fn();
|
|
58
|
+
chart.pointTap.subscribe(tapped);
|
|
59
|
+
const internals = chart as unknown as Internals;
|
|
60
|
+
internals.onSelect({ datum: data[2] } as ChartPoint);
|
|
61
|
+
internals.onSelect(null);
|
|
62
|
+
expect(tapped.mock.calls).toEqual([[data[2]]]);
|
|
156
63
|
});
|
|
157
64
|
});
|