@astryxdesign/lab 0.7.8-canary.36cde5e → 0.7.8-canary.55aaeda

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.
@@ -1 +1 @@
1
- {"version":3,"file":"ChartZoom.d.ts","sourceRoot":"","sources":["../../src/Chart/ChartZoom.tsx"],"names":[],"mappings":"AAmBA,6CAA6C;AAC7C,MAAM,MAAM,mBAAmB,GAC7B,WAAW,GAAG,UAAU,GAAG,cAAc,GAAG,aAAa,CAAC;AAE5D,MAAM,WAAW,cAAc;IAC7B,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,CAAC;IACrD,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,CAAC;IAErD;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB;;;;OAIG;IACH,OAAO,CAAC,EAAE,KAAK,GAAG,mBAAmB,CAAC;CACvC;AAiDD,wBAAgB,SAAS,CAAC,EACxB,eAAe,EACf,eAAe,EACf,SAAe,EACf,KAAa,EACb,KAAa,EACb,OAAqB,GACtB,EAAE,cAAc,+BAwShB"}
1
+ {"version":3,"file":"ChartZoom.d.ts","sourceRoot":"","sources":["../../src/Chart/ChartZoom.tsx"],"names":[],"mappings":"AAmBA,6CAA6C;AAC7C,MAAM,MAAM,mBAAmB,GAC7B,WAAW,GAAG,UAAU,GAAG,cAAc,GAAG,aAAa,CAAC;AAE5D,MAAM,WAAW,cAAc;IAC7B,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,CAAC;IACrD,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,CAAC;IAErD;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB;;;;OAIG;IACH,OAAO,CAAC,EAAE,KAAK,GAAG,mBAAmB,CAAC;CACvC;AAiDD,wBAAgB,SAAS,CAAC,EACxB,eAAe,EACf,eAAe,EACf,SAAe,EACf,KAAa,EACb,KAAa,EACb,OAAqB,GACtB,EAAE,cAAc,+BA6RhB"}
@@ -7,10 +7,10 @@
7
7
  *
8
8
  * Zoom is aspect-ratio-normalized: both axes scale by the same visual
9
9
  * factor regardless of their domain ranges. Reset button restores
10
- * the initial domains captured on mount.
10
+ * the domains of the first render.
11
11
  */
12
12
 
13
- import { useCallback, useRef, useEffect } from 'react';
13
+ import { useCallback, useRef, useState } from 'react';
14
14
  import { createPortal } from 'react-dom';
15
15
  import { IconButton } from '@astryxdesign/core/IconButton';
16
16
  import { useChart } from "./ChartContext.js";
@@ -77,20 +77,10 @@ export function ChartZoom({
77
77
  yScale,
78
78
  svgRef
79
79
  } = useChart();
80
-
81
- // Capture initial domains on mount for reset
82
- const initialDomainsRef = useRef(null);
83
- useEffect(() => {
84
- if (initialDomainsRef.current) {
85
- return;
86
- } // only capture once
87
- const xDomain = isBandScale(xScale) ? null : xScale.domain();
88
- const yDomain = yScale.domain();
89
- initialDomainsRef.current = {
90
- x: xDomain,
91
- y: yDomain
92
- };
93
- }, [xScale, yScale]);
80
+ const [initialDomains] = useState(() => ({
81
+ x: isBandScale(xScale) ? null : xScale.domain(),
82
+ y: yScale.domain()
83
+ }));
94
84
  const dragRef = useRef(null);
95
85
  const pointersRef = useRef(new Map());
96
86
  const pinchRef = useRef(null);
@@ -219,15 +209,11 @@ export function ChartZoom({
219
209
  }
220
210
  }, []);
221
211
  const handleReset = useCallback(() => {
222
- const init = initialDomainsRef.current;
223
- if (!init) {
224
- return;
225
- }
226
- if (init.x) {
227
- onXDomainChange?.(init.x);
212
+ if (initialDomains.x) {
213
+ onXDomainChange?.(initialDomains.x);
228
214
  }
229
- onYDomainChange?.(init.y);
230
- }, [onXDomainChange, onYDomainChange]);
215
+ onYDomainChange?.(initialDomains.y);
216
+ }, [initialDomains, onXDomainChange, onYDomainChange]);
231
217
 
232
218
  // Toolbar portal target
233
219
  const portalTarget = svgRef.current?.parentElement;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/lab",
3
- "version": "0.7.8-canary.36cde5e",
3
+ "version": "0.7.8-canary.55aaeda",
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",
@@ -49,7 +49,7 @@
49
49
  },
50
50
  "peerDependencies": {
51
51
  "@stylexjs/stylex": ">=0.10.0",
52
- "@astryxdesign/core": "0.7.8-canary.36cde5e",
52
+ "@astryxdesign/core": "0.7.8-canary.55aaeda",
53
53
  "react": ">=19.0.0",
54
54
  "react-dom": ">=19.0.0"
55
55
  },
@@ -59,9 +59,9 @@
59
59
  "d3-shape": "^3.2.0"
60
60
  },
