@sentropic/design-system-svelte 0.34.25 → 0.34.27
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/AreaChart.svelte +87 -0
- package/dist/AreaChart.svelte.d.ts +15 -0
- package/dist/AreaChart.svelte.d.ts.map +1 -1
- package/dist/BarChart.svelte +83 -0
- package/dist/BarChart.svelte.d.ts +15 -0
- package/dist/BarChart.svelte.d.ts.map +1 -1
- package/dist/ComboChart.svelte +89 -32
- package/dist/ComboChart.svelte.d.ts +9 -0
- package/dist/ComboChart.svelte.d.ts.map +1 -1
- package/dist/LineChart.svelte +87 -0
- package/dist/LineChart.svelte.d.ts +15 -0
- package/dist/LineChart.svelte.d.ts.map +1 -1
- package/dist/StackedBarChart.svelte +62 -16
- package/dist/StackedBarChart.svelte.d.ts +10 -0
- package/dist/StackedBarChart.svelte.d.ts.map +1 -1
- package/dist/chartKeyboardNav.d.ts +29 -0
- package/dist/chartKeyboardNav.d.ts.map +1 -0
- package/dist/chartKeyboardNav.js +69 -0
- package/package.json +1 -1
package/dist/AreaChart.svelte
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
} from "./chartAnnotations.js";
|
|
26
26
|
import { formatDataLabel, normalizeDataLabels, type DataLabelsProp } from "./chartDataLabels.js";
|
|
27
27
|
import { keyForX, resolveActiveIndex } from "./chartCrosshair.js";
|
|
28
|
+
import { datapointAriaLabel, datapointNavAction, rovingTabIndex } from "./chartKeyboardNav.js";
|
|
28
29
|
|
|
29
30
|
type AreaChartProps = {
|
|
30
31
|
data: (number | AreaChartDatum)[];
|
|
@@ -61,6 +62,21 @@
|
|
|
61
62
|
* keep the shared hover channel in sync.
|
|
62
63
|
*/
|
|
63
64
|
onHoverKeyChange?: (key: string | null) => void;
|
|
65
|
+
/**
|
|
66
|
+
* FR-5 — keyboard navigation of the data points (roving tabindex). When `true`
|
|
67
|
+
* (or implied by wiring `onSelectKey`), a thin focusable overlay is rendered
|
|
68
|
+
* over the points: the chart owns ONE tab stop, ←/↑/→/↓ move the focus between
|
|
69
|
+
* points (data order), Home/End jump to the first/last, Enter/Space select the
|
|
70
|
+
* focused point (`onSelectKey`), Escape leaves the navigation. Each focused
|
|
71
|
+
* point announces its `x` + value. Absent ⇒ no overlay, rendering unchanged.
|
|
72
|
+
*/
|
|
73
|
+
keyboardNav?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Emitted when the user selects the focused point via Enter/Space (its key,
|
|
76
|
+
* `String(x)`), or `null` when the navigation is left via Escape. Wiring it
|
|
77
|
+
* also turns the keyboard navigation on.
|
|
78
|
+
*/
|
|
79
|
+
onSelectKey?: (key: string | null) => void;
|
|
64
80
|
class?: string;
|
|
65
81
|
};
|
|
66
82
|
|
|
@@ -75,6 +91,8 @@
|
|
|
75
91
|
dataLabels,
|
|
76
92
|
hoverKey,
|
|
77
93
|
onHoverKeyChange,
|
|
94
|
+
keyboardNav,
|
|
95
|
+
onSelectKey,
|
|
78
96
|
class: className
|
|
79
97
|
}: AreaChartProps = $props();
|
|
80
98
|
|
|
@@ -133,6 +151,9 @@
|
|
|
133
151
|
]);
|
|
134
152
|
|
|
135
153
|
let hoveredIndex: number | null = $state(null);
|
|
154
|
+
// FR-5 — roving keyboard focus over the data points (separate from hover).
|
|
155
|
+
let focusedIndex: number = $state(-1);
|
|
156
|
+
let datapointRefs: Array<SVGRectElement | null> = [];
|
|
136
157
|
|
|
137
158
|
const plotWidth = $derived(Math.max(width - MARGIN.left - MARGIN.right, 1));
|
|
138
159
|
const plotHeight = $derived(Math.max(height - MARGIN.top - MARGIN.bottom, 1));
|
|
@@ -316,6 +337,35 @@
|
|
|
316
337
|
// provided (resolved against `hoverKeys`), else the internal pointer index.
|
|
317
338
|
const activeIndex = $derived(resolveActiveIndex(hoverKey, hoveredIndex, hoverKeys));
|
|
318
339
|
|
|
340
|
+
// --- Keyboard navigation (FR-5) ------------------------------------------
|
|
341
|
+
// Active when wired explicitly (`keyboardNav`) or implicitly (`onSelectKey`).
|
|
342
|
+
// Renders a focusable overlay (one transparent hit-rect per point) carrying a
|
|
343
|
+
// single roving tab stop. Arrow/Home/End move focus, Enter/Space select,
|
|
344
|
+
// Escape leaves. Focus also feeds the shared hover channel (FR-3 synergy).
|
|
345
|
+
const navEnabled = $derived((keyboardNav === true || onSelectKey !== undefined) && points.length > 0);
|
|
346
|
+
// Comfortable square hit area centred on each dot.
|
|
347
|
+
const NAV_HIT = 18;
|
|
348
|
+
function focusDatum(index: number) {
|
|
349
|
+
focusedIndex = index;
|
|
350
|
+
datapointRefs[index]?.focus();
|
|
351
|
+
emitHoverKey(index);
|
|
352
|
+
}
|
|
353
|
+
function handleDatapointKeyDown(event: KeyboardEvent, index: number) {
|
|
354
|
+
const action = datapointNavAction(event.key, index, points.length);
|
|
355
|
+
if (!action) return;
|
|
356
|
+
event.preventDefault();
|
|
357
|
+
if (action.kind === "move") {
|
|
358
|
+
focusDatum(action.index);
|
|
359
|
+
} else if (action.kind === "select") {
|
|
360
|
+
onSelectKey?.(hoverKeys[index] ?? null);
|
|
361
|
+
} else {
|
|
362
|
+
focusedIndex = -1;
|
|
363
|
+
emitHoverKey(null);
|
|
364
|
+
onSelectKey?.(null);
|
|
365
|
+
(event.currentTarget as SVGElement).blur();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
319
369
|
// Generates a unique gradient id to avoid conflicts when rendering multiple charts on the same page
|
|
320
370
|
const gradientId = $derived.by(() => {
|
|
321
371
|
return `st-areachart-gradient-${Math.random().toString(36).substring(2, 9)}`;
|
|
@@ -484,6 +534,42 @@
|
|
|
484
534
|
</g>
|
|
485
535
|
{/if}
|
|
486
536
|
</svg>
|
|
537
|
+
|
|
538
|
+
<!-- Keyboard navigation overlay (FR-5) — a focusable, transparent hit layer
|
|
539
|
+
over the points. NOT aria-hidden: it is the accessible roving cursor.
|
|
540
|
+
Each rect announces its category + value; the focus ring is tokenised
|
|
541
|
+
via CSS. Absent unless keyboard nav is enabled. -->
|
|
542
|
+
{#if navEnabled}
|
|
543
|
+
<svg
|
|
544
|
+
class="st-areaChart__navLayer"
|
|
545
|
+
viewBox="0 0 {width} {height}"
|
|
546
|
+
preserveAspectRatio="xMidYMid meet"
|
|
547
|
+
width="100%"
|
|
548
|
+
height="100%"
|
|
549
|
+
role="group"
|
|
550
|
+
aria-label={`${label} — points de données`}
|
|
551
|
+
>
|
|
552
|
+
{#each points as p, i (p.index)}
|
|
553
|
+
<rect
|
|
554
|
+
bind:this={datapointRefs[i]}
|
|
555
|
+
class="st-areaChart__navDatum"
|
|
556
|
+
x={p.x - NAV_HIT / 2}
|
|
557
|
+
y={p.y - NAV_HIT / 2}
|
|
558
|
+
width={NAV_HIT}
|
|
559
|
+
height={NAV_HIT}
|
|
560
|
+
rx="3"
|
|
561
|
+
role="img"
|
|
562
|
+
tabindex={rovingTabIndex(i, focusedIndex, points.length)}
|
|
563
|
+
aria-label={datapointAriaLabel(p.datum.x, p.datum.y)}
|
|
564
|
+
onkeydown={(event) => handleDatapointKeyDown(event, i)}
|
|
565
|
+
onfocus={() => {
|
|
566
|
+
focusedIndex = i;
|
|
567
|
+
emitHoverKey(i);
|
|
568
|
+
}}
|
|
569
|
+
/>
|
|
570
|
+
{/each}
|
|
571
|
+
</svg>
|
|
572
|
+
{/if}
|
|
487
573
|
</div>
|
|
488
574
|
|
|
489
575
|
<ChartDataList {label} items={dataValueItems} />
|
|
@@ -526,6 +612,7 @@
|
|
|
526
612
|
|
|
527
613
|
.st-areaChart__visual {
|
|
528
614
|
display: block;
|
|
615
|
+
position: relative;
|
|
529
616
|
}
|
|
530
617
|
|
|
531
618
|
.st-areaChart__grid {
|
|
@@ -40,6 +40,21 @@ type AreaChartProps = {
|
|
|
40
40
|
* keep the shared hover channel in sync.
|
|
41
41
|
*/
|
|
42
42
|
onHoverKeyChange?: (key: string | null) => void;
|
|
43
|
+
/**
|
|
44
|
+
* FR-5 — keyboard navigation of the data points (roving tabindex). When `true`
|
|
45
|
+
* (or implied by wiring `onSelectKey`), a thin focusable overlay is rendered
|
|
46
|
+
* over the points: the chart owns ONE tab stop, ←/↑/→/↓ move the focus between
|
|
47
|
+
* points (data order), Home/End jump to the first/last, Enter/Space select the
|
|
48
|
+
* focused point (`onSelectKey`), Escape leaves the navigation. Each focused
|
|
49
|
+
* point announces its `x` + value. Absent ⇒ no overlay, rendering unchanged.
|
|
50
|
+
*/
|
|
51
|
+
keyboardNav?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Emitted when the user selects the focused point via Enter/Space (its key,
|
|
54
|
+
* `String(x)`), or `null` when the navigation is left via Escape. Wiring it
|
|
55
|
+
* also turns the keyboard navigation on.
|
|
56
|
+
*/
|
|
57
|
+
onSelectKey?: (key: string | null) => void;
|
|
43
58
|
class?: string;
|
|
44
59
|
};
|
|
45
60
|
declare const AreaChart: import("svelte").Component<AreaChartProps, {}, "">;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AreaChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/AreaChart.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,aAAa,GACrB,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,MAAM,cAAc,GAAG;IAC3B,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACnB,CAAC,EAAE,MAAM,CAAC;CACX,CAAC;AAIJ,OAAO,EAIH,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAC;AACjC,OAAO,EAAwC,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"AreaChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/AreaChart.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,aAAa,GACrB,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,MAAM,cAAc,GAAG;IAC3B,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACnB,CAAC,EAAE,MAAM,CAAC;CACX,CAAC;AAIJ,OAAO,EAIH,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAC;AACjC,OAAO,EAAwC,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAK/F,KAAK,cAAc,GAAG;IACpB,IAAI,EAAE,CAAC,MAAM,GAAG,cAAc,CAAC,EAAE,CAAC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAChD;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAibJ,QAAA,MAAM,SAAS,oDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
package/dist/BarChart.svelte
CHANGED
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
} from "./chartAnnotations.js";
|
|
59
59
|
import { formatDataLabel, normalizeDataLabels, type DataLabelsProp } from "./chartDataLabels.js";
|
|
60
60
|
import { resolveActiveIndex } from "./chartCrosshair.js";
|
|
61
|
+
import { datapointAriaLabel, datapointNavAction, rovingTabIndex } from "./chartKeyboardNav.js";
|
|
61
62
|
|
|
62
63
|
type BarChartProps = {
|
|
63
64
|
data: BarChartDatum[];
|
|
@@ -139,6 +140,21 @@
|
|
|
139
140
|
* keep the shared hover channel in sync.
|
|
140
141
|
*/
|
|
141
142
|
onHoverKeyChange?: (key: string | null) => void;
|
|
143
|
+
/**
|
|
144
|
+
* FR-5 — keyboard navigation of the data points (roving tabindex). When `true`
|
|
145
|
+
* (or implied by wiring `onSelectKey`), a thin focusable overlay is rendered
|
|
146
|
+
* over the bars: the chart owns ONE tab stop, ←/↑/→/↓ move the focus between
|
|
147
|
+
* bars (data order), Home/End jump to the first/last, Enter/Space select the
|
|
148
|
+
* focused bar (`onSelectKey`), Escape leaves the navigation. Each focused bar
|
|
149
|
+
* announces its `label` + value. Absent ⇒ no overlay, rendering unchanged.
|
|
150
|
+
*/
|
|
151
|
+
keyboardNav?: boolean;
|
|
152
|
+
/**
|
|
153
|
+
* Emitted when the user selects the focused bar via Enter/Space (its `label`),
|
|
154
|
+
* or `null` when the navigation is left via Escape. Wiring it also turns the
|
|
155
|
+
* keyboard navigation on. Independent of `onSelect`/`selectedKeys`.
|
|
156
|
+
*/
|
|
157
|
+
onSelectKey?: (key: string | null) => void;
|
|
142
158
|
class?: string;
|
|
143
159
|
};
|
|
144
160
|
|
|
@@ -161,6 +177,8 @@
|
|
|
161
177
|
showLegend,
|
|
162
178
|
hoverKey,
|
|
163
179
|
onHoverKeyChange,
|
|
180
|
+
keyboardNav,
|
|
181
|
+
onSelectKey,
|
|
164
182
|
class: className
|
|
165
183
|
}: BarChartProps = $props();
|
|
166
184
|
|
|
@@ -643,6 +661,36 @@
|
|
|
643
661
|
// provided (resolved against `hoverKeys`), else the internal pointer index.
|
|
644
662
|
const activeIndex = $derived(resolveActiveIndex(hoverKey, hoveredIndex, hoverKeys));
|
|
645
663
|
|
|
664
|
+
// --- Keyboard navigation (FR-5) ------------------------------------------
|
|
665
|
+
// Active when wired explicitly (`keyboardNav`) or implicitly (`onSelectKey`).
|
|
666
|
+
// Renders a focusable overlay (one transparent hit-rect per bar) carrying a
|
|
667
|
+
// single roving tab stop. Arrow/Home/End move focus, Enter/Space select,
|
|
668
|
+
// Escape leaves. Focus also feeds the shared hover channel (FR-3 synergy).
|
|
669
|
+
let focusedIndex: number = $state(-1);
|
|
670
|
+
let datapointRefs: Array<SVGRectElement | null> = [];
|
|
671
|
+
const navEnabled = $derived((keyboardNav === true || onSelectKey !== undefined) && bars.length > 0);
|
|
672
|
+
function focusDatum(index: number) {
|
|
673
|
+
focusedIndex = index;
|
|
674
|
+
datapointRefs[index]?.focus();
|
|
675
|
+
emitHoverKey(index);
|
|
676
|
+
}
|
|
677
|
+
function handleDatapointKeyDown(event: KeyboardEvent, index: number) {
|
|
678
|
+
const action = datapointNavAction(event.key, index, bars.length);
|
|
679
|
+
if (!action) return;
|
|
680
|
+
event.preventDefault();
|
|
681
|
+
if (action.kind === "move") {
|
|
682
|
+
focusDatum(action.index);
|
|
683
|
+
} else if (action.kind === "select") {
|
|
684
|
+
onSelectKey?.(bars[index].datum.label);
|
|
685
|
+
} else {
|
|
686
|
+
// Escape — leave the navigation: clear focus + hover channel.
|
|
687
|
+
focusedIndex = -1;
|
|
688
|
+
emitHoverKey(null);
|
|
689
|
+
onSelectKey?.(null);
|
|
690
|
+
(event.currentTarget as SVGElement).blur();
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
646
694
|
const classes = () => ["st-barChart", className].filter(Boolean).join(" ");
|
|
647
695
|
</script>
|
|
648
696
|
|
|
@@ -882,6 +930,41 @@
|
|
|
882
930
|
</g>
|
|
883
931
|
{/if}
|
|
884
932
|
</svg>
|
|
933
|
+
|
|
934
|
+
<!-- Keyboard navigation overlay (FR-5) — a focusable, transparent hit layer
|
|
935
|
+
over the bars. NOT aria-hidden: it is the accessible roving cursor. Each
|
|
936
|
+
rect announces its category + value; the focus ring is tokenised via CSS.
|
|
937
|
+
Absent unless keyboard nav is enabled. -->
|
|
938
|
+
{#if navEnabled}
|
|
939
|
+
<svg
|
|
940
|
+
class="st-barChart__navLayer"
|
|
941
|
+
viewBox="0 0 {width} {height}"
|
|
942
|
+
preserveAspectRatio="xMidYMid meet"
|
|
943
|
+
width="100%"
|
|
944
|
+
height="100%"
|
|
945
|
+
role="group"
|
|
946
|
+
aria-label={`${label} — points de données`}
|
|
947
|
+
>
|
|
948
|
+
{#each bars as bar, i (bar.datum.label)}
|
|
949
|
+
<rect
|
|
950
|
+
bind:this={datapointRefs[i]}
|
|
951
|
+
class="st-barChart__navDatum"
|
|
952
|
+
x={bar.x}
|
|
953
|
+
y={bar.y}
|
|
954
|
+
width={bar.width}
|
|
955
|
+
height={bar.height}
|
|
956
|
+
role="img"
|
|
957
|
+
tabindex={rovingTabIndex(i, focusedIndex, bars.length)}
|
|
958
|
+
aria-label={datapointAriaLabel(bar.datum.label, bar.datum.value)}
|
|
959
|
+
onkeydown={(event) => handleDatapointKeyDown(event, i)}
|
|
960
|
+
onfocus={() => {
|
|
961
|
+
focusedIndex = i;
|
|
962
|
+
emitHoverKey(i);
|
|
963
|
+
}}
|
|
964
|
+
/>
|
|
965
|
+
{/each}
|
|
966
|
+
</svg>
|
|
967
|
+
{/if}
|
|
885
968
|
</div>
|
|
886
969
|
|
|
887
970
|
{#if interactive}
|
|
@@ -113,6 +113,21 @@ type BarChartProps = {
|
|
|
113
113
|
* keep the shared hover channel in sync.
|
|
114
114
|
*/
|
|
115
115
|
onHoverKeyChange?: (key: string | null) => void;
|
|
116
|
+
/**
|
|
117
|
+
* FR-5 — keyboard navigation of the data points (roving tabindex). When `true`
|
|
118
|
+
* (or implied by wiring `onSelectKey`), a thin focusable overlay is rendered
|
|
119
|
+
* over the bars: the chart owns ONE tab stop, ←/↑/→/↓ move the focus between
|
|
120
|
+
* bars (data order), Home/End jump to the first/last, Enter/Space select the
|
|
121
|
+
* focused bar (`onSelectKey`), Escape leaves the navigation. Each focused bar
|
|
122
|
+
* announces its `label` + value. Absent ⇒ no overlay, rendering unchanged.
|
|
123
|
+
*/
|
|
124
|
+
keyboardNav?: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* Emitted when the user selects the focused bar via Enter/Space (its `label`),
|
|
127
|
+
* or `null` when the navigation is left via Escape. Wiring it also turns the
|
|
128
|
+
* keyboard navigation on. Independent of `onSelect`/`selectedKeys`.
|
|
129
|
+
*/
|
|
130
|
+
onSelectKey?: (key: string | null) => void;
|
|
116
131
|
class?: string;
|
|
117
132
|
};
|
|
118
133
|
declare const BarChart: import("svelte").Component<BarChartProps, {}, "">;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BarChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/BarChart.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,YAAY,GACpB,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAEpF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,IAAI,CAAC,EAAE,GAAG,GAAG,GAAG,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,sEAAsE;AACtE,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,KAAK,CAAC;AAI5C,OAAO,EAIH,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAC;AACjC,OAAO,EAAwC,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"BarChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/BarChart.svelte.ts"],"names":[],"mappings":"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"}
|
package/dist/ComboChart.svelte
CHANGED
|
@@ -33,6 +33,15 @@
|
|
|
33
33
|
leftAxisLabel?: string;
|
|
34
34
|
rightAxisLabel?: string;
|
|
35
35
|
legend?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Interactive legend (FR-4). Ids/labels of bar/line series hidden from the
|
|
38
|
+
* render (controlled by the parent; default = all visible). Hidden series
|
|
39
|
+
* are omitted and their legend item is shown "off" (`aria-pressed`).
|
|
40
|
+
* Undefined → legacy non-interactive legend, unless `onToggleSeries` is set.
|
|
41
|
+
*/
|
|
42
|
+
hiddenSeries?: string[];
|
|
43
|
+
/** Emitted on click / Enter / Space on a legend item. */
|
|
44
|
+
onToggleSeries?: (seriesId: string) => void;
|
|
36
45
|
width?: number;
|
|
37
46
|
height?: number;
|
|
38
47
|
label: string;
|
|
@@ -46,12 +55,18 @@
|
|
|
46
55
|
leftAxisLabel,
|
|
47
56
|
rightAxisLabel,
|
|
48
57
|
legend = true,
|
|
58
|
+
hiddenSeries,
|
|
59
|
+
onToggleSeries,
|
|
49
60
|
width = 480,
|
|
50
61
|
height = 240,
|
|
51
62
|
label,
|
|
52
63
|
class: className
|
|
53
64
|
}: ComboChartProps = $props();
|
|
54
65
|
|
|
66
|
+
// Interactive legend is active as soon as the parent wires either prop.
|
|
67
|
+
const legendInteractive = $derived(onToggleSeries !== undefined || hiddenSeries !== undefined);
|
|
68
|
+
const hiddenSet = $derived(new Set(hiddenSeries ?? []));
|
|
69
|
+
|
|
55
70
|
const MARGIN = { top: 12, right: 52, bottom: 32, left: 52 };
|
|
56
71
|
|
|
57
72
|
function niceTicks(min: number, max: number, target = 5): number[] {
|
|
@@ -92,8 +107,9 @@
|
|
|
92
107
|
const plotHeight = $derived(Math.max(height - MARGIN.top - MARGIN.bottom, 1));
|
|
93
108
|
|
|
94
109
|
// Left axis (bars): include zero in the domain so bars rest on a baseline.
|
|
110
|
+
// Hidden series are excluded so the axis rescales to what is visible.
|
|
95
111
|
const leftScale = $derived.by(() => {
|
|
96
|
-
const values = bars.flatMap((s) => s.data);
|
|
112
|
+
const values = bars.filter((s) => !hiddenSet.has(s.label)).flatMap((s) => s.data);
|
|
97
113
|
const minRaw = Math.min(0, ...(values.length ? values : [0]));
|
|
98
114
|
const maxRaw = Math.max(0, ...(values.length ? values : [0]));
|
|
99
115
|
const ticks = niceTicks(minRaw, maxRaw, 5);
|
|
@@ -102,7 +118,7 @@
|
|
|
102
118
|
|
|
103
119
|
// Right axis (lines): padded domain like LineChart.
|
|
104
120
|
const rightScale = $derived.by(() => {
|
|
105
|
-
const values = lines.flatMap((s) => s.data);
|
|
121
|
+
const values = lines.filter((s) => !hiddenSet.has(s.label)).flatMap((s) => s.data);
|
|
106
122
|
if (values.length === 0) {
|
|
107
123
|
const ticks = niceTicks(0, 1, 5);
|
|
108
124
|
return { ticks, domainMin: ticks[0], domainMax: ticks[ticks.length - 1] };
|
|
@@ -136,6 +152,7 @@
|
|
|
136
152
|
const groupX = MARGIN.left + band * ci + (band - groupWidth) / 2;
|
|
137
153
|
const segments = bars
|
|
138
154
|
.map((series, si) => {
|
|
155
|
+
if (hiddenSet.has(series.label)) return null;
|
|
139
156
|
const raw = series.data[ci];
|
|
140
157
|
if (!isPresent(raw)) return null;
|
|
141
158
|
const value = raw;
|
|
@@ -203,6 +220,7 @@
|
|
|
203
220
|
path,
|
|
204
221
|
points,
|
|
205
222
|
seriesLabel: series.label,
|
|
223
|
+
hidden: hiddenSet.has(series.label),
|
|
206
224
|
tone: series.tone ?? `category${((bars.length + li) % 8) + 1}`
|
|
207
225
|
};
|
|
208
226
|
});
|
|
@@ -240,12 +258,12 @@
|
|
|
240
258
|
]);
|
|
241
259
|
|
|
242
260
|
const dataValueItems = $derived([
|
|
243
|
-
...bars
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
...lines
|
|
247
|
-
|
|
248
|
-
|
|
261
|
+
...bars
|
|
262
|
+
.filter((s) => !hiddenSet.has(s.label))
|
|
263
|
+
.flatMap((s) => categories.map((c, ci) => `${s.label}, ${c}: ${s.data[ci] ?? 0}`)),
|
|
264
|
+
...lines
|
|
265
|
+
.filter((s) => !hiddenSet.has(s.label))
|
|
266
|
+
.flatMap((s) => categories.map((c, ci) => `${s.label}, ${c}: ${s.data[ci] ?? 0}`))
|
|
249
267
|
]);
|
|
250
268
|
|
|
251
269
|
type Hover =
|
|
@@ -404,25 +422,27 @@
|
|
|
404
422
|
|
|
405
423
|
<!-- lines -->
|
|
406
424
|
{#each lineSeries as series, li (li)}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
{#each series.points as p, pi (pi)}
|
|
416
|
-
<circle
|
|
417
|
-
class="st-comboChart__dot st-comboChart__dot--{series.tone}"
|
|
418
|
-
cx={p.x}
|
|
419
|
-
cy={p.y}
|
|
420
|
-
r="4"
|
|
421
|
-
data-chart-kind="line"
|
|
422
|
-
data-chart-a={li}
|
|
423
|
-
data-chart-b={pi}
|
|
425
|
+
{#if !series.hidden}
|
|
426
|
+
<path
|
|
427
|
+
class="st-comboChart__line st-comboChart__line--{series.tone}"
|
|
428
|
+
d={series.path}
|
|
429
|
+
fill="none"
|
|
430
|
+
stroke-width="2"
|
|
431
|
+
stroke-linecap="round"
|
|
432
|
+
stroke-linejoin="round"
|
|
424
433
|
/>
|
|
425
|
-
|
|
434
|
+
{#each series.points as p, pi (pi)}
|
|
435
|
+
<circle
|
|
436
|
+
class="st-comboChart__dot st-comboChart__dot--{series.tone}"
|
|
437
|
+
cx={p.x}
|
|
438
|
+
cy={p.y}
|
|
439
|
+
r="4"
|
|
440
|
+
data-chart-kind="line"
|
|
441
|
+
data-chart-a={li}
|
|
442
|
+
data-chart-b={pi}
|
|
443
|
+
/>
|
|
444
|
+
{/each}
|
|
445
|
+
{/if}
|
|
426
446
|
{/each}
|
|
427
447
|
</svg>
|
|
428
448
|
</div>
|
|
@@ -430,13 +450,28 @@
|
|
|
430
450
|
<ChartDataList {label} items={dataValueItems} />
|
|
431
451
|
|
|
432
452
|
{#if legend && legendItems.length > 0}
|
|
433
|
-
<ul class="st-comboChart__legend" aria-hidden="true">
|
|
453
|
+
<ul class="st-comboChart__legend" aria-hidden={legendInteractive ? undefined : "true"}>
|
|
434
454
|
{#each legendItems as item (item.key)}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
455
|
+
{@const off = hiddenSet.has(item.label)}
|
|
456
|
+
<li class="st-comboChart__legendItem" class:st-comboChart__legendItem--off={legendInteractive && off}>
|
|
457
|
+
{#if legendInteractive}
|
|
458
|
+
<button
|
|
459
|
+
type="button"
|
|
460
|
+
class="st-comboChart__legendButton"
|
|
461
|
+
aria-pressed={off}
|
|
462
|
+
onclick={() => onToggleSeries?.(item.label)}
|
|
463
|
+
>
|
|
464
|
+
<span
|
|
465
|
+
class="st-comboChart__legendSwatch st-comboChart__legendSwatch--{item.kind} st-comboChart__legendSwatch--{item.tone}"
|
|
466
|
+
></span>
|
|
467
|
+
{item.label}
|
|
468
|
+
</button>
|
|
469
|
+
{:else}
|
|
470
|
+
<span
|
|
471
|
+
class="st-comboChart__legendSwatch st-comboChart__legendSwatch--{item.kind} st-comboChart__legendSwatch--{item.tone}"
|
|
472
|
+
></span>
|
|
473
|
+
{item.label}
|
|
474
|
+
{/if}
|
|
440
475
|
</li>
|
|
441
476
|
{/each}
|
|
442
477
|
</ul>
|
|
@@ -567,6 +602,28 @@
|
|
|
567
602
|
gap: var(--st-spacing-1, 0.25rem);
|
|
568
603
|
}
|
|
569
604
|
|
|
605
|
+
.st-comboChart__legendItem--off {
|
|
606
|
+
opacity: 0.45;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.st-comboChart__legendButton {
|
|
610
|
+
align-items: center;
|
|
611
|
+
background: none;
|
|
612
|
+
border: 0;
|
|
613
|
+
border-radius: var(--st-radius-sm, 0.25rem);
|
|
614
|
+
color: inherit;
|
|
615
|
+
cursor: pointer;
|
|
616
|
+
display: inline-flex;
|
|
617
|
+
font: inherit;
|
|
618
|
+
gap: var(--st-spacing-1, 0.25rem);
|
|
619
|
+
padding: 0.125rem 0.25rem;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
.st-comboChart__legendButton:focus-visible {
|
|
623
|
+
outline: 2px solid var(--st-semantic-border-interactive);
|
|
624
|
+
outline-offset: 2px;
|
|
625
|
+
}
|
|
626
|
+
|
|
570
627
|
.st-comboChart__legendSwatch {
|
|
571
628
|
display: inline-block;
|
|
572
629
|
flex: none;
|
|
@@ -17,6 +17,15 @@ type ComboChartProps = {
|
|
|
17
17
|
leftAxisLabel?: string;
|
|
18
18
|
rightAxisLabel?: string;
|
|
19
19
|
legend?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Interactive legend (FR-4). Ids/labels of bar/line series hidden from the
|
|
22
|
+
* render (controlled by the parent; default = all visible). Hidden series
|
|
23
|
+
* are omitted and their legend item is shown "off" (`aria-pressed`).
|
|
24
|
+
* Undefined → legacy non-interactive legend, unless `onToggleSeries` is set.
|
|
25
|
+
*/
|
|
26
|
+
hiddenSeries?: string[];
|
|
27
|
+
/** Emitted on click / Enter / Space on a legend item. */
|
|
28
|
+
onToggleSeries?: (seriesId: string) => void;
|
|
20
29
|
width?: number;
|
|
21
30
|
height?: number;
|
|
22
31
|
label: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ComboChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/ComboChart.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,cAAc,GACtB,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAMF,KAAK,eAAe,GAAG;IACrB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,IAAI,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAC7B,KAAK,CAAC,EAAE,oBAAoB,EAAE,CAAC;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;
|
|
1
|
+
{"version":3,"file":"ComboChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/ComboChart.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,cAAc,GACtB,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAMF,KAAK,eAAe,GAAG;IACrB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,IAAI,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAC7B,KAAK,CAAC,EAAE,oBAAoB,EAAE,CAAC;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,yDAAyD;IACzD,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA6WJ,QAAA,MAAM,UAAU,qDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
|
package/dist/LineChart.svelte
CHANGED
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
} from "./chartAnnotations.js";
|
|
62
62
|
import { formatDataLabel, normalizeDataLabels, type DataLabelsProp } from "./chartDataLabels.js";
|
|
63
63
|
import { keyForX, resolveActiveIndex } from "./chartCrosshair.js";
|
|
64
|
+
import { datapointAriaLabel, datapointNavAction, rovingTabIndex } from "./chartKeyboardNav.js";
|
|
64
65
|
|
|
65
66
|
type LineChartProps = {
|
|
66
67
|
data: LineChartDatum[];
|
|
@@ -126,6 +127,21 @@
|
|
|
126
127
|
* keep the shared hover channel in sync.
|
|
127
128
|
*/
|
|
128
129
|
onHoverKeyChange?: (key: string | null) => void;
|
|
130
|
+
/**
|
|
131
|
+
* FR-5 — keyboard navigation of the data points (roving tabindex). When `true`
|
|
132
|
+
* (or implied by wiring `onSelectKey`), a thin focusable overlay is rendered
|
|
133
|
+
* over the points: the chart owns ONE tab stop, ←/↑/→/↓ move the focus between
|
|
134
|
+
* points (data order), Home/End jump to the first/last, Enter/Space select the
|
|
135
|
+
* focused point (`onSelectKey`), Escape leaves the navigation. Each focused
|
|
136
|
+
* point announces its `x` + value. Absent ⇒ no overlay, rendering unchanged.
|
|
137
|
+
*/
|
|
138
|
+
keyboardNav?: boolean;
|
|
139
|
+
/**
|
|
140
|
+
* Emitted when the user selects the focused point via Enter/Space (its key,
|
|
141
|
+
* `String(x)`), or `null` when the navigation is left via Escape. Wiring it
|
|
142
|
+
* also turns the keyboard navigation on.
|
|
143
|
+
*/
|
|
144
|
+
onSelectKey?: (key: string | null) => void;
|
|
129
145
|
class?: string;
|
|
130
146
|
};
|
|
131
147
|
|
|
@@ -149,6 +165,8 @@
|
|
|
149
165
|
showLegend,
|
|
150
166
|
hoverKey,
|
|
151
167
|
onHoverKeyChange,
|
|
168
|
+
keyboardNav,
|
|
169
|
+
onSelectKey,
|
|
152
170
|
class: className
|
|
153
171
|
}: LineChartProps = $props();
|
|
154
172
|
|
|
@@ -323,6 +341,9 @@
|
|
|
323
341
|
}
|
|
324
342
|
|
|
325
343
|
let hoveredIndex: number | null = $state(null);
|
|
344
|
+
// FR-5 — roving keyboard focus over the data points (separate from hover).
|
|
345
|
+
let focusedIndex: number = $state(-1);
|
|
346
|
+
let datapointRefs: Array<SVGRectElement | null> = [];
|
|
326
347
|
|
|
327
348
|
const plotWidth = $derived(Math.max(width - MARGIN.left - MARGIN.right, 1));
|
|
328
349
|
const plotHeight = $derived(Math.max(height - MARGIN.top - MARGIN.bottom, 1));
|
|
@@ -661,6 +682,35 @@
|
|
|
661
682
|
// provided (resolved against `hoverKeys`), else the internal pointer index.
|
|
662
683
|
const activeIndex = $derived(resolveActiveIndex(hoverKey, hoveredIndex, hoverKeys));
|
|
663
684
|
|
|
685
|
+
// --- Keyboard navigation (FR-5) ------------------------------------------
|
|
686
|
+
// Active when wired explicitly (`keyboardNav`) or implicitly (`onSelectKey`).
|
|
687
|
+
// Renders a focusable overlay (one transparent hit-rect per point) carrying a
|
|
688
|
+
// single roving tab stop. Arrow/Home/End move focus, Enter/Space select,
|
|
689
|
+
// Escape leaves. Focus also feeds the shared hover channel (FR-3 synergy).
|
|
690
|
+
const navEnabled = $derived((keyboardNav === true || onSelectKey !== undefined) && points.length > 0);
|
|
691
|
+
// Comfortable square hit area centred on each dot.
|
|
692
|
+
const NAV_HIT = 18;
|
|
693
|
+
function focusDatum(index: number) {
|
|
694
|
+
focusedIndex = index;
|
|
695
|
+
datapointRefs[index]?.focus();
|
|
696
|
+
emitHoverKey(index);
|
|
697
|
+
}
|
|
698
|
+
function handleDatapointKeyDown(event: KeyboardEvent, index: number) {
|
|
699
|
+
const action = datapointNavAction(event.key, index, points.length);
|
|
700
|
+
if (!action) return;
|
|
701
|
+
event.preventDefault();
|
|
702
|
+
if (action.kind === "move") {
|
|
703
|
+
focusDatum(action.index);
|
|
704
|
+
} else if (action.kind === "select") {
|
|
705
|
+
onSelectKey?.(hoverKeys[index] ?? null);
|
|
706
|
+
} else {
|
|
707
|
+
focusedIndex = -1;
|
|
708
|
+
emitHoverKey(null);
|
|
709
|
+
onSelectKey?.(null);
|
|
710
|
+
(event.currentTarget as SVGElement).blur();
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
664
714
|
const classes = () =>
|
|
665
715
|
["st-lineChart", `st-lineChart--${tone}`, className].filter(Boolean).join(" ");
|
|
666
716
|
</script>
|
|
@@ -862,6 +912,42 @@
|
|
|
862
912
|
</g>
|
|
863
913
|
{/if}
|
|
864
914
|
</svg>
|
|
915
|
+
|
|
916
|
+
<!-- Keyboard navigation overlay (FR-5) — a focusable, transparent hit layer
|
|
917
|
+
over the points. NOT aria-hidden: it is the accessible roving cursor.
|
|
918
|
+
Each rect announces its category + value; the focus ring is tokenised
|
|
919
|
+
via CSS. Absent unless keyboard nav is enabled. -->
|
|
920
|
+
{#if navEnabled}
|
|
921
|
+
<svg
|
|
922
|
+
class="st-lineChart__navLayer"
|
|
923
|
+
viewBox="0 0 {width} {height}"
|
|
924
|
+
preserveAspectRatio="xMidYMid meet"
|
|
925
|
+
width="100%"
|
|
926
|
+
height="100%"
|
|
927
|
+
role="group"
|
|
928
|
+
aria-label={`${label} — points de données`}
|
|
929
|
+
>
|
|
930
|
+
{#each points as p, i (p.index)}
|
|
931
|
+
<rect
|
|
932
|
+
bind:this={datapointRefs[i]}
|
|
933
|
+
class="st-lineChart__navDatum"
|
|
934
|
+
x={p.x - NAV_HIT / 2}
|
|
935
|
+
y={p.y - NAV_HIT / 2}
|
|
936
|
+
width={NAV_HIT}
|
|
937
|
+
height={NAV_HIT}
|
|
938
|
+
rx="3"
|
|
939
|
+
role="img"
|
|
940
|
+
tabindex={rovingTabIndex(i, focusedIndex, points.length)}
|
|
941
|
+
aria-label={datapointAriaLabel(p.datum.x, p.datum.y)}
|
|
942
|
+
onkeydown={(event) => handleDatapointKeyDown(event, i)}
|
|
943
|
+
onfocus={() => {
|
|
944
|
+
focusedIndex = i;
|
|
945
|
+
emitHoverKey(i);
|
|
946
|
+
}}
|
|
947
|
+
/>
|
|
948
|
+
{/each}
|
|
949
|
+
</svg>
|
|
950
|
+
{/if}
|
|
865
951
|
</div>
|
|
866
952
|
|
|
867
953
|
<ChartDataList {label} items={dataValueItems} />
|
|
@@ -904,6 +990,7 @@
|
|
|
904
990
|
|
|
905
991
|
.st-lineChart__visual {
|
|
906
992
|
display: block;
|
|
993
|
+
position: relative;
|
|
907
994
|
}
|
|
908
995
|
|
|
909
996
|
.st-lineChart__grid {
|
|
@@ -100,6 +100,21 @@ type LineChartProps = {
|
|
|
100
100
|
* keep the shared hover channel in sync.
|
|
101
101
|
*/
|
|
102
102
|
onHoverKeyChange?: (key: string | null) => void;
|
|
103
|
+
/**
|
|
104
|
+
* FR-5 — keyboard navigation of the data points (roving tabindex). When `true`
|
|
105
|
+
* (or implied by wiring `onSelectKey`), a thin focusable overlay is rendered
|
|
106
|
+
* over the points: the chart owns ONE tab stop, ←/↑/→/↓ move the focus between
|
|
107
|
+
* points (data order), Home/End jump to the first/last, Enter/Space select the
|
|
108
|
+
* focused point (`onSelectKey`), Escape leaves the navigation. Each focused
|
|
109
|
+
* point announces its `x` + value. Absent ⇒ no overlay, rendering unchanged.
|
|
110
|
+
*/
|
|
111
|
+
keyboardNav?: boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Emitted when the user selects the focused point via Enter/Space (its key,
|
|
114
|
+
* `String(x)`), or `null` when the navigation is left via Escape. Wiring it
|
|
115
|
+
* also turns the keyboard navigation on.
|
|
116
|
+
*/
|
|
117
|
+
onSelectKey?: (key: string | null) => void;
|
|
103
118
|
class?: string;
|
|
104
119
|
};
|
|
105
120
|
declare const LineChart: import("svelte").Component<LineChartProps, {}, "">;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LineChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/LineChart.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,aAAa,GACrB,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,MAAM,cAAc,GAAG;IAC3B,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACnB,CAAC,EAAE,MAAM,CAAC;IACV;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAEpF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,IAAI,CAAC,EAAE,GAAG,GAAG,GAAG,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,sEAAsE;AACtE,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,KAAK,CAAC;AAI5C,OAAO,EAIH,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAC;AACjC,OAAO,EAAwC,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"LineChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/LineChart.svelte.ts"],"names":[],"mappings":"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"}
|
|
@@ -33,6 +33,16 @@
|
|
|
33
33
|
* values already live in the accessible ChartDataList.
|
|
34
34
|
*/
|
|
35
35
|
dataLabels?: DataLabelsProp;
|
|
36
|
+
/**
|
|
37
|
+
* Interactive legend (FR-4). Ids/labels of series hidden from the render
|
|
38
|
+
* (controlled by the parent; default = all visible). Each segment whose
|
|
39
|
+
* `label` ∈ `hiddenSeries` is omitted and its legend item is shown "off"
|
|
40
|
+
* (`aria-pressed`). Undefined → legacy non-interactive legend, unless
|
|
41
|
+
* `onToggleSeries` is provided.
|
|
42
|
+
*/
|
|
43
|
+
hiddenSeries?: string[];
|
|
44
|
+
/** Emitted on click / Enter / Space on a legend item. */
|
|
45
|
+
onToggleSeries?: (seriesId: string) => void;
|
|
36
46
|
class?: string;
|
|
37
47
|
};
|
|
38
48
|
|
|
@@ -43,9 +53,15 @@
|
|
|
43
53
|
label,
|
|
44
54
|
showLegend = true,
|
|
45
55
|
dataLabels,
|
|
56
|
+
hiddenSeries,
|
|
57
|
+
onToggleSeries,
|
|
46
58
|
class: className
|
|
47
59
|
}: StackedBarChartProps = $props();
|
|
48
60
|
|
|
61
|
+
// Interactive legend is active as soon as the parent wires either prop.
|
|
62
|
+
const legendInteractive = $derived(onToggleSeries !== undefined || hiddenSeries !== undefined);
|
|
63
|
+
const hiddenSet = $derived(new Set(hiddenSeries ?? []));
|
|
64
|
+
|
|
49
65
|
const MARGIN = { top: 14, right: 16, bottom: 34, left: 44 };
|
|
50
66
|
const TONES = ["category1","category2","category3","category4","category5","category6","category7","category8"] as const;
|
|
51
67
|
|
|
@@ -81,7 +97,9 @@
|
|
|
81
97
|
let hovered: { bar: number; seg: number } | null = $state(null);
|
|
82
98
|
|
|
83
99
|
const scales = $derived.by(() => {
|
|
84
|
-
const totals = data.map((b) =>
|
|
100
|
+
const totals = data.map((b) =>
|
|
101
|
+
b.segments.reduce((s, x) => (hiddenSet.has(x.label) ? s : s + Math.max(x.value, 0)), 0)
|
|
102
|
+
);
|
|
85
103
|
const ticks = niceTicks(0, Math.max(0, ...totals));
|
|
86
104
|
return {
|
|
87
105
|
ticks, domainMax: ticks[ticks.length - 1],
|
|
@@ -98,23 +116,32 @@
|
|
|
98
116
|
return data.map((bar, bi) => {
|
|
99
117
|
const x = MARGIN.left + band * bi + (band - barWidth) / 2;
|
|
100
118
|
let acc = 0;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
119
|
+
// Tone is bound to the original segment index so it stays stable when a
|
|
120
|
+
// series is toggled off; hidden segments are dropped before stacking.
|
|
121
|
+
const segs = bar.segments
|
|
122
|
+
.map((seg, si) => ({ seg, tone: seg.tone ?? TONES[si % TONES.length] }))
|
|
123
|
+
.filter(({ seg }) => !hiddenSet.has(seg.label))
|
|
124
|
+
.map(({ seg, tone }) => {
|
|
125
|
+
const v = Math.max(seg.value, 0);
|
|
126
|
+
const yTop = MARGIN.top + scaleLinear(acc + v, 0, domainMax, plotH, 0);
|
|
127
|
+
const yBottom = MARGIN.top + scaleLinear(acc, 0, domainMax, plotH, 0);
|
|
128
|
+
acc += v;
|
|
129
|
+
return {
|
|
130
|
+
x, y: yTop, width: barWidth, height: Math.max(yBottom - yTop, 0),
|
|
131
|
+
seg, tone,
|
|
132
|
+
cx: x + barWidth / 2, cy: yTop + (yBottom - yTop) / 2
|
|
133
|
+
};
|
|
134
|
+
});
|
|
112
135
|
return { x, band, label: bar.label, segs, cxLabel: MARGIN.left + band * (bi + 0.5) };
|
|
113
136
|
});
|
|
114
137
|
});
|
|
115
138
|
|
|
116
139
|
const dataValueItems = $derived(
|
|
117
|
-
data.flatMap((bar) =>
|
|
140
|
+
data.flatMap((bar) =>
|
|
141
|
+
bar.segments
|
|
142
|
+
.filter((seg) => !hiddenSet.has(seg.label))
|
|
143
|
+
.map((seg) => `${bar.label}, ${seg.label}: ${seg.value}`)
|
|
144
|
+
)
|
|
118
145
|
);
|
|
119
146
|
|
|
120
147
|
// --- Data labels ----------------------------------------------------------
|
|
@@ -206,9 +233,22 @@
|
|
|
206
233
|
{#if showLegend && legend.length > 0}
|
|
207
234
|
<ul class="st-stackedBar__legend">
|
|
208
235
|
{#each legend as item (item.seriesLabel)}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
{
|
|
236
|
+
{@const off = hiddenSet.has(item.seriesLabel)}
|
|
237
|
+
<li class="st-stackedBar__legendItem" class:st-stackedBar__legendItem--off={legendInteractive && off}>
|
|
238
|
+
{#if legendInteractive}
|
|
239
|
+
<button
|
|
240
|
+
type="button"
|
|
241
|
+
class="st-stackedBar__legendButton"
|
|
242
|
+
aria-pressed={off}
|
|
243
|
+
onclick={() => onToggleSeries?.(item.seriesLabel)}
|
|
244
|
+
>
|
|
245
|
+
<span class="st-stackedBar__legendSwatch st-stackedBar__legendSwatch--{item.tone}" aria-hidden="true"></span>
|
|
246
|
+
{item.seriesLabel}
|
|
247
|
+
</button>
|
|
248
|
+
{:else}
|
|
249
|
+
<span class="st-stackedBar__legendSwatch st-stackedBar__legendSwatch--{item.tone}" aria-hidden="true"></span>
|
|
250
|
+
{item.seriesLabel}
|
|
251
|
+
{/if}
|
|
212
252
|
</li>
|
|
213
253
|
{/each}
|
|
214
254
|
</ul>
|
|
@@ -243,6 +283,12 @@
|
|
|
243
283
|
.st-stackedBar__tooltipValue { opacity: 0.85; }
|
|
244
284
|
.st-stackedBar__legend { display: flex; flex-wrap: wrap; gap: 0.75rem; list-style: none; margin: 0.5rem 0 0; padding: 0; }
|
|
245
285
|
.st-stackedBar__legendItem { align-items: center; color: var(--st-semantic-text-secondary); display: inline-flex; font-size: 0.75rem; gap: 0.35rem; }
|
|
286
|
+
.st-stackedBar__legendItem--off { opacity: 0.45; }
|
|
287
|
+
.st-stackedBar__legendButton {
|
|
288
|
+
align-items: center; background: none; border: 0; border-radius: var(--st-radius-sm, 0.25rem);
|
|
289
|
+
color: inherit; cursor: pointer; display: inline-flex; font: inherit; gap: 0.35rem; padding: 0.125rem 0.25rem;
|
|
290
|
+
}
|
|
291
|
+
.st-stackedBar__legendButton:focus-visible { outline: 2px solid var(--st-semantic-border-interactive); outline-offset: 2px; }
|
|
246
292
|
.st-stackedBar__legendSwatch { border-radius: 2px; height: 0.7rem; width: 0.7rem; }
|
|
247
293
|
.st-stackedBar__legendSwatch--category1 { background: var(--st-semantic-data-category1); }
|
|
248
294
|
.st-stackedBar__legendSwatch--category2 { background: var(--st-semantic-data-category2); }
|
|
@@ -23,6 +23,16 @@ type StackedBarChartProps = {
|
|
|
23
23
|
* values already live in the accessible ChartDataList.
|
|
24
24
|
*/
|
|
25
25
|
dataLabels?: DataLabelsProp;
|
|
26
|
+
/**
|
|
27
|
+
* Interactive legend (FR-4). Ids/labels of series hidden from the render
|
|
28
|
+
* (controlled by the parent; default = all visible). Each segment whose
|
|
29
|
+
* `label` ∈ `hiddenSeries` is omitted and its legend item is shown "off"
|
|
30
|
+
* (`aria-pressed`). Undefined → legacy non-interactive legend, unless
|
|
31
|
+
* `onToggleSeries` is provided.
|
|
32
|
+
*/
|
|
33
|
+
hiddenSeries?: string[];
|
|
34
|
+
/** Emitted on click / Enter / Space on a legend item. */
|
|
35
|
+
onToggleSeries?: (seriesId: string) => void;
|
|
26
36
|
class?: string;
|
|
27
37
|
};
|
|
28
38
|
declare const StackedBarChart: import("svelte").Component<StackedBarChartProps, {}, "">;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StackedBarChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/StackedBarChart.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,cAAc,GACtB,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GACrD,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;AAE1D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,iBAAiB,EAAE,CAAC;CAC/B,CAAC;AAIJ,OAAO,EAAwC,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG/F,KAAK,oBAAoB,GAAG;IAC1B,IAAI,EAAE,eAAe,EAAE,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;
|
|
1
|
+
{"version":3,"file":"StackedBarChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/StackedBarChart.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,cAAc,GACtB,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GACrD,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;AAE1D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,iBAAiB,EAAE,CAAC;CAC/B,CAAC;AAIJ,OAAO,EAAwC,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG/F,KAAK,oBAAoB,GAAG;IAC1B,IAAI,EAAE,eAAe,EAAE,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,yDAAyD;IACzD,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA4MJ,QAAA,MAAM,eAAe,0DAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Keys that drive the roving focus, mapped to an action. */
|
|
2
|
+
export type DatapointNavAction = {
|
|
3
|
+
kind: "move";
|
|
4
|
+
index: number;
|
|
5
|
+
} | {
|
|
6
|
+
kind: "select";
|
|
7
|
+
} | {
|
|
8
|
+
kind: "escape";
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Maps a keydown to a roving-nav action given the current focused index and the
|
|
12
|
+
* datum count. Returns `null` for keys we don't handle (so the caller leaves the
|
|
13
|
+
* event untouched). `count` is assumed >= 1 when called.
|
|
14
|
+
*/
|
|
15
|
+
export declare function datapointNavAction(key: string, current: number, count: number): DatapointNavAction | null;
|
|
16
|
+
/**
|
|
17
|
+
* The roving `tabindex` for the datum at `index`: `0` (the single tab stop) for
|
|
18
|
+
* the focused datum, `-1` for every other. When nothing is focused yet
|
|
19
|
+
* (`focusedIndex < 0`) the FIRST datum holds the tab stop so the group is
|
|
20
|
+
* reachable by Tab.
|
|
21
|
+
*/
|
|
22
|
+
export declare function rovingTabIndex(index: number, focusedIndex: number, count: number): number;
|
|
23
|
+
/**
|
|
24
|
+
* Accessible label for a focused datum: its category followed by its value, e.g.
|
|
25
|
+
* `"Janvier, 42"`. `category` is the x/categorical label, `value` the y value.
|
|
26
|
+
* Kept framework-agnostic and identical across the three packages.
|
|
27
|
+
*/
|
|
28
|
+
export declare function datapointAriaLabel(category: string | number, value: string | number): string;
|
|
29
|
+
//# sourceMappingURL=chartKeyboardNav.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chartKeyboardNav.d.ts","sourceRoot":"","sources":["../src/lib/chartKeyboardNav.ts"],"names":[],"mappings":"AAqBA,6DAA6D;AAC7D,MAAM,MAAM,kBAAkB,GAC1B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEvB;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,kBAAkB,GAAG,IAAI,CAwB3B;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAGzF;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAE5F"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// --- Chart datapoint keyboard navigation (shared, framework-agnostic) --------
|
|
2
|
+
//
|
|
3
|
+
// FR-5: ROVING-TABINDEX keyboard navigation over a cartesian chart's data
|
|
4
|
+
// points (Bar / Line / Area). The decorative SVG stays `aria-hidden`; a thin,
|
|
5
|
+
// transparent overlay of focusable hit-rects (one per datum, in data order) is
|
|
6
|
+
// rendered on top. The overlay carries a SINGLE tab stop — the focused datum —
|
|
7
|
+
// and the arrow keys move that focus between datums:
|
|
8
|
+
//
|
|
9
|
+
// ←/↑ previous datum →/↓ next datum
|
|
10
|
+
// Home first datum End last datum
|
|
11
|
+
// Enter / Space select the focused datum Escape leave the nav
|
|
12
|
+
//
|
|
13
|
+
// Each focusable element announces its category + value via `aria-label`
|
|
14
|
+
// (built by `datapointAriaLabel`). Selection emits `onSelectKey?(key | null)`;
|
|
15
|
+
// Escape clears the internal selection (emits null). The focus also feeds the
|
|
16
|
+
// shared crosshair channel: moving focus emits `onHoverKeyChange(key)` so a
|
|
17
|
+
// linked tooltip (FR-3) tracks the keyboard cursor, and leaving emits null.
|
|
18
|
+
//
|
|
19
|
+
// Purely additive: a chart that wires neither `onSelectKey` nor `keyboardNav`
|
|
20
|
+
// renders exactly as before (no overlay, no extra tab stop).
|
|
21
|
+
/**
|
|
22
|
+
* Maps a keydown to a roving-nav action given the current focused index and the
|
|
23
|
+
* datum count. Returns `null` for keys we don't handle (so the caller leaves the
|
|
24
|
+
* event untouched). `count` is assumed >= 1 when called.
|
|
25
|
+
*/
|
|
26
|
+
export function datapointNavAction(key, current, count) {
|
|
27
|
+
if (count <= 0)
|
|
28
|
+
return null;
|
|
29
|
+
const clamp = (i) => Math.min(count - 1, Math.max(0, i));
|
|
30
|
+
switch (key) {
|
|
31
|
+
case "ArrowRight":
|
|
32
|
+
case "ArrowDown":
|
|
33
|
+
return { kind: "move", index: clamp(current + 1) };
|
|
34
|
+
case "ArrowLeft":
|
|
35
|
+
case "ArrowUp":
|
|
36
|
+
return { kind: "move", index: clamp(current - 1) };
|
|
37
|
+
case "Home":
|
|
38
|
+
return { kind: "move", index: 0 };
|
|
39
|
+
case "End":
|
|
40
|
+
return { kind: "move", index: count - 1 };
|
|
41
|
+
case "Enter":
|
|
42
|
+
case " ":
|
|
43
|
+
case "Spacebar": // legacy IE/Edge value
|
|
44
|
+
return { kind: "select" };
|
|
45
|
+
case "Escape":
|
|
46
|
+
case "Esc": // legacy value
|
|
47
|
+
return { kind: "escape" };
|
|
48
|
+
default:
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The roving `tabindex` for the datum at `index`: `0` (the single tab stop) for
|
|
54
|
+
* the focused datum, `-1` for every other. When nothing is focused yet
|
|
55
|
+
* (`focusedIndex < 0`) the FIRST datum holds the tab stop so the group is
|
|
56
|
+
* reachable by Tab.
|
|
57
|
+
*/
|
|
58
|
+
export function rovingTabIndex(index, focusedIndex, count) {
|
|
59
|
+
const active = focusedIndex >= 0 && focusedIndex < count ? focusedIndex : 0;
|
|
60
|
+
return index === active ? 0 : -1;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Accessible label for a focused datum: its category followed by its value, e.g.
|
|
64
|
+
* `"Janvier, 42"`. `category` is the x/categorical label, `value` the y value.
|
|
65
|
+
* Kept framework-agnostic and identical across the three packages.
|
|
66
|
+
*/
|
|
67
|
+
export function datapointAriaLabel(category, value) {
|
|
68
|
+
return `${category}, ${value}`;
|
|
69
|
+
}
|