@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
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @file ThreeDChart.tsx
|
|
5
|
-
* @output Root 3D chart container — projected SVG with depth sorting
|
|
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)
|
|
6
8
|
* @position Parent component; all 3D marks read from its context
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import {
|
|
10
12
|
type CSSProperties,
|
|
13
|
+
type KeyboardEvent as ReactKeyboardEvent,
|
|
11
14
|
type ReactNode,
|
|
12
15
|
useMemo,
|
|
13
16
|
useRef,
|
|
@@ -16,6 +19,7 @@ import {
|
|
|
16
19
|
useLayoutEffect,
|
|
17
20
|
useCallback,
|
|
18
21
|
} from 'react';
|
|
22
|
+
import {useMediaQuery} from '@astryxdesign/core/hooks';
|
|
19
23
|
import {ThreeDProvider} from './ThreeDContext';
|
|
20
24
|
import type {Camera, ProjectedPoint} from './types';
|
|
21
25
|
|
|
@@ -31,11 +35,25 @@ export interface ThreeDChartProps {
|
|
|
31
35
|
azimuth?: number;
|
|
32
36
|
elevation?: number;
|
|
33
37
|
interactive?: boolean;
|
|
34
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Auto-rotate speed in degrees per frame (default: 0 = off).
|
|
40
|
+
*
|
|
41
|
+
* Rotation pauses while the chart is hovered or focused, and is disabled
|
|
42
|
+
* entirely when the user has `prefers-reduced-motion: reduce` set. Pass 0
|
|
43
|
+
* (or omit) to turn it off.
|
|
44
|
+
*/
|
|
35
45
|
autoRotate?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Accessible label for the chart. Defaults to a description derived from
|
|
48
|
+
* the data keys, e.g. "3D chart of sales by month and region".
|
|
49
|
+
*/
|
|
50
|
+
label?: string;
|
|
36
51
|
children: ReactNode;
|
|
37
52
|
}
|
|
38
53
|
|
|
54
|
+
/** Degrees the camera rotates per arrow-key press. */
|
|
55
|
+
const KEYBOARD_STEP_DEGREES = 10;
|
|
56
|
+
|
|
39
57
|
function computeDomain(
|
|
40
58
|
data: Record<string, unknown>[],
|
|
41
59
|
key: string,
|
|
@@ -69,10 +87,16 @@ export function ThreeDChart({
|
|
|
69
87
|
elevation: elevationProp = 25,
|
|
70
88
|
interactive = false,
|
|
71
89
|
autoRotate = 0,
|
|
90
|
+
label,
|
|
72
91
|
children,
|
|
73
92
|
}: ThreeDChartProps) {
|
|
74
93
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
75
94
|
const [containerWidth, setContainerWidth] = useState(0);
|
|
95
|
+
const [hovered, setHovered] = useState(false);
|
|
96
|
+
const [focused, setFocused] = useState(false);
|
|
97
|
+
const prefersReducedMotion = useMediaQuery(
|
|
98
|
+
'(prefers-reduced-motion: reduce)',
|
|
99
|
+
);
|
|
76
100
|
const [camera, setCamera] = useState<Camera>({
|
|
77
101
|
azimuth: azimuthProp,
|
|
78
102
|
elevation: elevationProp,
|
|
@@ -168,9 +192,48 @@ export function ThreeDChart({
|
|
|
168
192
|
dragRef.current = null;
|
|
169
193
|
}, []);
|
|
170
194
|
|
|
171
|
-
//
|
|
195
|
+
// Keyboard camera: arrow keys rotate yaw/pitch with the same clamps as
|
|
196
|
+
// pointer drag (WCAG 2.1.1 — every pointer operation needs a keyboard
|
|
197
|
+
// equivalent). preventDefault stops the page from scrolling while focused.
|
|
198
|
+
const handleKeyDown = useCallback(
|
|
199
|
+
(e: ReactKeyboardEvent<SVGSVGElement>) => {
|
|
200
|
+
if (!interactive) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
let deltaAzimuth = 0;
|
|
204
|
+
let deltaElevation = 0;
|
|
205
|
+
switch (e.key) {
|
|
206
|
+
case 'ArrowLeft':
|
|
207
|
+
deltaAzimuth = -KEYBOARD_STEP_DEGREES;
|
|
208
|
+
break;
|
|
209
|
+
case 'ArrowRight':
|
|
210
|
+
deltaAzimuth = KEYBOARD_STEP_DEGREES;
|
|
211
|
+
break;
|
|
212
|
+
case 'ArrowUp':
|
|
213
|
+
deltaElevation = KEYBOARD_STEP_DEGREES;
|
|
214
|
+
break;
|
|
215
|
+
case 'ArrowDown':
|
|
216
|
+
deltaElevation = -KEYBOARD_STEP_DEGREES;
|
|
217
|
+
break;
|
|
218
|
+
default:
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
e.preventDefault();
|
|
222
|
+
setCamera(prev => ({
|
|
223
|
+
azimuth: prev.azimuth + deltaAzimuth,
|
|
224
|
+
elevation: Math.max(-89, Math.min(89, prev.elevation + deltaElevation)),
|
|
225
|
+
}));
|
|
226
|
+
},
|
|
227
|
+
[interactive],
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// Auto-rotation — throttled to ~20fps to avoid overwhelming React with
|
|
231
|
+
// re-renders. Disabled under prefers-reduced-motion (WCAG 2.3.3) and
|
|
232
|
+
// paused while the chart is hovered or focused (WCAG 2.2.2).
|
|
233
|
+
const rotationActive =
|
|
234
|
+
autoRotate !== 0 && !prefersReducedMotion && !hovered && !focused;
|
|
172
235
|
useEffect(() => {
|
|
173
|
-
if (
|
|
236
|
+
if (!rotationActive) {
|
|
174
237
|
return;
|
|
175
238
|
}
|
|
176
239
|
let raf: number;
|
|
@@ -187,7 +250,7 @@ export function ThreeDChart({
|
|
|
187
250
|
};
|
|
188
251
|
raf = requestAnimationFrame(tick);
|
|
189
252
|
return () => cancelAnimationFrame(raf);
|
|
190
|
-
}, [autoRotate]);
|
|
253
|
+
}, [autoRotate, rotationActive]);
|
|
191
254
|
|
|
192
255
|
const ctx = useMemo(
|
|
193
256
|
() => ({
|
|
@@ -226,16 +289,31 @@ export function ThreeDChart({
|
|
|
226
289
|
userSelect: interactive ? 'none' : undefined,
|
|
227
290
|
};
|
|
228
291
|
|
|
292
|
+
const accessibleLabel = label ?? `3D chart of ${yKey} by ${xKey} and ${zKey}`;
|
|
293
|
+
// Focusable when there is something keyboard users can do with it:
|
|
294
|
+
// rotate the camera (interactive) or pause the auto-rotation.
|
|
295
|
+
const focusable = interactive || autoRotate !== 0;
|
|
296
|
+
|
|
229
297
|
return (
|
|
230
298
|
<div ref={containerRef} style={containerStyle}>
|
|
231
299
|
{containerWidth > 0 && (
|
|
232
300
|
<svg
|
|
301
|
+
role="img"
|
|
302
|
+
aria-label={accessibleLabel}
|
|
303
|
+
tabIndex={focusable ? 0 : undefined}
|
|
233
304
|
width={containerWidth}
|
|
234
305
|
height={height}
|
|
306
|
+
onKeyDown={handleKeyDown}
|
|
307
|
+
onFocus={() => setFocused(true)}
|
|
308
|
+
onBlur={() => setFocused(false)}
|
|
309
|
+
onMouseEnter={() => setHovered(true)}
|
|
235
310
|
onMouseDown={e => handleStart(e.clientX, e.clientY)}
|
|
236
311
|
onMouseMove={e => handleMove(e.clientX, e.clientY)}
|
|
237
312
|
onMouseUp={handleEnd}
|
|
238
|
-
onMouseLeave={
|
|
313
|
+
onMouseLeave={() => {
|
|
314
|
+
setHovered(false);
|
|
315
|
+
handleEnd();
|
|
316
|
+
}}
|
|
239
317
|
onTouchStart={e => {
|
|
240
318
|
const t = e.touches[0];
|
|
241
319
|
if (t) {
|