61
61
  "devDependencies": {
62
- "@astryxdesign/a11y-spec": "0.7.8-canary.36cde5e",
63
- "@astryxdesign/cli": "0.7.8-canary.36cde5e",
64
- "@astryxdesign/interaction-harness": "0.7.8-canary.36cde5e",
62
+ "@astryxdesign/a11y-spec": "0.7.8-canary.55aaeda",
63
+ "@astryxdesign/cli": "0.7.8-canary.55aaeda",
64
+ "@astryxdesign/interaction-harness": "0.7.8-canary.55aaeda",
65
65
  "@babel/cli": "^7.29.7",
66
66
  "@babel/core": "^7.29.7",
67
67
  "@babel/preset-react": "^7.29.7",
@@ -3,14 +3,15 @@
3
3
  /**
4
4
  * @file ChartZoom.test.tsx
5
5
  * @input Renders ChartZoom inside Chart with a stubbed ResizeObserver
6
- * @output Render-smoke coverage for zoom: capture surface, portaled toolbar, toolbar opt-out.
6
+ * @output Render-smoke coverage for zoom: capture surface, portaled toolbar, toolbar opt-out,
7
+ * and Reset back to the first render's domains.
7
8
  * Wheel/pinch gestures need createSVGPoint/getScreenCTM, which do not exist in
8
9
  * jsdom, so they are intentionally out of scope here. (#4295)
9
10
  * @position Colocated test for ChartZoom.tsx
10
11
  */
11
12
 
12
13
  import {describe, it, expect, beforeEach, afterEach, vi} from 'vitest';
13
- import {render, screen, act} from '@testing-library/react';
14
+ import {render, screen, act, fireEvent} from '@testing-library/react';
14
15
  import {Chart} from './Chart';
15
16
  import {ChartZoom} from './ChartZoom';
16
17
 
@@ -87,6 +88,44 @@ describe('ChartZoom', () => {
87
88
  ).toBeInTheDocument();
88
89
  });
89
90
 
