@sentropic/design-system-svelte 0.34.26 → 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.
@@ -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;AAI/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,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAkYJ,QAAA,MAAM,SAAS,oDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,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"}
@@ -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;AAI/F,KAAK,aAAa,GAAG;IACnB,IAAI,EAAE,aAAa,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,UAAU,GAAG,YAAY,CAAC;IACxC,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,+DAA+D;IAC/D,cAAc,CAAC,EAAE,kBAAkB,EAAE,CAAC;IACtC,oDAAoD;IACpD,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC;IACpB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;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,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA8qBJ,QAAA,MAAM,QAAQ,mDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"BarChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/BarChart.svelte.ts"],"names":[],"mappings":"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"}
@@ -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;AAI/F,KAAK,cAAc,GAAG;IACpB,IAAI,EAAE,cAAc,EAAE,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,cAAc,CAAC,EAAE,kBAAkB,EAAE,CAAC;IACtC,oDAAoD;IACpD,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC;IACpB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,qDAAqD;IACrD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B;;;;OAIG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1B;;;;OAIG;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,iDAAiD;IACjD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;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,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA+qBJ,QAAA,MAAM,SAAS,oDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"LineChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/LineChart.svelte.ts"],"names":[],"mappings":"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"}
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentropic/design-system-svelte",
3
- "version": "0.34.26",
3
+ "version": "0.34.27",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"