@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.
- package/dist/Chart/Chart.d.ts +12 -1
- package/dist/Chart/Chart.d.ts.map +1 -1
- package/dist/Chart/Chart.js +51 -3
- package/dist/CodeEditor/CodeEditor.d.ts +14 -2
- package/dist/CodeEditor/CodeEditor.d.ts.map +1 -1
- package/dist/CodeEditor/CodeEditor.js +110 -9
- package/dist/Radial/RadialChart.d.ts +12 -1
- package/dist/Radial/RadialChart.d.ts.map +1 -1
- package/dist/Radial/RadialChart.js +89 -4
- package/dist/Sankey/SankeyChart.d.ts +13 -1
- package/dist/Sankey/SankeyChart.d.ts.map +1 -1
- package/dist/Sankey/SankeyChart.js +53 -5
- package/dist/ThreeD/ThreeDChart.d.ts +16 -3
- package/dist/ThreeD/ThreeDChart.d.ts.map +1 -1
- package/dist/ThreeD/ThreeDChart.js +63 -5
- package/package.json +2 -2
- package/src/Chart/Chart.test.tsx +126 -0
- package/src/Chart/Chart.tsx +59 -0
- package/src/CodeEditor/CodeEditor.test.tsx +172 -2
- package/src/CodeEditor/CodeEditor.tsx +142 -9
- package/src/Radial/RadialChart.test.tsx +157 -0
- package/src/Radial/RadialChart.tsx +102 -1
- package/src/Sankey/SankeyChart.test.tsx +95 -0
- package/src/Sankey/SankeyChart.tsx +55 -1
- package/src/ThreeD/ThreeDChart.test.tsx +251 -0
- package/src/ThreeD/ThreeDChart.tsx +84 -6
package/dist/Chart/Chart.d.ts
CHANGED
|
@@ -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
|
|
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"}
|
package/dist/Chart/Chart.js
CHANGED
|
@@ -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
|
-
|
|
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":"
|
|
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
|
-
//
|
|
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
|
-
|
|
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: () =>
|
|
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
|
|
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
|
-
|
|
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
|
|
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"}
|