91
+ it("resets to the first render's domains after the caller changes them", () => {
92
+ const onXDomainChange = vi.fn();
93
+ const onYDomainChange = vi.fn();
94
+ function ZoomedChart({
95
+ xDomain,
96
+ yDomain,
97
+ }: {
98
+ xDomain: [number, number];
99
+ yDomain: [number, number];
100
+ }) {
101
+ return (
102
+ <Chart
103
+ data={data}
104
+ xKey="x"
105
+ yKeys={['y']}
106
+ xDomain={xDomain}
107
+ yDomain={yDomain}
108
+ interactive>
109
+ <ChartZoom
110
+ onXDomainChange={onXDomainChange}
111
+ onYDomainChange={onYDomainChange}
112
+ />
113
+ </Chart>
114
+ );
115
+ }
116
+ const {rerender} = render(
117
+ <ZoomedChart xDomain={[0, 2]} yDomain={[0, 4]} />,
118
+ );
119
+ reportWidth(464);
120
+ reportWidth(465);
121
+
122
+ rerender(<ZoomedChart xDomain={[0.5, 1.5]} yDomain={[1, 3]} />);
123
+ fireEvent.click(screen.getByRole('button', {name: 'Reset zoom'}));
124
+
125
+ expect(onXDomainChange).toHaveBeenLastCalledWith([0, 2]);
126
+ expect(onYDomainChange).toHaveBeenLastCalledWith([0, 4]);
127
+ });
128
+
90
129
  it('renders no toolbar when toolbar is false', () => {
91
130
  render(
92
131
  <Chart data={data} xKey="x" yKeys={['y']} interactive>
@@ -7,10 +7,10 @@
7
7
  *
8
8
  * Zoom is aspect-ratio-normalized: both axes scale by the same visual
9
9
  * factor regardless of their domain ranges. Reset button restores
10
- * the initial domains captured on mount.
10
+ * the domains of the first render.
11
11
  */
12
12
 
13
- import {type CSSProperties, useCallback, useRef, useEffect} from 'react';
13
+ import {type CSSProperties, useCallback, useRef, useState} from 'react';
14
14
  import {createPortal} from 'react-dom';
15
15
  import {IconButton} from '@astryxdesign/core/IconButton';
16
16
  import {useChart} from './ChartContext';
@@ -101,22 +101,15 @@ export function ChartZoom({
101
101
  }: ChartZoomProps) {
102
102
  const {width, height, xScale, yScale, svgRef} = useChart();
103
103
 
104
- // Capture initial domains on mount for reset
105
- const initialDomainsRef = useRef<{
104
+ const [initialDomains] = useState<{
106
105
  x: [number, number] | null;
107
106
  y: [number, number];
108
- } | null>(null);
109
-
110
- useEffect(() => {
111
- if (initialDomainsRef.current) {
112
- return;
113
- } // only capture once
114
- const xDomain = isBandScale(xScale)
107
+ }>(() => ({
108
+ x: isBandScale(xScale)
115
109
  ? null
116
- : ((xScale as ScaleLinear<number, number>).domain() as [number, number]);
117
- const yDomain = yScale.domain() as [number, number];
118
- initialDomainsRef.current = {x: xDomain, y: yDomain};
119
- }, [xScale, yScale]);
110
+ : ((xScale as ScaleLinear<number, number>).domain() as [number, number]),
111
+ y: yScale.domain() as [number, number],
112
+ }));
120
113
 
121
114
  const dragRef = useRef<{
122
115
  startX: number;
@@ -303,15 +296,11 @@ export function ChartZoom({
303
296
  }, []);
304
297
 
305
298
  const handleReset = useCallback(() => {
306
- const init = initialDomainsRef.current;
307
- if (!init) {
308
- return;
309
- }
310
- if (init.x) {
311
- onXDomainChange?.(init.x);
299
+ if (initialDomains.x) {
300
+ onXDomainChange?.(initialDomains.x);
312
301
  }
313
- onYDomainChange?.(init.y);
314
- }, [onXDomainChange, onYDomainChange]);
302
+ onYDomainChange?.(initialDomains.y);
303
+ }, [initialDomains, onXDomainChange, onYDomainChange]);
315
304
 
316
305
  // Toolbar portal target
317
306
  const portalTarget = svgRef.current?.parentElement;
@@ -28,8 +28,10 @@ import {
28
28
  import {
29
29
  busyWindowProof,
30
30
  CONTROL_RGB,
31
+ countBetween,
31
32
  inPresentationOrder,
32
33
  startBusyControl,
34
+ traceRendering,
33
35
  trackMarker,
34
36
  type TrackedFrame,
35
37
  } from '@astryxdesign/interaction-harness';
@@ -413,50 +415,47 @@ test('a value change keeps moving while a script holds the main thread', async (
413
415
  ).toBeLessThanOrEqual(6);
414
416
  });
415
417
 
416
- test('a value change lays out and paints nothing per frame', async ({
417
- page,
418
- browser,
419
- }) => {
418
+ test('a value change lays out and paints nothing per frame', async ({page}) => {
420
419
  await open(page, 20);
421
- // Stretch the theme's duration so the trace sits wholly inside the change.
420
+ // Stretch the theme's duration so the second judged below sits wholly
421
+ // inside the change.
422
422
  await page.evaluate(() =>
423
423
  (
424
424
  document.querySelector('.astryx-circular-progress-fill') as HTMLElement
425
425
  ).style.setProperty('--duration-medium', '3000ms'),
426
426
  );
427
- await setArgs(page, {value: 80});
428
- await page.waitForTimeout(200);
429
- await browser.startTracing(page, {
430
- categories: ['devtools.timeline', 'disabled-by-default-devtools.timeline'],
427
+ // Tracing starts before the change, so however long it takes to start, it
428
+ // takes none of the change's time. The judged second starts two frames
429
+ // after the change's animations are ready, 200 ms in.
430
+ let from = 0;
431
+ let to = 0;
432
+ let moving = false;
433
+ const trace = await traceRendering(page, async () => {
434
+ await setArgs(page, {value: 80});
435
+ await page.waitForTimeout(200);
436
+ from = await page.evaluate(async () => {
437
+ await Promise.all(document.getAnimations().map(async a => a.ready));
438
+ await new Promise(resolve =>
439
+ requestAnimationFrame(() => requestAnimationFrame(resolve)),
440
+ );
441
+ return performance.timeOrigin + performance.now();
442
+ });
443
+ await page.waitForTimeout(1000);
444
+ [to, moving] = await page.evaluate(
445
+ () =>
446
+ [
447
+ performance.timeOrigin + performance.now(),
448
+ document.getAnimations().some(a => a.playState === 'running'),
449
+ ] as const,
450
+ );
431
451
  });
432
- await page.waitForTimeout(1000);
433
- const moving = await page.evaluate(() =>
434
- document.getAnimations().some(a => a.playState === 'running'),
435
- );
436
- const trace = JSON.parse((await browser.stopTracing()).toString()) as {
437
- traceEvents: {
438
- name: string;
439
- ph: string;
440
- pid: number;
441
- tid: number;
442
- args?: {name?: string};
443
- }[];
444
- };
445
- const mainThreads = new Set(
446
- trace.traceEvents
447
- .filter(e => e.ph === 'M' && e.name === 'thread_name')
448
- .filter(e => e.args?.name === 'CrRendererMain')
449
- .map(e => `${e.pid}:${e.tid}`),
450
- );
451
- const count = (name: string) =>
452
- trace.traceEvents.filter(
453
- e => e.name === name && mainThreads.has(`${e.pid}:${e.tid}`),
454
- ).length;
452
+ const counts = countBetween(trace, from, to);
455
453
  // A full second of the change: nothing lays out or paints. A
456
454
  // stroke-dashoffset transition repainted every frame (36 paints in 410 ms).
455
+ expect(to - from).toBeGreaterThanOrEqual(1000);
457
456
  expect(moving).toBe(true);
458
- expect(count('Layout')).toBe(0);
459
- expect(count('Paint')).toBe(0);
457
+ expect(counts.Layout ?? 0).toBe(0);
458
+ expect(counts.Paint ?? 0).toBe(0);
460
459
  });
461
460
 
462
461
  test('at rest the ring draws what the stroked circle drew', async ({