@sentropic/design-system-svelte 0.34.26 → 0.34.28
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/AppChrome.svelte +22 -3
- package/dist/AppChrome.svelte.d.ts +2 -0
- package/dist/AppChrome.svelte.d.ts.map +1 -1
- package/dist/AppChrome.test.js +10 -0
- 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/LineChart.svelte +87 -0
- package/dist/LineChart.svelte.d.ts +15 -0
- package/dist/LineChart.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/AppChrome.svelte
CHANGED
|
@@ -75,6 +75,10 @@
|
|
|
75
75
|
/** Zone identité à droite (IdentityMenu, bouton connexion, …). */
|
|
76
76
|
identity?: Snippet;
|
|
77
77
|
|
|
78
|
+
// ── Contrôles additionnels ─────────────────────────────────────────────────
|
|
79
|
+
/** Contrôles additionnels dans la zone utilitaire. */
|
|
80
|
+
extraSelectors?: Snippet;
|
|
81
|
+
|
|
78
82
|
// ── Mobile ─────────────────────────────────────────────────────────────────
|
|
79
83
|
/** État ouvert du tiroir mobile (contrôlé). */
|
|
80
84
|
mobileMenuOpen?: boolean;
|
|
@@ -95,10 +99,10 @@
|
|
|
95
99
|
<script lang="ts">
|
|
96
100
|
import { untrack } from "svelte";
|
|
97
101
|
import {
|
|
98
|
-
Boxes,
|
|
99
102
|
ChevronDown,
|
|
100
103
|
Github,
|
|
101
104
|
Globe,
|
|
105
|
+
Menu,
|
|
102
106
|
Moon,
|
|
103
107
|
Palette,
|
|
104
108
|
Sun,
|
|
@@ -127,6 +131,7 @@
|
|
|
127
131
|
githubHref,
|
|
128
132
|
githubLabel = "GitHub",
|
|
129
133
|
identity,
|
|
134
|
+
extraSelectors,
|
|
130
135
|
mobileMenuOpen = false,
|
|
131
136
|
onMobileMenuToggle,
|
|
132
137
|
menuLabel = "Menu",
|
|
@@ -320,6 +325,7 @@
|
|
|
320
325
|
{#if showLocaleSelector}{@render localeSelector()}{/if}
|
|
321
326
|
{#if showGithub}{@render githubLink()}{/if}
|
|
322
327
|
{#if identity}<div class="st-appChrome__identity">{@render identity()}</div>{/if}
|
|
328
|
+
{#if extraSelectors}<div class="st-appChrome__extraSelectors">{@render extraSelectors()}</div>{/if}
|
|
323
329
|
</div>
|
|
324
330
|
{/snippet}
|
|
325
331
|
|
|
@@ -334,7 +340,7 @@
|
|
|
334
340
|
aria-controls={drawerId}
|
|
335
341
|
aria-label={menuLabel}
|
|
336
342
|
>
|
|
337
|
-
<
|
|
343
|
+
<Menu size={20} aria-hidden="true" />
|
|
338
344
|
</button>
|
|
339
345
|
{/snippet}
|
|
340
346
|
|
|
@@ -434,11 +440,18 @@
|
|
|
434
440
|
react/vue : la source de vérité du CSS est le bloc publié (styles.css).
|
|
435
441
|
Ce <style> scoped ne fait que rendre la démo Svelte autonome. */
|
|
436
442
|
.st-appChrome {
|
|
443
|
+
position: sticky;
|
|
444
|
+
top: 0;
|
|
437
445
|
width: 100%;
|
|
446
|
+
z-index: 30;
|
|
438
447
|
}
|
|
439
448
|
|
|
440
449
|
:global(.st-appChrome__header .st-appHeader__bar) {
|
|
450
|
+
background: color-mix(in srgb, var(--st-semantic-surface-default) 96%, transparent);
|
|
451
|
+
backdrop-filter: blur(8px);
|
|
452
|
+
height: 5rem;
|
|
441
453
|
max-width: none;
|
|
454
|
+
padding: 0 var(--st-spacing-6, 1.5rem);
|
|
442
455
|
}
|
|
443
456
|
|
|
444
457
|
.st-appChrome__brand {
|
|
@@ -482,7 +495,7 @@
|
|
|
482
495
|
.st-appChrome__utilityNav {
|
|
483
496
|
align-items: center;
|
|
484
497
|
display: flex;
|
|
485
|
-
gap: var(--st-spacing-
|
|
498
|
+
gap: var(--st-spacing-1, 0.25rem);
|
|
486
499
|
}
|
|
487
500
|
|
|
488
501
|
.st-appChrome__menuWrap {
|
|
@@ -579,6 +592,12 @@
|
|
|
579
592
|
color: var(--st-semantic-text-primary);
|
|
580
593
|
}
|
|
581
594
|
|
|
595
|
+
.st-appChrome__extraSelectors {
|
|
596
|
+
align-items: center;
|
|
597
|
+
display: flex;
|
|
598
|
+
gap: var(--st-spacing-1, 0.25rem);
|
|
599
|
+
}
|
|
600
|
+
|
|
582
601
|
.st-appChrome__drawer {
|
|
583
602
|
background: var(--st-semantic-surface-default);
|
|
584
603
|
border-bottom: 1px solid var(--st-semantic-border-subtle);
|
|
@@ -60,6 +60,8 @@ export interface AppChromeProps {
|
|
|
60
60
|
githubLabel?: string;
|
|
61
61
|
/** Zone identité à droite (IdentityMenu, bouton connexion, …). */
|
|
62
62
|
identity?: Snippet;
|
|
63
|
+
/** Contrôles additionnels dans la zone utilitaire. */
|
|
64
|
+
extraSelectors?: Snippet;
|
|
63
65
|
/** État ouvert du tiroir mobile (contrôlé). */
|
|
64
66
|
mobileMenuOpen?: boolean;
|
|
65
67
|
/** Callback de bascule du tiroir mobile. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppChrome.svelte.d.ts","sourceRoot":"","sources":["../src/lib/AppChrome.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,iDAAiD;AACjD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wCAAwC;AACxC,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;AAC3D,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,IAAI,CAAC;AAE1C,MAAM,WAAW,cAAc;IAE7B,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACzB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,yDAAyD;IACzD,MAAM,CAAC,EAAE,oBAAoB,EAAE,CAAC;IAChC,yBAAyB;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,aAAa,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,wCAAwC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B,4DAA4D;IAC5D,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACvD,0DAA0D;IAC1D,eAAe,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAGhE,2DAA2D;IAC3D,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,wCAAwC;IACxC,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;IACnD,yCAAyC;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;IAGrB,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iCAAiC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IAGrB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAGnB,+CAA+C;IAC/C,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,4CAA4C;IAC5C,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,mCAAmC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;
|
|
1
|
+
{"version":3,"file":"AppChrome.svelte.d.ts","sourceRoot":"","sources":["../src/lib/AppChrome.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,iDAAiD;AACjD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wCAAwC;AACxC,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;AAC3D,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,IAAI,CAAC;AAE1C,MAAM,WAAW,cAAc;IAE7B,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACzB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,yDAAyD;IACzD,MAAM,CAAC,EAAE,oBAAoB,EAAE,CAAC;IAChC,yBAAyB;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,aAAa,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,wCAAwC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B,4DAA4D;IAC5D,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACvD,0DAA0D;IAC1D,eAAe,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAGhE,2DAA2D;IAC3D,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,wCAAwC;IACxC,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;IACnD,yCAAyC;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;IAGrB,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iCAAiC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IAGrB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAGnB,sDAAsD;IACtD,cAAc,CAAC,EAAE,OAAO,CAAC;IAGzB,+CAA+C;IAC/C,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,4CAA4C;IAC5C,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,mCAAmC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAuRH,QAAA,MAAM,SAAS,oDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
package/dist/AppChrome.test.js
CHANGED
|
@@ -120,3 +120,13 @@ describe("AppChrome — mobile burger + tiroir", () => {
|
|
|
120
120
|
expect(onMobileMenuToggle).toHaveBeenCalledTimes(1);
|
|
121
121
|
});
|
|
122
122
|
});
|
|
123
|
+
describe("AppChrome — extraSelectors", () => {
|
|
124
|
+
it("renders extraSelectors snippet content in the utility nav", () => {
|
|
125
|
+
const { container } = render(AppChrome, { props: { extraSelectors: snippet("extra-ctrl") } });
|
|
126
|
+
expect(container.querySelector(".st-appChrome__extraSelectors")?.textContent).toContain("extra-ctrl");
|
|
127
|
+
});
|
|
128
|
+
it("does not render extraSelectors div when not provided", () => {
|
|
129
|
+
const { container } = render(AppChrome);
|
|
130
|
+
expect(container.querySelector(".st-appChrome__extraSelectors")).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
});
|
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/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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"}
|
|
@@ -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
|
+
}
|