@astryxdesign/lab 0.2.0-canary.f306f13 → 0.2.0-canary.fc53ff7

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.
@@ -5,6 +5,10 @@
5
5
  *
6
6
  * Handles the d3 margin convention: outer SVG at full size, inner <g> translated
7
7
  * by margins. Children receive scales and dimensions via context.
8
+ *
9
+ * Accessibility: the svg is exposed as a named image (role="img" + aria-label,
10
+ * overridable via `label`), and small datasets are mirrored in a visually
11
+ * hidden data table so screen reader users can read the underlying values.
8
12
  */
9
13
  import { type ReactNode } from 'react';
10
14
  import type { ChartMargin } from './types';
@@ -58,6 +62,13 @@ export interface ChartProps {
58
62
  * interference on mobile.
59
63
  */
60
64
  interactive?: boolean;
65
+ /**
66
+ * Accessible name for the chart, announced by screen readers (the svg is
67
+ * exposed as `role="img"`). Defaults to an English description derived from
68
+ * the keys (e.g. "Chart of revenue by month") — pass a localized string to
69
+ * override.
70
+ */
71
+ label?: string;
61
72
  /** Chart contents — axes, marks, tooltips */
62
73
  children: ReactNode;
63
74
  }
@@ -73,5 +84,5 @@ export interface ChartProps {
73
84
  * </Chart>
74
85
  * ```
75
86
  */
76
- export declare function Chart({ data, xKey, yKeys, height, margin: marginOverride, yBaseline, yDomain: yDomainProp, xDomain: xDomainProp, interactive, children, }: ChartProps): import("react").JSX.Element;
87
+ export declare function Chart({ data, xKey, yKeys, height, margin: marginOverride, yBaseline, yDomain: yDomainProp, xDomain: xDomainProp, interactive, label, children, }: ChartProps): import("react").JSX.Element;
77
88
  //# sourceMappingURL=Chart.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Chart.d.ts","sourceRoot":"","sources":["../../src/Chart/Chart.tsx"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH,OAAO,EAEL,KAAK,SAAS,EAMf,MAAM,OAAO,CAAC;AAKf,OAAO,KAAK,EAAC,WAAW,EAAa,MAAM,SAAS,CAAC;AAErD;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEjD,MAAM,WAAW,UAAU;IACzB,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAChC;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+BAA+B;IAC/B,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IAC9B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,6CAA6C;IAC7C,QAAQ,EAAE,SAAS,CAAC;CACrB;AAID;;;;;;;;;;;GAWG;AACH,wBAAgB,KAAK,CAAC,EACpB,IAAI,EACJ,IAAI,EACJ,KAAK,EACL,MAAY,EACZ,MAAM,EAAE,cAAc,EACtB,SAAkB,EAClB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,WAAW,EACpB,WAAmB,EACnB,QAAQ,GACT,EAAE,UAAU,+BA6MZ"}
1
+ {"version":3,"file":"Chart.d.ts","sourceRoot":"","sources":["../../src/Chart/Chart.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AAEH,OAAO,EAEL,KAAK,SAAS,EAMf,MAAM,OAAO,CAAC;AAMf,OAAO,KAAK,EAAC,WAAW,EAAa,MAAM,SAAS,CAAC;AAErD;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEjD,MAAM,WAAW,UAAU;IACzB,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAChC;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+BAA+B;IAC/B,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IAC9B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6CAA6C;IAC7C,QAAQ,EAAE,SAAS,CAAC;CACrB;AAgBD;;;;;;;;;;;GAWG;AACH,wBAAgB,KAAK,CAAC,EACpB,IAAI,EACJ,IAAI,EACJ,KAAK,EACL,MAAY,EACZ,MAAM,EAAE,cAAc,EACtB,SAAkB,EAClB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,WAAW,EACpB,WAAmB,EACnB,KAAK,EACL,QAAQ,GACT,EAAE,UAAU,+BA+OZ"}
@@ -7,10 +7,15 @@
7
7
  *
8
8
  * Handles the d3 margin convention: outer SVG at full size, inner <g> translated
9
9
  * by margins. Children receive scales and dimensions via context.
10
+ *
11
+ * Accessibility: the svg is exposed as a named image (role="img" + aria-label,
12
+ * overridable via `label`), and small datasets are mirrored in a visually
13
+ * hidden data table so screen reader users can read the underlying values.
10
14
  */
11
15
 
12
16
  import { useMemo, useRef, useState, useCallback, useLayoutEffect } from 'react';
13
17
  import { scaleLinear, scaleBand } from 'd3-scale';
18
+ import { VisuallyHidden } from '@astryxdesign/core/VisuallyHidden';
14
19
  import { isBandScale } from "./utils.js";
15
20
  import { ChartProvider } from "./ChartContext.js";
16
21
 
@@ -28,6 +33,18 @@ const DEFAULT_MARGIN = {
28
33
  left: 48
29
34
  };
30
35
 
36
+ /**
37
+ * Maximum number of data points (rows × series) for which the visually hidden
38
+ * data-table fallback is rendered. Beyond this a table is more noise than
39
+ * signal for screen reader users, so larger charts are name-only.
40
+ */
41
+ const MAX_TABLE_POINTS = 100;
42
+
43
+ /** Stringify a cell value for the hidden data table. */
44
+ function tableCell(value) {
45
+ return value == null ? '' : String(value);
46
+ }
47
+
31
48
  /**
32
49
  * Root chart container. Computes scales from data and provides them to children.
33
50
  *
@@ -50,6 +67,7 @@ export function Chart({
50
67
  yDomain: yDomainProp,
51
68
  xDomain: xDomainProp,
52
69
  interactive = false,
70
+ label,
53
71
  children
54
72
  }) {
55
73
  const containerRef = useRef(null);
@@ -215,11 +233,15 @@ export function Chart({
215
233
  touchAction: interactive ? 'none' : undefined,
216
234
  userSelect: interactive ? 'none' : undefined
217
235
  };
218
- return /*#__PURE__*/_jsx("div", {
236
+ const accessibleLabel = label ?? `Chart of ${yKeys.join(', ')} by ${xKey}`;
237
+ const showDataTable = data.length > 0 && data.length * Math.max(yKeys.length, 1) <= MAX_TABLE_POINTS;
238
+ return /*#__PURE__*/_jsxs("div", {
219
239
  ref: containerRef,
220
240
  style: containerStyle,
221
- children: containerWidth > 0 && /*#__PURE__*/_jsxs("svg", {
241
+ children: [containerWidth > 0 && /*#__PURE__*/_jsxs("svg", {
222
242
  ref: svgRef,
243
+ role: "img",
244
+ "aria-label": accessibleLabel,
223
245
  width: containerWidth,
224
246
  height: height,
225
247
  style: interactive ? {
@@ -242,6 +264,32 @@ export function Chart({
242
264
  children: children
243
265
  })
244
266
  })]
245
- })
267
+ }), showDataTable && /*#__PURE__*/_jsx(VisuallyHidden, {
268
+ as: "div",
269
+ children: /*#__PURE__*/_jsxs("table", {
270
+ children: [/*#__PURE__*/_jsx("caption", {
271
+ children: `${accessibleLabel} data`
272
+ }), /*#__PURE__*/_jsx("thead", {
273
+ children: /*#__PURE__*/_jsxs("tr", {
274
+ children: [/*#__PURE__*/_jsx("th", {
275
+ scope: "col",
276
+ children: xKey
277
+ }), yKeys.map(key => /*#__PURE__*/_jsx("th", {
278
+ scope: "col",
279
+ children: key
280
+ }, key))]
281
+ })
282
+ }), /*#__PURE__*/_jsx("tbody", {
283
+ children: data.map((d, i) => /*#__PURE__*/_jsxs("tr", {
284
+ children: [/*#__PURE__*/_jsx("th", {
285
+ scope: "row",
286
+ children: tableCell(d[xKey])
287
+ }), yKeys.map(key => /*#__PURE__*/_jsx("td", {
288
+ children: tableCell(d[key])
289
+ }, key))]
290
+ }, i))
291
+ })]
292
+ })
293
+ })]
246
294
  });
247
295
  }
@@ -23,12 +23,24 @@ export interface CodeEditorProps extends Omit<BaseProps<HTMLDivElement>, 'onChan
23
23
  size?: 'sm' | 'md';
24
24
  /** Custom tokenizer */
25
25
  tokenizer?: (code: string, language: string) => TokenLine[];
26
+ /**
27
+ * Hint exposed to assistive technology (via aria-describedby) explaining
28
+ * how to move focus out of the editor. English default — the lab package
29
+ * has no i18n catalog yet, so override this prop to localize.
30
+ * @default 'Press Escape then Tab to move focus out of the editor.'
31
+ */
32
+ escapeHint?: string;
26
33
  }
27
34
  /**
28
35
  * An editable code input using contentEditable="plaintext-only".
29
36
  *
30
37
  * Uses CSS Custom Highlight API for syntax coloring. Supports
31
- * auto-indent, tab insertion, and bracket auto-closing.
38
+ * auto-indent, tab insertion, Shift+Tab outdent, and bracket auto-closing.
39
+ *
40
+ * Because Tab is captured for indentation, pressing Escape arms a one-shot
41
+ * "tab moves focus" mode so keyboard users can leave the editor (WCAG 2.1.2).
42
+ * The escape is advertised to assistive technology via a visually hidden
43
+ * aria-describedby hint (see `escapeHint`).
32
44
  *
33
45
  * @example
34
46
  * ```
@@ -42,7 +54,7 @@ export interface CodeEditorProps extends Omit<BaseProps<HTMLDivElement>, 'onChan
42
54
  * />
43
55
  * ```
44
56
  */
45
- export declare function CodeEditor({ value, onChange, language, hasLineNumbers, isReadOnly, placeholder, maxHeight, size, tokenizer: customTokenizer, label, xstyle, className, style, ref, ...props }: CodeEditorProps): import("react").JSX.Element;
57
+ export declare function CodeEditor({ value, onChange, language, hasLineNumbers, isReadOnly, placeholder, maxHeight, size, tokenizer: customTokenizer, label, escapeHint, xstyle, className, style, ref, ...props }: CodeEditorProps): import("react").JSX.Element;
46
58
  export declare namespace CodeEditor {
47
59
  var displayName: string;
48
60
  }
@@ -1 +1 @@
1
- {"version":3,"file":"CodeEditor.d.ts","sourceRoot":"","sources":["../../src/CodeEditor/CodeEditor.tsx"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAiBlD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,8BAA8B,CAAC;AAuF5D,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAC3C,SAAS,CAAC,cAAc,CAAC,EACzB,UAAU,CACX;IACC,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC,uBAAuB;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,qBAAqB;IACrB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sFAAsF;IACtF,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,qCAAqC;IACrC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,6BAA6B;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kCAAkC;IAClC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,+BAA+B;IAC/B,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACnB,uBAAuB;IACvB,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,SAAS,EAAE,CAAC;CAC7D;AA6BD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,QAAsB,EACtB,cAAsB,EACtB,UAAkB,EAClB,WAAW,EACX,SAAS,EACT,IAAW,EACX,SAAS,EAAE,eAAe,EAC1B,KAAK,EACL,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,KAAK,EACT,EAAE,eAAe,+BAkQjB;yBAlRe,UAAU"}
1
+ {"version":3,"file":"CodeEditor.d.ts","sourceRoot":"","sources":["../../src/CodeEditor/CodeEditor.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAkBlD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,8BAA8B,CAAC;AAuF5D,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAC3C,SAAS,CAAC,cAAc,CAAC,EACzB,UAAU,CACX;IACC,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC,uBAAuB;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,qBAAqB;IACrB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sFAAsF;IACtF,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,qCAAqC;IACrC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,6BAA6B;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kCAAkC;IAClC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,+BAA+B;IAC/B,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACnB,uBAAuB;IACvB,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,SAAS,EAAE,CAAC;IAC5D;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAsED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,QAAsB,EACtB,cAAsB,EACtB,UAAkB,EAClB,WAAW,EACX,SAAS,EACT,IAAW,EACX,SAAS,EAAE,eAAe,EAC1B,KAAK,EACL,UAAqE,EACrE,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,KAAK,EACT,EAAE,eAAe,+BAwUjB;yBAzVe,UAAU"}
@@ -2,7 +2,8 @@
2
2
 
3
3
  /**
4
4
  * @file CodeEditor.tsx
5
- * @input Uses React, StyleX, theme tokens, CSS Custom Highlight API
5
+ * @input Uses React, StyleX, theme tokens, CSS Custom Highlight API,
6
+ * VisuallyHidden (core)
6
7
  * @output Exports CodeEditor component and CodeEditorProps
7
8
  * @position Core implementation; editable code input (lab/experimental)
8
9
  *
@@ -14,7 +15,8 @@
14
15
 
15
16
  'use client';
16
17
 
17
- import { useEffect, useLayoutEffect, useRef, useCallback, useState } from 'react';
18
+ import { useEffect, useId, useLayoutEffect, useRef, useCallback, useState } from 'react';
19
+ import { VisuallyHidden } from '@astryxdesign/core/VisuallyHidden';
18
20
  import * as stylex from '@stylexjs/stylex';
19
21
  import '@astryxdesign/core/theme/tokens.stylex';
20
22
  import { colorVars, spacingVars, radiusVars, textSizeVars, typographyVars, typeScaleVars, borderVars } from '@astryxdesign/core/theme/tokens.stylex';
@@ -116,6 +118,41 @@ const AUTO_CLOSE_PAIRS = {
116
118
  "'": "'",
117
119
  '`': '`'
118
120
  };
121
+
122
+ /**
123
+ * Modifier keys that must NOT re-arm Tab indentation after Escape has armed
124
+ * "tab moves focus" mode — otherwise the Shift keydown that precedes
125
+ * Shift+Tab would cancel the mode before Tab arrives.
126
+ */
127
+ const MODE_NEUTRAL_KEYS = new Set(['Shift', 'Control', 'Alt', 'Meta', 'CapsLock']);
128
+
129
+ /** Place a collapsed caret at a character offset within an element's text. */
130
+ function setCaretAtTextOffset(el, offset) {
131
+ const sel = window.getSelection();
132
+ if (!sel) {
133
+ return;
134
+ }
135
+ const range = document.createRange();
136
+ let remaining = offset;
137
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
138
+ let node = walker.nextNode();
139
+ while (node) {
140
+ const length = node.textContent?.length ?? 0;
141
+ if (remaining <= length) {
142
+ range.setStart(node, remaining);
143
+ range.collapse(true);
144
+ sel.removeAllRanges();
145
+ sel.addRange(range);
146
+ return;
147
+ }
148
+ remaining -= length;
149
+ node = walker.nextNode();
150
+ }
151
+ range.selectNodeContents(el);
152
+ range.collapse(false);
153
+ sel.removeAllRanges();
154
+ sel.addRange(range);
155
+ }
119
156
  let editorInstanceCounter = 0;
120
157
 
121
158
  // ---------------------------------------------------------------------------
@@ -126,7 +163,12 @@ let editorInstanceCounter = 0;
126
163
  * An editable code input using contentEditable="plaintext-only".
127
164
  *
128
165
  * Uses CSS Custom Highlight API for syntax coloring. Supports
129
- * auto-indent, tab insertion, and bracket auto-closing.
166
+ * auto-indent, tab insertion, Shift+Tab outdent, and bracket auto-closing.
167
+ *
168
+ * Because Tab is captured for indentation, pressing Escape arms a one-shot
169
+ * "tab moves focus" mode so keyboard users can leave the editor (WCAG 2.1.2).
170
+ * The escape is advertised to assistive technology via a visually hidden
171
+ * aria-describedby hint (see `escapeHint`).
130
172
  *
131
173
  * @example
132
174
  * ```
@@ -151,6 +193,7 @@ export function CodeEditor({
151
193
  size = 'md',
152
194
  tokenizer: customTokenizer,
153
195
  label,
196
+ escapeHint = 'Press Escape then Tab to move focus out of the editor.',
154
197
  xstyle,
155
198
  className,
156
199
  style,
@@ -161,6 +204,10 @@ export function CodeEditor({
161
204
  const [instanceId] = useState(() => ++editorInstanceCounter);
162
205
  const [focused, setFocused] = useState(false);
163
206
  const isComposingRef = useRef(false);
207
+ // One-shot "tab moves focus" mode, armed by Escape (WCAG 2.1.2 — the
208
+ // standard escape from a code editor's Tab-to-indent keyboard trap).
209
+ const tabMovesFocusRef = useRef(false);
210
+ const hintId = useId();
164
211
  const lines = value.split('\n');
165
212
 
166
213
  // Sync textContent with controlled value
@@ -243,13 +290,56 @@ export function CodeEditor({
243
290
  return;
244
291
  }
245
292
 
246
- // Tab key: insert 2 spaces
293
+ // Escape arms one-shot "tab moves focus" mode so keyboard users can
294
+ // leave the editor. Only engage when nothing inside the editor (e.g.
295
+ // an embedded popover/autocomplete) already consumed the Escape.
296
+ if (e.key === 'Escape') {
297
+ if (!e.defaultPrevented) {
298
+ tabMovesFocusRef.current = true;
299
+ }
300
+ return;
301
+ }
247
302
  if (e.key === 'Tab') {
303
+ // Tab-moves-focus mode: let the browser handle Tab / Shift+Tab
304
+ // natively so focus leaves the editor. One-shot — disarm.
305
+ if (tabMovesFocusRef.current) {
306
+ tabMovesFocusRef.current = false;
307
+ return;
308
+ }
248
309
  e.preventDefault();
249
310
  const sel = window.getSelection();
250
311
  if (!sel || sel.rangeCount === 0) {
251
312
  return;
252
313
  }
314
+ const el = editorRef.current;
315
+ if (!el) {
316
+ return;
317
+ }
318
+
319
+ // Shift+Tab: outdent — remove up to two leading spaces on the
320
+ // current line.
321
+ if (e.shiftKey) {
322
+ const range = sel.getRangeAt(0);
323
+ const preCaretRange = range.cloneRange();
324
+ preCaretRange.selectNodeContents(el);
325
+ preCaretRange.setEnd(range.startContainer, range.startOffset);
326
+ const cursorOffset = preCaretRange.toString().length;
327
+ const fullText = el.textContent ?? '';
328
+ const lineStart = fullText.lastIndexOf('\n', cursorOffset - 1) + 1;
329
+ const line = fullText.slice(lineStart);
330
+ const removeCount = line.startsWith(' ') ? 2 : line.startsWith(' ') ? 1 : 0;
331
+ if (removeCount === 0) {
332
+ return;
333
+ }
334
+ const newText = fullText.slice(0, lineStart) + fullText.slice(lineStart + removeCount);
335
+ el.textContent = newText;
336
+ const removedBeforeCursor = Math.min(removeCount, Math.max(0, cursorOffset - lineStart));
337
+ setCaretAtTextOffset(el, cursorOffset - removedBeforeCursor);
338
+ onChange(newText);
339
+ return;
340
+ }
341
+
342
+ // Tab: insert 2 spaces
253
343
  const range = sel.getRangeAt(0);
254
344
  range.deleteContents();
255
345
  const textNode = document.createTextNode(' ');
@@ -258,13 +348,17 @@ export function CodeEditor({
258
348
  range.setEndAfter(textNode);
259
349
  sel.removeAllRanges();
260
350
  sel.addRange(range);
261
- const el = editorRef.current;
262
- if (el) {
263
- onChange(el.textContent ?? '');
264
- }
351
+ onChange(el.textContent ?? '');
265
352
  return;
266
353
  }
267
354
 
355
+ // Any other non-modifier key re-arms Tab indentation. Modifiers are
356
+ // excluded so Escape → Shift+Tab still moves focus backward (the
357
+ // Shift keydown fires before the Tab keydown).
358
+ if (!MODE_NEUTRAL_KEYS.has(e.key)) {
359
+ tabMovesFocusRef.current = false;
360
+ }
361
+
268
362
  // Enter key: preserve indentation
269
363
  if (e.key === 'Enter') {
270
364
  e.preventDefault();
@@ -383,14 +477,21 @@ export function CodeEditor({
383
477
  "aria-multiline": "true",
384
478
  "aria-label": label,
385
479
  "aria-readonly": isReadOnly,
480
+ "aria-describedby": isReadOnly ? undefined : hintId,
386
481
  spellCheck: false,
387
482
  onInput: handleInput,
388
483
  onKeyDown: handleKeyDown,
389
484
  onFocus: () => setFocused(true),
390
- onBlur: () => setFocused(false),
485
+ onBlur: () => {
486
+ setFocused(false);
487
+ tabMovesFocusRef.current = false;
488
+ },
391
489
  onCompositionStart: handleCompositionStart,
392
490
  onCompositionEnd: handleCompositionEnd,
393
491
  ...stylex.props(styles.editor, sizeStyle)
492
+ }), !isReadOnly && /*#__PURE__*/_jsx(VisuallyHidden, {
493
+ id: hintId,
494
+ children: escapeHint
394
495
  })]
395
496
  })]
396
497
  })
@@ -5,6 +5,10 @@
5
5
  *
6
6
  * Owns the radial coordinate space: center, radius, angle/radius scales.
7
7
  * Same Tier 1 guarantee as Chart — all children map through the same context.
8
+ *
9
+ * Accessibility: the svg is exposed as a named image (role="img" + aria-label,
10
+ * overridable via `label`), and small datasets are mirrored in a visually
11
+ * hidden data table (slices in pie mode, axis values in spider mode).
8
12
  */
9
13
  import { type ReactNode } from 'react';
10
14
  export interface RadialChartProps {
@@ -40,6 +44,13 @@ export interface RadialChartProps {
40
44
  * Enable touch interaction mode — blocks scroll on mobile.
41
45
  */
42
46
  interactive?: boolean;
47
+ /**
48
+ * Accessible name for the chart, announced by screen readers (the svg is
49
+ * exposed as `role="img"`). Defaults to an English description derived from
50
+ * the mode and keys (e.g. "Radar chart of speed, handling" or
51
+ * "Pie chart of revenue") — pass a localized string to override.
52
+ */
53
+ label?: string;
43
54
  children: ReactNode;
44
55
  }
45
56
  /**
@@ -61,5 +72,5 @@ export interface RadialChartProps {
61
72
  * </RadialChart>
62
73
  * ```
63
74
  */
64
- export declare function RadialChart({ data, height, axes, valueKey, labelKey, innerRadius: innerRadiusFraction, padAngle, children, }: RadialChartProps): import("react").JSX.Element;
75
+ export declare function RadialChart({ data, height, axes, valueKey, labelKey, innerRadius: innerRadiusFraction, padAngle, label, children, }: RadialChartProps): import("react").JSX.Element;
65
76
  //# sourceMappingURL=RadialChart.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RadialChart.d.ts","sourceRoot":"","sources":["../../src/Radial/RadialChart.tsx"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH,OAAO,EACL,KAAK,SAAS,EAKf,MAAM,OAAO,CAAC;AAIf,MAAM,WAAW,gBAAgB;IAC/B,kBAAkB;IAClB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAChC,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qBAAqB;IACrB;;OAEG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,MAAY,EACZ,IAAI,EACJ,QAAQ,EACR,QAAQ,EACR,WAAW,EAAE,mBAAuB,EACpC,QAAe,EACf,QAAQ,GACT,EAAE,gBAAgB,+BAgIlB"}
1
+ {"version":3,"file":"RadialChart.d.ts","sourceRoot":"","sources":["../../src/Radial/RadialChart.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AAEH,OAAO,EACL,KAAK,SAAS,EAKf,MAAM,OAAO,CAAC;AAKf,MAAM,WAAW,gBAAgB;IAC/B,kBAAkB;IAClB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAChC,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qBAAqB;IACrB;;OAEG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,SAAS,CAAC;CACrB;AAkBD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,MAAY,EACZ,IAAI,EACJ,QAAQ,EACR,QAAQ,EACR,WAAW,EAAE,mBAAuB,EACpC,QAAe,EACf,KAAK,EACL,QAAQ,GACT,EAAE,gBAAgB,+BAwMlB"}
@@ -7,11 +7,32 @@
7
7
  *
8
8
  * Owns the radial coordinate space: center, radius, angle/radius scales.
9
9
  * Same Tier 1 guarantee as Chart — all children map through the same context.
10
+ *
11
+ * Accessibility: the svg is exposed as a named image (role="img" + aria-label,
12
+ * overridable via `label`), and small datasets are mirrored in a visually
13
+ * hidden data table (slices in pie mode, axis values in spider mode).
10
14
  */
11
15
 
12
16
  import { useMemo, useRef, useState, useLayoutEffect } from 'react';
17
+ import { VisuallyHidden } from '@astryxdesign/core/VisuallyHidden';
13
18
  import { RadialProvider } from "./RadialContext.js";
14
- import { jsx as _jsx } from "react/jsx-runtime";
19
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
20
+ /**
21
+ * Maximum number of data points (rows × axes in spider mode, slices in pie
22
+ * mode) for which the visually hidden data-table fallback is rendered. Beyond
23
+ * this a table is more noise than signal, so larger charts are name-only.
24
+ */
25
+ const MAX_TABLE_POINTS = 100;
26
+
27
+ /**
28
+ * Row header for a spider-mode table row: the first string field in the row
29
+ * (the same heuristic RadialArea uses to match its `dataKey`), else the index.
30
+ */
31
+ function spiderRowHeader(row, index) {
32
+ const name = Object.values(row).find(v => typeof v === 'string');
33
+ return typeof name === 'string' ? name : `Series ${index + 1}`;
34
+ }
35
+
15
36
  /**
16
37
  * Root radial chart container. Computes angular/radial scales and provides
17
38
  * them to children via context.
@@ -39,6 +60,7 @@ export function RadialChart({
39
60
  labelKey,
40
61
  innerRadius: innerRadiusFraction = 0,
41
62
  padAngle = 0.02,
63
+ label,
42
64
  children
43
65
  }) {
44
66
  const containerRef = useRef(null);
@@ -152,18 +174,81 @@ export function RadialChart({
152
174
  ...spiderCtx,
153
175
  ...pieCtx
154
176
  }), [cx, cy, outerRadius, innerRadiusPx, data, mode, spiderCtx, pieCtx]);
155
- return /*#__PURE__*/_jsx("div", {
177
+ const accessibleLabel = label ?? (mode === 'spider' ? `Radar chart of ${(axes ?? []).join(', ')}` : `Pie chart of ${valueKey ?? 'values'}`);
178
+ const slices = pieCtx.slices ?? [];
179
+ const showPieTable = mode === 'pie' && slices.length > 0 && slices.length <= MAX_TABLE_POINTS;
180
+ const showSpiderTable = mode === 'spider' && axes != null && axes.length > 0 && data.length > 0 && data.length * axes.length <= MAX_TABLE_POINTS;
181
+ return /*#__PURE__*/_jsxs("div", {
156
182
  ref: containerRef,
157
183
  style: {
158
184
  width: '100%'
159
185
  },
160
- children: containerWidth > 0 && /*#__PURE__*/_jsx("svg", {
186
+ children: [containerWidth > 0 && /*#__PURE__*/_jsx("svg", {
187
+ role: "img",
188
+ "aria-label": accessibleLabel,
161
189
  width: containerWidth,
162
190
  height: height,
163
191
  children: /*#__PURE__*/_jsx(RadialProvider, {
164
192
  value: ctx,
165
193
  children: children
166
194
  })
167
- })
195
+ }), showPieTable && /*#__PURE__*/_jsx(VisuallyHidden, {
196
+ as: "div",
197
+ children: /*#__PURE__*/_jsxs("table", {
198
+ children: [/*#__PURE__*/_jsx("caption", {
199
+ children: `${accessibleLabel} data`
200
+ }), /*#__PURE__*/_jsx("thead", {
201
+ children: /*#__PURE__*/_jsxs("tr", {
202
+ children: [/*#__PURE__*/_jsx("th", {
203
+ scope: "col",
204
+ children: labelKey ?? 'Label'
205
+ }), /*#__PURE__*/_jsx("th", {
206
+ scope: "col",
207
+ children: valueKey
208
+ }), /*#__PURE__*/_jsx("th", {
209
+ scope: "col",
210
+ children: "Percentage"
211
+ })]
212
+ })
213
+ }), /*#__PURE__*/_jsx("tbody", {
214
+ children: slices.map((slice, i) => /*#__PURE__*/_jsxs("tr", {
215
+ children: [/*#__PURE__*/_jsx("th", {
216
+ scope: "row",
217
+ children: slice.key
218
+ }), /*#__PURE__*/_jsx("td", {
219
+ children: String(slice.value)
220
+ }), /*#__PURE__*/_jsx("td", {
221
+ children: `${(slice.percentage * 100).toFixed(1)}%`
222
+ })]
223
+ }, i))
224
+ })]
225
+ })
226
+ }), showSpiderTable && /*#__PURE__*/_jsx(VisuallyHidden, {
227
+ as: "div",
228
+ children: /*#__PURE__*/_jsxs("table", {
229
+ children: [/*#__PURE__*/_jsx("caption", {
230
+ children: `${accessibleLabel} data`
231
+ }), /*#__PURE__*/_jsx("thead", {
232
+ children: /*#__PURE__*/_jsxs("tr", {
233
+ children: [/*#__PURE__*/_jsx("th", {
234
+ scope: "col",
235
+ children: "Series"
236
+ }), axes.map(key => /*#__PURE__*/_jsx("th", {
237
+ scope: "col",
238
+ children: key
239
+ }, key))]
240
+ })
241
+ }), /*#__PURE__*/_jsx("tbody", {
242
+ children: data.map((row, i) => /*#__PURE__*/_jsxs("tr", {
243
+ children: [/*#__PURE__*/_jsx("th", {
244
+ scope: "row",
245
+ children: spiderRowHeader(row, i)
246
+ }), axes.map(key => /*#__PURE__*/_jsx("td", {
247
+ children: row[key] == null ? '' : String(row[key])
248
+ }, key))]
249
+ }, i))
250
+ })]
251
+ })
252
+ })]
168
253
  });
169
254
  }
@@ -5,6 +5,10 @@
5
5
  *
6
6
  * When columns × minColumnWidth exceeds the container, the chart
7
7
  * scrolls horizontally rather than squishing columns together.
8
+ *
9
+ * Accessibility: the svg is exposed as a named image (role="img" + aria-label,
10
+ * overridable via `label`) in both the scrolling and non-scrolling paths, and
11
+ * small graphs are mirrored in a visually hidden From/To/Value table.
8
12
  */
9
13
  import { type ReactNode } from 'react';
10
14
  import type { SankeyNodeDatum, SankeyLinkDatum, SankeyColumn } from './types';
@@ -39,6 +43,14 @@ export interface SankeyChartProps {
39
43
  * When total min width exceeds the container, horizontal scrolling activates.
40
44
  */
41
45
  minColumnWidth?: number;
46
+ /**
47
+ * Accessible name for the chart, announced by screen readers. Applied to the
48
+ * svg (`role="img"`) and, when the chart scrolls, to the focusable scroll
49
+ * region. Pass a localized string to override the English default.
50
+ *
51
+ * @default 'Sankey chart'
52
+ */
53
+ label?: string;
42
54
  /** Chart contents */
43
55
  children: ReactNode;
44
56
  }
@@ -64,7 +76,7 @@ export interface SankeyChartProps {
64
76
  * </SankeyChart>
65
77
  * ```
66
78
  */
67
- export declare function SankeyChart({ nodes, links, columns, height, nodeWidth, nodeGap, nodeColor, minColumnWidth, children, }: SankeyChartProps): import("react").JSX.Element;
79
+ export declare function SankeyChart({ nodes, links, columns, height, nodeWidth, nodeGap, nodeColor, minColumnWidth, label, children, }: SankeyChartProps): import("react").JSX.Element;
68
80
  export declare namespace SankeyChart {
69
81
  var displayName: string;
70
82
  }
@@ -1 +1 @@
1
- {"version":3,"file":"SankeyChart.d.ts","sourceRoot":"","sources":["../../src/Sankey/SankeyChart.tsx"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH,OAAO,EACL,KAAK,SAAS,EAKf,MAAM,OAAO,CAAC;AAGf,OAAO,KAAK,EAAC,eAAe,EAAE,eAAe,EAAE,YAAY,EAAC,MAAM,SAAS,CAAC;AAE5E,MAAM,WAAW,gBAAgB;IAC/B,uBAAuB;IACvB,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,uBAAuB;IACvB,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IACzB,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qBAAqB;IACrB,QAAQ,EAAE,SAAS,CAAC;CACrB;AA+CD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,KAAK,EACL,OAAO,EACP,MAAY,EACZ,SAAc,EACd,OAAY,EACZ,SAAS,EACT,cAAoB,EACpB,QAAQ,GACT,EAAE,gBAAgB,+BAqFlB;yBA/Fe,WAAW"}
1
+ {"version":3,"file":"SankeyChart.d.ts","sourceRoot":"","sources":["../../src/Sankey/SankeyChart.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AAEH,OAAO,EACL,KAAK,SAAS,EAKf,MAAM,OAAO,CAAC;AAIf,OAAO,KAAK,EAAC,eAAe,EAAE,eAAe,EAAE,YAAY,EAAC,MAAM,SAAS,CAAC;AAE5E,MAAM,WAAW,gBAAgB;IAC/B,uBAAuB;IACvB,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,uBAAuB;IACvB,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IACzB,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAsDD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,KAAK,EACL,OAAO,EACP,MAAY,EACZ,SAAc,EACd,OAAY,EACZ,SAAS,EACT,cAAoB,EACpB,KAAsB,EACtB,QAAQ,GACT,EAAE,gBAAgB,+BAsHlB;yBAjIe,WAAW"}