@astryxdesign/lab 0.2.0-canary.ea693ac → 0.2.0-canary.f306f13
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 +1 -12
- package/dist/Chart/Chart.d.ts.map +1 -1
- package/dist/Chart/Chart.js +3 -51
- package/dist/CodeEditor/CodeEditor.d.ts +2 -14
- package/dist/CodeEditor/CodeEditor.d.ts.map +1 -1
- package/dist/CodeEditor/CodeEditor.js +9 -110
- package/dist/Radial/RadialChart.d.ts +1 -12
- package/dist/Radial/RadialChart.d.ts.map +1 -1
- package/dist/Radial/RadialChart.js +4 -89
- package/dist/Sankey/SankeyChart.d.ts +1 -13
- package/dist/Sankey/SankeyChart.d.ts.map +1 -1
- package/dist/Sankey/SankeyChart.js +5 -53
- package/dist/ThreeD/ThreeDChart.d.ts +3 -16
- package/dist/ThreeD/ThreeDChart.d.ts.map +1 -1
- package/dist/ThreeD/ThreeDChart.js +5 -63
- package/package.json +2 -2
- package/src/Chart/Chart.tsx +0 -59
- package/src/CodeEditor/CodeEditor.test.tsx +2 -172
- package/src/CodeEditor/CodeEditor.tsx +9 -142
- package/src/Radial/RadialChart.tsx +1 -102
- package/src/Sankey/SankeyChart.test.tsx +0 -95
- package/src/Sankey/SankeyChart.tsx +1 -55
- package/src/ThreeD/ThreeDChart.tsx +6 -84
- package/src/Chart/Chart.test.tsx +0 -126
- package/src/Radial/RadialChart.test.tsx +0 -157
- package/src/ThreeD/ThreeDChart.test.tsx +0 -251
|
@@ -7,24 +7,12 @@
|
|
|
7
7
|
*
|
|
8
8
|
* When columns × minColumnWidth exceeds the container, the chart
|
|
9
9
|
* scrolls horizontally rather than squishing columns together.
|
|
10
|
-
*
|
|
11
|
-
* Accessibility: the svg is exposed as a named image (role="img" + aria-label,
|
|
12
|
-
* overridable via `label`) in both the scrolling and non-scrolling paths, and
|
|
13
|
-
* small graphs are mirrored in a visually hidden From/To/Value table.
|
|
14
10
|
*/
|
|
15
11
|
|
|
16
12
|
import { useMemo, useRef, useState, useLayoutEffect } from 'react';
|
|
17
|
-
import { VisuallyHidden } from '@astryxdesign/core/VisuallyHidden';
|
|
18
13
|
import { SankeyProvider } from "./SankeyContext.js";
|
|
19
14
|
import { computeLayout } from "./layout.js";
|
|
20
|
-
import { jsx as _jsx
|
|
21
|
-
/**
|
|
22
|
-
* Maximum number of links for which the visually hidden data-table fallback is
|
|
23
|
-
* rendered. Beyond this a table is more noise than signal, so larger charts
|
|
24
|
-
* are name-only.
|
|
25
|
-
*/
|
|
26
|
-
const MAX_TABLE_LINKS = 100;
|
|
27
|
-
|
|
15
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
28
16
|
/** Resolve column count from raw column input or auto-detect from graph */
|
|
29
17
|
function resolveColumnCount(columns, nodes, links) {
|
|
30
18
|
if (columns) {
|
|
@@ -97,7 +85,6 @@ export function SankeyChart({
|
|
|
97
85
|
nodeGap = 14,
|
|
98
86
|
nodeColor,
|
|
99
87
|
minColumnWidth = 160,
|
|
100
|
-
label = 'Sankey chart',
|
|
101
88
|
children
|
|
102
89
|
}) {
|
|
103
90
|
const containerRef = useRef(null);
|
|
@@ -146,24 +133,20 @@ export function SankeyChart({
|
|
|
146
133
|
nodeColor
|
|
147
134
|
};
|
|
148
135
|
}, [layout, chartWidth, height, nodeWidth, nodeColor]);
|
|
149
|
-
|
|
150
|
-
const showDataTable = links.length > 0 && links.length <= MAX_TABLE_LINKS;
|
|
151
|
-
return /*#__PURE__*/_jsxs("div", {
|
|
136
|
+
return /*#__PURE__*/_jsx("div", {
|
|
152
137
|
ref: containerRef,
|
|
153
138
|
style: {
|
|
154
139
|
width: '100%'
|
|
155
140
|
},
|
|
156
|
-
children:
|
|
141
|
+
children: ctx && /*#__PURE__*/_jsx("div", {
|
|
157
142
|
role: needsScroll ? 'group' : undefined,
|
|
158
|
-
"aria-label": needsScroll ?
|
|
143
|
+
"aria-label": needsScroll ? 'Sankey chart' : undefined,
|
|
159
144
|
tabIndex: needsScroll ? 0 : undefined,
|
|
160
145
|
style: needsScroll ? {
|
|
161
146
|
overflowX: 'auto',
|
|
162
147
|
overflowY: 'hidden'
|
|
163
148
|
} : undefined,
|
|
164
149
|
children: /*#__PURE__*/_jsx("svg", {
|
|
165
|
-
role: "img",
|
|
166
|
-
"aria-label": label,
|
|
167
150
|
width: chartWidth,
|
|
168
151
|
height: height,
|
|
169
152
|
style: {
|
|
@@ -175,38 +158,7 @@ export function SankeyChart({
|
|
|
175
158
|
children: children
|
|
176
159
|
})
|
|
177
160
|
})
|
|
178
|
-
})
|
|
179
|
-
as: "div",
|
|
180
|
-
children: /*#__PURE__*/_jsxs("table", {
|
|
181
|
-
children: [/*#__PURE__*/_jsx("caption", {
|
|
182
|
-
children: `${label} data`
|
|
183
|
-
}), /*#__PURE__*/_jsx("thead", {
|
|
184
|
-
children: /*#__PURE__*/_jsxs("tr", {
|
|
185
|
-
children: [/*#__PURE__*/_jsx("th", {
|
|
186
|
-
scope: "col",
|
|
187
|
-
children: "From"
|
|
188
|
-
}), /*#__PURE__*/_jsx("th", {
|
|
189
|
-
scope: "col",
|
|
190
|
-
children: "To"
|
|
191
|
-
}), /*#__PURE__*/_jsx("th", {
|
|
192
|
-
scope: "col",
|
|
193
|
-
children: "Value"
|
|
194
|
-
})]
|
|
195
|
-
})
|
|
196
|
-
}), /*#__PURE__*/_jsx("tbody", {
|
|
197
|
-
children: links.map((link, i) => /*#__PURE__*/_jsxs("tr", {
|
|
198
|
-
children: [/*#__PURE__*/_jsx("th", {
|
|
199
|
-
scope: "row",
|
|
200
|
-
children: nodeLabelById.get(link.source) ?? link.source
|
|
201
|
-
}), /*#__PURE__*/_jsx("td", {
|
|
202
|
-
children: nodeLabelById.get(link.target) ?? link.target
|
|
203
|
-
}), /*#__PURE__*/_jsx("td", {
|
|
204
|
-
children: String(link.value)
|
|
205
|
-
})]
|
|
206
|
-
}, i))
|
|
207
|
-
})]
|
|
208
|
-
})
|
|
209
|
-
})]
|
|
161
|
+
})
|
|
210
162
|
});
|
|
211
163
|
}
|
|
212
164
|
SankeyChart.displayName = 'SankeyChart';
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file ThreeDChart.tsx
|
|
3
|
-
* @output Root 3D chart container — projected SVG with depth sorting
|
|
4
|
-
* accessible name, pointer + keyboard camera controls, and pausable
|
|
5
|
-
* auto-rotation (respects prefers-reduced-motion)
|
|
3
|
+
* @output Root 3D chart container — projected SVG with depth sorting
|
|
6
4
|
* @position Parent component; all 3D marks read from its context
|
|
7
5
|
*/
|
|
8
6
|
import { type ReactNode } from 'react';
|
|
@@ -18,20 +16,9 @@ export interface ThreeDChartProps {
|
|
|
18
16
|
azimuth?: number;
|
|
19
17
|
elevation?: number;
|
|
20
18
|
interactive?: boolean;
|
|
21
|
-
/**
|
|
22
|
-
* Auto-rotate speed in degrees per frame (default: 0 = off).
|
|
23
|
-
*
|
|
24
|
-
* Rotation pauses while the chart is hovered or focused, and is disabled
|
|
25
|
-
* entirely when the user has `prefers-reduced-motion: reduce` set. Pass 0
|
|
26
|
-
* (or omit) to turn it off.
|
|
27
|
-
*/
|
|
19
|
+
/** Auto-rotate speed in degrees per frame (default: 0 = off) */
|
|
28
20
|
autoRotate?: number;
|
|
29
|
-
/**
|
|
30
|
-
* Accessible label for the chart. Defaults to a description derived from
|
|
31
|
-
* the data keys, e.g. "3D chart of sales by month and region".
|
|
32
|
-
*/
|
|
33
|
-
label?: string;
|
|
34
21
|
children: ReactNode;
|
|
35
22
|
}
|
|
36
|
-
export declare function ThreeDChart({ data, xKey, yKey, zKey, xDomain: xDomainProp, yDomain: yDomainProp, zDomain: zDomainProp, height, azimuth: azimuthProp, elevation: elevationProp, interactive, autoRotate,
|
|
23
|
+
export declare function ThreeDChart({ data, xKey, yKey, zKey, xDomain: xDomainProp, yDomain: yDomainProp, zDomain: zDomainProp, height, azimuth: azimuthProp, elevation: elevationProp, interactive, autoRotate, children, }: ThreeDChartProps): import("react").JSX.Element;
|
|
37
24
|
//# sourceMappingURL=ThreeDChart.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThreeDChart.d.ts","sourceRoot":"","sources":["../../src/ThreeD/ThreeDChart.tsx"],"names":[],"mappings":"AAEA
|
|
1
|
+
{"version":3,"file":"ThreeDChart.d.ts","sourceRoot":"","sources":["../../src/ThreeD/ThreeDChart.tsx"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH,OAAO,EAEL,KAAK,SAAS,EAOf,MAAM,OAAO,CAAC;AAIf,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAsBD,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,IAAI,EACJ,IAAI,EACJ,IAAI,EACJ,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,WAAW,EACpB,MAAY,EACZ,OAAO,EAAE,WAAgB,EACzB,SAAS,EAAE,aAAkB,EAC7B,WAAmB,EACnB,UAAc,EACd,QAAQ,GACT,EAAE,gBAAgB,+BA6LlB"}
|
|
@@ -2,18 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @file ThreeDChart.tsx
|
|
5
|
-
* @output Root 3D chart container — projected SVG with depth sorting
|
|
6
|
-
* accessible name, pointer + keyboard camera controls, and pausable
|
|
7
|
-
* auto-rotation (respects prefers-reduced-motion)
|
|
5
|
+
* @output Root 3D chart container — projected SVG with depth sorting
|
|
8
6
|
* @position Parent component; all 3D marks read from its context
|
|
9
7
|
*/
|
|
10
8
|
|
|
11
9
|
import { useMemo, useRef, useState, useEffect, useLayoutEffect, useCallback } from 'react';
|
|
12
|
-
import { useMediaQuery } from '@astryxdesign/core/hooks';
|
|
13
10
|
import { ThreeDProvider } from "./ThreeDContext.js";
|
|
14
11
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
15
|
-
/** Degrees the camera rotates per arrow-key press. */
|
|
16
|
-
const KEYBOARD_STEP_DEGREES = 10;
|
|
17
12
|
function computeDomain(data, key) {
|
|
18
13
|
let min = Infinity,
|
|
19
14
|
max = -Infinity;
|
|
@@ -43,14 +38,10 @@ export function ThreeDChart({
|
|
|
43
38
|
elevation: elevationProp = 25,
|
|
44
39
|
interactive = false,
|
|
45
40
|
autoRotate = 0,
|
|
46
|
-
label,
|
|
47
41
|
children
|
|
48
42
|
}) {
|
|
49
43
|
const containerRef = useRef(null);
|
|
50
44
|
const [containerWidth, setContainerWidth] = useState(0);
|
|
51
|
-
const [hovered, setHovered] = useState(false);
|
|
52
|
-
const [focused, setFocused] = useState(false);
|
|
53
|
-
const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
|
|
54
45
|
const [camera, setCamera] = useState({
|
|
55
46
|
azimuth: azimuthProp,
|
|
56
47
|
elevation: elevationProp
|
|
@@ -132,44 +123,9 @@ export function ThreeDChart({
|
|
|
132
123
|
dragRef.current = null;
|
|
133
124
|
}, []);
|
|
134
125
|
|
|
135
|
-
//
|
|
136
|
-
// pointer drag (WCAG 2.1.1 — every pointer operation needs a keyboard
|
|
137
|
-
// equivalent). preventDefault stops the page from scrolling while focused.
|
|
138
|
-
const handleKeyDown = useCallback(e => {
|
|
139
|
-
if (!interactive) {
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
let deltaAzimuth = 0;
|
|
143
|
-
let deltaElevation = 0;
|
|
144
|
-
switch (e.key) {
|
|
145
|
-
case 'ArrowLeft':
|
|
146
|
-
deltaAzimuth = -KEYBOARD_STEP_DEGREES;
|
|
147
|
-
break;
|
|
148
|
-
case 'ArrowRight':
|
|
149
|
-
deltaAzimuth = KEYBOARD_STEP_DEGREES;
|
|
150
|
-
break;
|
|
151
|
-
case 'ArrowUp':
|
|
152
|
-
deltaElevation = KEYBOARD_STEP_DEGREES;
|
|
153
|
-
break;
|
|
154
|
-
case 'ArrowDown':
|
|
155
|
-
deltaElevation = -KEYBOARD_STEP_DEGREES;
|
|
156
|
-
break;
|
|
157
|
-
default:
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
e.preventDefault();
|
|
161
|
-
setCamera(prev => ({
|
|
162
|
-
azimuth: prev.azimuth + deltaAzimuth,
|
|
163
|
-
elevation: Math.max(-89, Math.min(89, prev.elevation + deltaElevation))
|
|
164
|
-
}));
|
|
165
|
-
}, [interactive]);
|
|
166
|
-
|
|
167
|
-
// Auto-rotation — throttled to ~20fps to avoid overwhelming React with
|
|
168
|
-
// re-renders. Disabled under prefers-reduced-motion (WCAG 2.3.3) and
|
|
169
|
-
// paused while the chart is hovered or focused (WCAG 2.2.2).
|
|
170
|
-
const rotationActive = autoRotate !== 0 && !prefersReducedMotion && !hovered && !focused;
|
|
126
|
+
// Auto-rotation — throttled to ~20fps to avoid overwhelming React with re-renders
|
|
171
127
|
useEffect(() => {
|
|
172
|
-
if (
|
|
128
|
+
if (autoRotate === 0) {
|
|
173
129
|
return;
|
|
174
130
|
}
|
|
175
131
|
let raf;
|
|
@@ -189,7 +145,7 @@ export function ThreeDChart({
|
|
|
189
145
|
};
|
|
190
146
|
raf = requestAnimationFrame(tick);
|
|
191
147
|
return () => cancelAnimationFrame(raf);
|
|
192
|
-
}, [autoRotate
|
|
148
|
+
}, [autoRotate]);
|
|
193
149
|
const ctx = useMemo(() => ({
|
|
194
150
|
width,
|
|
195
151
|
height,
|
|
@@ -209,30 +165,16 @@ export function ThreeDChart({
|
|
|
209
165
|
touchAction: interactive ? 'none' : undefined,
|
|
210
166
|
userSelect: interactive ? 'none' : undefined
|
|
211
167
|
};
|
|
212
|
-
const accessibleLabel = label ?? `3D chart of ${yKey} by ${xKey} and ${zKey}`;
|
|
213
|
-
// Focusable when there is something keyboard users can do with it:
|
|
214
|
-
// rotate the camera (interactive) or pause the auto-rotation.
|
|
215
|
-
const focusable = interactive || autoRotate !== 0;
|
|
216
168
|
return /*#__PURE__*/_jsx("div", {
|
|
217
169
|
ref: containerRef,
|
|
218
170
|
style: containerStyle,
|
|
219
171
|
children: containerWidth > 0 && /*#__PURE__*/_jsx("svg", {
|
|
220
|
-
role: "img",
|
|
221
|
-
"aria-label": accessibleLabel,
|
|
222
|
-
tabIndex: focusable ? 0 : undefined,
|
|
223
172
|
width: containerWidth,
|
|
224
173
|
height: height,
|
|
225
|
-
onKeyDown: handleKeyDown,
|
|
226
|
-
onFocus: () => setFocused(true),
|
|
227
|
-
onBlur: () => setFocused(false),
|
|
228
|
-
onMouseEnter: () => setHovered(true),
|
|
229
174
|
onMouseDown: e => handleStart(e.clientX, e.clientY),
|
|
230
175
|
onMouseMove: e => handleMove(e.clientX, e.clientY),
|
|
231
176
|
onMouseUp: handleEnd,
|
|
232
|
-
onMouseLeave:
|
|
233
|
-
setHovered(false);
|
|
234
|
-
handleEnd();
|
|
235
|
-
},
|
|
177
|
+
onMouseLeave: handleEnd,
|
|
236
178
|
onTouchStart: e => {
|
|
237
179
|
const t = e.touches[0];
|
|
238
180
|
if (t) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/lab",
|
|
3
|
-
"version": "0.2.0-canary.
|
|
3
|
+
"version": "0.2.0-canary.f306f13",
|
|
4
4
|
"description": "Experimental Astryx components — published to npm only under the @canary dist-tag for early testing; never released as a stable (latest) version.",
|
|
5
5
|
"author": "Meta Open Source",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@stylexjs/stylex": ">=0.10.0",
|
|
48
|
-
"@astryxdesign/core": "0.2.0-canary.
|
|
48
|
+
"@astryxdesign/core": "0.2.0-canary.f306f13",
|
|
49
49
|
"react": ">=19.0.0",
|
|
50
50
|
"react-dom": ">=19.0.0",
|
|
51
51
|
"lexical": "^0.46.0",
|
package/src/Chart/Chart.tsx
CHANGED
|
@@ -7,10 +7,6 @@
|
|
|
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.
|
|
14
10
|
*/
|
|
15
11
|
|
|
16
12
|
import {
|
|
@@ -24,7 +20,6 @@ import {
|
|
|
24
20
|
} from 'react';
|
|
25
21
|
import {scaleLinear, scaleBand} from 'd3-scale';
|
|
26
22
|
import type {ScaleLinear} from 'd3-scale';
|
|
27
|
-
import {VisuallyHidden} from '@astryxdesign/core/VisuallyHidden';
|
|
28
23
|
import {isBandScale} from './utils';
|
|
29
24
|
import {ChartProvider} from './ChartContext';
|
|
30
25
|
import type {ChartMargin, ChartScale} from './types';
|
|
@@ -80,31 +75,12 @@ export interface ChartProps {
|
|
|
80
75
|
* interference on mobile.
|
|
81
76
|
*/
|
|
82
77
|
interactive?: boolean;
|
|
83
|
-
/**
|
|
84
|
-
* Accessible name for the chart, announced by screen readers (the svg is
|
|
85
|
-
* exposed as `role="img"`). Defaults to an English description derived from
|
|
86
|
-
* the keys (e.g. "Chart of revenue by month") — pass a localized string to
|
|
87
|
-
* override.
|
|
88
|
-
*/
|
|
89
|
-
label?: string;
|
|
90
78
|
/** Chart contents — axes, marks, tooltips */
|
|
91
79
|
children: ReactNode;
|
|
92
80
|
}
|
|
93
81
|
|
|
94
82
|
const DEFAULT_MARGIN: ChartMargin = {top: 16, right: 16, bottom: 32, left: 48};
|
|
95
83
|
|
|
96
|
-
/**
|
|
97
|
-
* Maximum number of data points (rows × series) for which the visually hidden
|
|
98
|
-
* data-table fallback is rendered. Beyond this a table is more noise than
|
|
99
|
-
* signal for screen reader users, so larger charts are name-only.
|
|
100
|
-
*/
|
|
101
|
-
const MAX_TABLE_POINTS = 100;
|
|
102
|
-
|
|
103
|
-
/** Stringify a cell value for the hidden data table. */
|
|
104
|
-
function tableCell(value: unknown): string {
|
|
105
|
-
return value == null ? '' : String(value);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
84
|
/**
|
|
109
85
|
* Root chart container. Computes scales from data and provides them to children.
|
|
110
86
|
*
|
|
@@ -127,7 +103,6 @@ export function Chart({
|
|
|
127
103
|
yDomain: yDomainProp,
|
|
128
104
|
xDomain: xDomainProp,
|
|
129
105
|
interactive = false,
|
|
130
|
-
label,
|
|
131
106
|
children,
|
|
132
107
|
}: ChartProps) {
|
|
133
108
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
@@ -314,18 +289,11 @@ export function Chart({
|
|
|
314
289
|
userSelect: interactive ? 'none' : undefined,
|
|
315
290
|
};
|
|
316
291
|
|
|
317
|
-
const accessibleLabel = label ?? `Chart of ${yKeys.join(', ')} by ${xKey}`;
|
|
318
|
-
const showDataTable =
|
|
319
|
-
data.length > 0 &&
|
|
320
|
-
data.length * Math.max(yKeys.length, 1) <= MAX_TABLE_POINTS;
|
|
321
|
-
|
|
322
292
|
return (
|
|
323
293
|
<div ref={containerRef} style={containerStyle}>
|
|
324
294
|
{containerWidth > 0 && (
|
|
325
295
|
<svg
|
|
326
296
|
ref={svgRef}
|
|
327
|
-
role="img"
|
|
328
|
-
aria-label={accessibleLabel}
|
|
329
297
|
width={containerWidth}
|
|
330
298
|
height={height}
|
|
331
299
|
style={interactive ? {touchAction: 'none'} : undefined}>
|
|
@@ -339,33 +307,6 @@ export function Chart({
|
|
|
339
307
|
</g>
|
|
340
308
|
</svg>
|
|
341
309
|
)}
|
|
342
|
-
{showDataTable && (
|
|
343
|
-
<VisuallyHidden as="div">
|
|
344
|
-
<table>
|
|
345
|
-
<caption>{`${accessibleLabel} data`}</caption>
|
|
346
|
-
<thead>
|
|
347
|
-
<tr>
|
|
348
|
-
<th scope="col">{xKey}</th>
|
|
349
|
-
{yKeys.map(key => (
|
|
350
|
-
<th key={key} scope="col">
|
|
351
|
-
{key}
|
|
352
|
-
</th>
|
|
353
|
-
))}
|
|
354
|
-
</tr>
|
|
355
|
-
</thead>
|
|
356
|
-
<tbody>
|
|
357
|
-
{data.map((d, i) => (
|
|
358
|
-
<tr key={i}>
|
|
359
|
-
<th scope="row">{tableCell(d[xKey])}</th>
|
|
360
|
-
{yKeys.map(key => (
|
|
361
|
-
<td key={key}>{tableCell(d[key])}</td>
|
|
362
|
-
))}
|
|
363
|
-
</tr>
|
|
364
|
-
))}
|
|
365
|
-
</tbody>
|
|
366
|
-
</table>
|
|
367
|
-
</VisuallyHidden>
|
|
368
|
-
)}
|
|
369
310
|
</div>
|
|
370
311
|
);
|
|
371
312
|
}
|
|
@@ -1,55 +1,9 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* @input Uses vitest, @testing-library/react, @testing-library/user-event, CodeEditor
|
|
6
|
-
* @output Unit tests for CodeEditor accessibility and keyboard behavior
|
|
7
|
-
* @position Testing; validates CodeEditor.tsx implementation
|
|
8
|
-
*
|
|
9
|
-
* SYNC: When CodeEditor.tsx changes, update tests to match new behavior
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import {describe, it, expect, vi} from 'vitest';
|
|
13
|
-
import {render, screen, fireEvent} from '@testing-library/react';
|
|
14
|
-
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import {describe, it, expect} from 'vitest';
|
|
4
|
+
import {render, screen} from '@testing-library/react';
|
|
15
5
|
import {CodeEditor} from './CodeEditor';
|
|
16
6
|
|
|
17
|
-
/** Place a collapsed caret at a character offset inside the editor. */
|
|
18
|
-
function setCaret(el: HTMLElement, offset: number) {
|
|
19
|
-
const sel = window.getSelection();
|
|
20
|
-
const range = document.createRange();
|
|
21
|
-
const node = el.firstChild;
|
|
22
|
-
if (node && node.nodeType === Node.TEXT_NODE) {
|
|
23
|
-
range.setStart(node, offset);
|
|
24
|
-
} else {
|
|
25
|
-
range.setStart(el, 0);
|
|
26
|
-
}
|
|
27
|
-
range.collapse(true);
|
|
28
|
-
sel?.removeAllRanges();
|
|
29
|
-
sel?.addRange(range);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function renderEditor(value = '', props: {escapeHint?: string} = {}) {
|
|
33
|
-
const onChange = vi.fn();
|
|
34
|
-
render(
|
|
35
|
-
<div>
|
|
36
|
-
<button>before</button>
|
|
37
|
-
<CodeEditor
|
|
38
|
-
value={value}
|
|
39
|
-
onChange={onChange}
|
|
40
|
-
language="typescript"
|
|
41
|
-
label="Edit snippet"
|
|
42
|
-
{...props}
|
|
43
|
-
/>
|
|
44
|
-
<button>after</button>
|
|
45
|
-
</div>,
|
|
46
|
-
);
|
|
47
|
-
const editor = screen.getByRole('textbox', {name: 'Edit snippet'});
|
|
48
|
-
editor.focus();
|
|
49
|
-
setCaret(editor, value.length);
|
|
50
|
-
return {editor, onChange};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
7
|
describe('CodeEditor', () => {
|
|
54
8
|
it('uses the label prop as the accessible name', () => {
|
|
55
9
|
render(
|
|
@@ -64,128 +18,4 @@ describe('CodeEditor', () => {
|
|
|
64
18
|
screen.getByRole('textbox', {name: 'Edit snippet'}),
|
|
65
19
|
).toBeInTheDocument();
|
|
66
20
|
});
|
|
67
|
-
|
|
68
|
-
it('advertises the Escape-then-Tab escape via aria-describedby', () => {
|
|
69
|
-
const {editor} = renderEditor();
|
|
70
|
-
const hintId = editor.getAttribute('aria-describedby');
|
|
71
|
-
expect(hintId).toBeTruthy();
|
|
72
|
-
const hint = document.getElementById(hintId as string);
|
|
73
|
-
expect(hint).toHaveTextContent(
|
|
74
|
-
'Press Escape then Tab to move focus out of the editor.',
|
|
75
|
-
);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('supports overriding the escape hint text', () => {
|
|
79
|
-
const {editor} = renderEditor('', {escapeHint: 'Échap puis Tab'});
|
|
80
|
-
const hintId = editor.getAttribute('aria-describedby');
|
|
81
|
-
const hint = document.getElementById(hintId as string);
|
|
82
|
-
expect(hint).toHaveTextContent('Échap puis Tab');
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('Tab inserts two spaces by default and keeps focus in the editor', async () => {
|
|
86
|
-
const user = userEvent.setup();
|
|
87
|
-
const {editor, onChange} = renderEditor();
|
|
88
|
-
|
|
89
|
-
await user.keyboard('{Tab}');
|
|
90
|
-
|
|
91
|
-
expect(onChange).toHaveBeenCalledWith(' ');
|
|
92
|
-
expect(document.activeElement).toBe(editor);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('Escape then Tab moves focus out of the editor without indenting', async () => {
|
|
96
|
-
const user = userEvent.setup();
|
|
97
|
-
const {editor, onChange} = renderEditor();
|
|
98
|
-
|
|
99
|
-
await user.keyboard('{Escape}{Tab}');
|
|
100
|
-
|
|
101
|
-
expect(document.activeElement).not.toBe(editor);
|
|
102
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('Escape then Shift+Tab lets the browser move focus backward', async () => {
|
|
106
|
-
// user-event cannot compute a backward tab destination from a
|
|
107
|
-
// contenteditable element in jsdom, so assert on the preventDefault
|
|
108
|
-
// contract: an unprevented Shift+Tab keydown means the browser performs
|
|
109
|
-
// its native backward focus move.
|
|
110
|
-
const user = userEvent.setup();
|
|
111
|
-
const {editor, onChange} = renderEditor();
|
|
112
|
-
|
|
113
|
-
await user.keyboard('{Escape}');
|
|
114
|
-
fireEvent.keyDown(editor, {key: 'Shift'});
|
|
115
|
-
const notPrevented = fireEvent.keyDown(editor, {
|
|
116
|
-
key: 'Tab',
|
|
117
|
-
shiftKey: true,
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
expect(notPrevented).toBe(true);
|
|
121
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('tab-moves-focus mode is one-shot: the following Tab indents again', async () => {
|
|
125
|
-
const user = userEvent.setup();
|
|
126
|
-
const {editor, onChange} = renderEditor();
|
|
127
|
-
|
|
128
|
-
await user.keyboard('{Escape}{Tab}');
|
|
129
|
-
expect(document.activeElement).not.toBe(editor);
|
|
130
|
-
|
|
131
|
-
editor.focus();
|
|
132
|
-
setCaret(editor, 0);
|
|
133
|
-
await user.keyboard('{Tab}');
|
|
134
|
-
|
|
135
|
-
expect(onChange).toHaveBeenCalledWith(' ');
|
|
136
|
-
expect(document.activeElement).toBe(editor);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('typing after Escape re-arms Tab indentation', async () => {
|
|
140
|
-
const user = userEvent.setup();
|
|
141
|
-
const {editor, onChange} = renderEditor();
|
|
142
|
-
|
|
143
|
-
await user.keyboard('{Escape}');
|
|
144
|
-
// Any non-modifier key re-arms indentation mode.
|
|
145
|
-
fireEvent.keyDown(editor, {key: 'a'});
|
|
146
|
-
await user.keyboard('{Tab}');
|
|
147
|
-
|
|
148
|
-
expect(document.activeElement).toBe(editor);
|
|
149
|
-
expect(onChange).toHaveBeenCalledWith(' ');
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('Shift+Tab outdents the current line in normal mode', async () => {
|
|
153
|
-
const user = userEvent.setup();
|
|
154
|
-
const {editor, onChange} = renderEditor(' const x = 1;');
|
|
155
|
-
|
|
156
|
-
await user.keyboard('{Shift>}{Tab}{/Shift}');
|
|
157
|
-
|
|
158
|
-
expect(onChange).toHaveBeenCalledWith('const x = 1;');
|
|
159
|
-
expect(document.activeElement).toBe(editor);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('Shift+Tab removes a single leading space when only one exists', async () => {
|
|
163
|
-
const user = userEvent.setup();
|
|
164
|
-
const {onChange} = renderEditor(' const x = 1;');
|
|
165
|
-
|
|
166
|
-
await user.keyboard('{Shift>}{Tab}{/Shift}');
|
|
167
|
-
|
|
168
|
-
expect(onChange).toHaveBeenCalledWith('const x = 1;');
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('Shift+Tab on an unindented line changes nothing and keeps focus', async () => {
|
|
172
|
-
const user = userEvent.setup();
|
|
173
|
-
const {editor, onChange} = renderEditor('const x = 1;');
|
|
174
|
-
|
|
175
|
-
await user.keyboard('{Shift>}{Tab}{/Shift}');
|
|
176
|
-
|
|
177
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
178
|
-
expect(document.activeElement).toBe(editor);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it('Shift+Tab outdents only the current line in multi-line code', async () => {
|
|
182
|
-
const user = userEvent.setup();
|
|
183
|
-
const {editor, onChange} = renderEditor(' a;\n b;');
|
|
184
|
-
// Caret on the second line (offset 7 = inside " b;").
|
|
185
|
-
setCaret(editor, 7);
|
|
186
|
-
|
|
187
|
-
await user.keyboard('{Shift>}{Tab}{/Shift}');
|
|
188
|
-
|
|
189
|
-
expect(onChange).toHaveBeenCalledWith(' a;\nb;');
|
|
190
|
-
});
|
|
191
21
|
});
|