@wallarm-org/design-system 0.44.0 → 0.44.1

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.
@@ -25,15 +25,15 @@ function LineChart({ data, series, xKey, activeKey: controlledActiveKey, onActiv
25
25
  onActiveKeyChange,
26
26
  seriesByKey
27
27
  });
28
+ const onZoomChangeRef = useRef(onZoomChange);
29
+ onZoomChangeRef.current = onZoomChange;
28
30
  const emitZoom = useCallback((range)=>{
29
- onZoomChange?.(range);
30
- }, [
31
- onZoomChange
32
- ]);
31
+ onZoomChangeRef.current?.(range);
32
+ }, []);
33
33
  const zoom = useLineChartZoomState({
34
34
  data,
35
35
  xKey,
36
- onZoomChange
36
+ onZoomChangeRef
37
37
  });
38
38
  const hiddenSet = useMemo(()=>{
39
39
  if (!filteredKeys?.length) return EMPTY_HIDDEN_SET;
@@ -1,5 +1,5 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
- import { useContext, useEffect, useMemo, useRef } from "react";
2
+ import { useContext, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { createPortal } from "react-dom";
4
4
  import { ReferenceArea, usePlotArea } from "recharts";
5
5
  import { formatChartDateTime } from "../lib/timeFormatters.js";
@@ -12,22 +12,81 @@ import { LineChartZoomPopoverRange } from "./LineChartZoomPopoverRange.js";
12
12
  import { formatRange as formatRange_js_formatRange } from "./lib/formatRange.js";
13
13
  const defaultFormatRange = formatRange_js_formatRange((value)=>formatChartDateTime(value) || String(value));
14
14
  const POPOVER_OFFSET_X = 12;
15
+ const RECHARTS_SURFACE = '.recharts-surface';
15
16
  const LineChartZoomBrush = ({ disabled = false, formatRange = defaultFormatRange, confirmLabel = 'Zoom in', container })=>{
16
17
  const dataCtx = useContext(LineChartDataContext);
17
18
  const zoomCtx = useContext(LineChartZoomContext);
18
19
  const popoverRef = useRef(null);
19
20
  const plotArea = usePlotArea();
20
21
  const rootRef = zoomCtx?.rootRef;
21
- const centerY = useMemo(()=>{
22
- if (!plotArea || !rootRef?.current) return null;
23
- const surface = rootRef.current.querySelector('.recharts-surface');
24
- if (!surface) return null;
22
+ const drag = zoomCtx?.drag ?? null;
23
+ const pending = zoomCtx?.pending ?? null;
24
+ const cancelPending = zoomCtx?.cancelPending;
25
+ const confirmZoom = zoomCtx?.confirmZoom;
26
+ const isPopoverOpen = null !== drag || null !== pending;
27
+ const isPending = null !== pending;
28
+ const [scrollTick, setScrollTick] = useState(0);
29
+ useEffect(()=>{
30
+ if (!isPopoverOpen) return;
31
+ let lastTop = null;
32
+ let lastLeft = null;
33
+ const onScroll = ()=>{
34
+ const surface = rootRef?.current?.querySelector(RECHARTS_SURFACE);
35
+ if (!surface) return;
36
+ const { top, left } = surface.getBoundingClientRect();
37
+ if (top === lastTop && left === lastLeft) return;
38
+ lastTop = top;
39
+ lastLeft = left;
40
+ setScrollTick((n)=>n + 1);
41
+ };
42
+ window.addEventListener('scroll', onScroll, {
43
+ passive: true,
44
+ capture: true
45
+ });
46
+ window.addEventListener('resize', onScroll);
47
+ return ()=>{
48
+ window.removeEventListener('scroll', onScroll, {
49
+ capture: true
50
+ });
51
+ window.removeEventListener('resize', onScroll);
52
+ };
53
+ }, [
54
+ isPopoverOpen,
55
+ rootRef
56
+ ]);
57
+ const cachedGeometryRef = useRef(null);
58
+ const popoverGeometry = useMemo(()=>{
59
+ if (!isPopoverOpen) {
60
+ cachedGeometryRef.current = null;
61
+ return null;
62
+ }
63
+ if (!plotArea || !rootRef?.current) return cachedGeometryRef.current;
64
+ const surface = rootRef.current.querySelector(RECHARTS_SURFACE);
65
+ if (!surface) return cachedGeometryRef.current;
25
66
  const rect = surface.getBoundingClientRect();
26
- return rect.top + plotArea.y + plotArea.height / 2;
67
+ const next = {
68
+ centerY: rect.top + plotArea.y + plotArea.height / 2,
69
+ left: rect.left
70
+ };
71
+ cachedGeometryRef.current = next;
72
+ return next;
27
73
  }, [
74
+ isPopoverOpen,
28
75
  plotArea,
76
+ rootRef,
77
+ scrollTick
78
+ ]);
79
+ const pendingAnchorLeft = useMemo(()=>{
80
+ if (!isPending || !rootRef?.current) return null;
81
+ const surface = rootRef.current.querySelector(RECHARTS_SURFACE);
82
+ if (!surface) return null;
83
+ return surface.getBoundingClientRect().left;
84
+ }, [
85
+ isPending,
29
86
  rootRef
30
87
  ]);
88
+ const centerY = popoverGeometry?.centerY ?? null;
89
+ const pendingScrollDeltaX = isPending && null !== pendingAnchorLeft && popoverGeometry ? popoverGeometry.left - pendingAnchorLeft : 0;
31
90
  const registerEnabled = zoomCtx?.registerEnabled;
32
91
  useEffect(()=>{
33
92
  if (disabled || !registerEnabled) return;
@@ -36,10 +95,6 @@ const LineChartZoomBrush = ({ disabled = false, formatRange = defaultFormatRange
36
95
  disabled,
37
96
  registerEnabled
38
97
  ]);
39
- const drag = zoomCtx?.drag ?? null;
40
- const pending = zoomCtx?.pending ?? null;
41
- const cancelPending = zoomCtx?.cancelPending;
42
- const confirmZoom = zoomCtx?.confirmZoom;
43
98
  useZoomPendingListeners({
44
99
  enabled: null !== pending,
45
100
  rootRef,
@@ -72,7 +127,6 @@ const LineChartZoomBrush = ({ disabled = false, formatRange = defaultFormatRange
72
127
  ]);
73
128
  if (disabled || !dataCtx || !zoomCtx) return null;
74
129
  const popoverPosition = drag ?? pending ?? null;
75
- const isPending = null !== pending;
76
130
  const popoverContent = range && popoverPosition ? /*#__PURE__*/ jsx("div", {
77
131
  ref: popoverRef,
78
132
  "data-slot": "line-chart-zoom-cursor-popover",
@@ -80,7 +134,7 @@ const LineChartZoomBrush = ({ disabled = false, formatRange = defaultFormatRange
80
134
  className: lineChartZoomCursorPopoverClasses,
81
135
  style: {
82
136
  top: centerY ?? popoverPosition.clientY,
83
- left: popoverPosition.clientX + POPOVER_OFFSET_X,
137
+ left: popoverPosition.clientX + pendingScrollDeltaX + POPOVER_OFFSET_X,
84
138
  transform: 'translateY(-50%)',
85
139
  pointerEvents: isPending ? 'auto' : 'none'
86
140
  },
@@ -16,14 +16,15 @@ const useLineChartActiveKey = ({ controlledActiveKey, onActiveKeyChange, seriesB
16
16
  ]);
17
17
  const lastActiveKeyRef = useRef(void 0);
18
18
  lastActiveKeyRef.current = activeKey;
19
+ const onActiveKeyChangeRef = useRef(onActiveKeyChange);
20
+ onActiveKeyChangeRef.current = onActiveKeyChange;
19
21
  const setActiveKey = useCallback((key)=>{
20
22
  if (lastActiveKeyRef.current === key) return;
21
23
  lastActiveKeyRef.current = key;
22
24
  setInternalActiveKey(key);
23
- onActiveKeyChange?.(key);
25
+ onActiveKeyChangeRef.current?.(key);
24
26
  }, [
25
- setInternalActiveKey,
26
- onActiveKeyChange
27
+ setInternalActiveKey
27
28
  ]);
28
29
  return {
29
30
  activeKey,
@@ -1,3 +1,4 @@
1
+ import { type RefObject } from 'react';
1
2
  import type { LineChartDatum, LineChartZoomDragState, LineChartZoomPendingState, LineChartZoomRange } from '../LineChartContext';
2
3
  interface UseLineChartZoomStateResult {
3
4
  enabled: boolean;
@@ -25,13 +26,19 @@ interface UseLineChartZoomStateResult {
25
26
  * `useZoomDragListeners` so the popover keeps tracking when the cursor leaves
26
27
  * the SVG and a mouseup outside the chart still releases into pending.
27
28
  *
29
+ * Drag updates from recharts (`updateDrag` — carries index+coords) and from
30
+ * the window listener (`handleDragMove` — coords only) both write into a
31
+ * single pending-frame buffer and flush together on the next animation frame.
32
+ * That collapses the two motion paths into one React commit per frame and
33
+ * keeps state writes off the synchronous mousemove path.
34
+ *
28
35
  * Dataset changes invalidate cached indices on both `drag` and `pending`, so
29
36
  * both reset whenever `data` or `xKey` flips — otherwise a stale range could
30
37
  * be committed against a refreshed dataset.
31
38
  */
32
- export declare const useLineChartZoomState: ({ data, xKey, onZoomChange, }: {
39
+ export declare const useLineChartZoomState: ({ data, xKey, onZoomChangeRef, }: {
33
40
  data: LineChartDatum[];
34
41
  xKey: string;
35
- onZoomChange: ((range: LineChartZoomRange | null) => void) | undefined;
42
+ onZoomChangeRef: RefObject<((range: LineChartZoomRange | null) => void) | undefined>;
36
43
  }) => UseLineChartZoomStateResult;
37
44
  export {};
@@ -1,6 +1,6 @@
1
- import { useCallback, useEffect, useState } from "react";
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { useZoomDragListeners } from "./useZoomDragListeners.js";
3
- const useLineChartZoomState = ({ data, xKey, onZoomChange })=>{
3
+ const useLineChartZoomState = ({ data, xKey, onZoomChangeRef })=>{
4
4
  const [zoomEnabledCount, setZoomEnabledCount] = useState(0);
5
5
  const [zoomDrag, setZoomDrag] = useState(null);
6
6
  const [zoomPending, setZoomPending] = useState(null);
@@ -8,35 +8,79 @@ const useLineChartZoomState = ({ data, xKey, onZoomChange })=>{
8
8
  setZoomEnabledCount((n)=>n + 1);
9
9
  return ()=>setZoomEnabledCount((n)=>n - 1);
10
10
  }, []);
11
- const startDrag = useCallback((index, clientX, clientY)=>{
12
- setZoomPending(null);
13
- setZoomDrag({
14
- startIndex: index,
15
- endIndex: index,
16
- clientX,
17
- clientY
18
- });
19
- }, []);
20
- const updateDrag = useCallback((index, clientX, clientY)=>{
11
+ const pendingDragRef = useRef(null);
12
+ const rafIdRef = useRef(null);
13
+ const flushPendingDrag = useCallback(()=>{
14
+ rafIdRef.current = null;
15
+ const buffered = pendingDragRef.current;
16
+ if (!buffered) return;
17
+ pendingDragRef.current = null;
21
18
  setZoomDrag((prev)=>{
22
19
  if (!prev) return null;
23
- if (prev.endIndex === index && prev.clientX === clientX && prev.clientY === clientY) return prev;
20
+ const endIndex = buffered.index ?? prev.endIndex;
21
+ const clientX = buffered.clientX ?? prev.clientX;
22
+ const clientY = buffered.clientY ?? prev.clientY;
23
+ if (prev.endIndex === endIndex && prev.clientX === clientX && prev.clientY === clientY) return prev;
24
24
  return {
25
25
  startIndex: prev.startIndex,
26
- endIndex: index,
26
+ endIndex,
27
27
  clientX,
28
28
  clientY
29
29
  };
30
30
  });
31
31
  }, []);
32
+ const scheduleFlush = useCallback(()=>{
33
+ if (null !== rafIdRef.current) return;
34
+ rafIdRef.current = requestAnimationFrame(flushPendingDrag);
35
+ }, [
36
+ flushPendingDrag
37
+ ]);
38
+ const cancelScheduledFlush = useCallback(()=>{
39
+ if (null !== rafIdRef.current) {
40
+ cancelAnimationFrame(rafIdRef.current);
41
+ rafIdRef.current = null;
42
+ }
43
+ pendingDragRef.current = null;
44
+ }, []);
45
+ const startDrag = useCallback((index, clientX, clientY)=>{
46
+ cancelScheduledFlush();
47
+ setZoomPending(null);
48
+ setZoomDrag({
49
+ startIndex: index,
50
+ endIndex: index,
51
+ clientX,
52
+ clientY
53
+ });
54
+ }, [
55
+ cancelScheduledFlush
56
+ ]);
57
+ const updateDrag = useCallback((index, clientX, clientY)=>{
58
+ pendingDragRef.current = {
59
+ ...pendingDragRef.current,
60
+ index,
61
+ clientX,
62
+ clientY
63
+ };
64
+ scheduleFlush();
65
+ }, [
66
+ scheduleFlush
67
+ ]);
32
68
  const cancelDrag = useCallback(()=>{
69
+ cancelScheduledFlush();
33
70
  setZoomDrag(null);
34
- }, []);
71
+ }, [
72
+ cancelScheduledFlush
73
+ ]);
35
74
  const endDrag = useCallback(()=>{
75
+ const buffered = pendingDragRef.current;
76
+ cancelScheduledFlush();
36
77
  setZoomDrag((currentDrag)=>{
37
78
  if (!currentDrag) return null;
38
- const lo = Math.min(currentDrag.startIndex, currentDrag.endIndex);
39
- const hi = Math.max(currentDrag.startIndex, currentDrag.endIndex);
79
+ const endIndex = buffered?.index ?? currentDrag.endIndex;
80
+ const clientX = buffered?.clientX ?? currentDrag.clientX;
81
+ const clientY = buffered?.clientY ?? currentDrag.clientY;
82
+ const lo = Math.min(currentDrag.startIndex, endIndex);
83
+ const hi = Math.max(currentDrag.startIndex, endIndex);
40
84
  if (lo === hi) return null;
41
85
  const fromDatum = data[lo];
42
86
  const toDatum = data[hi];
@@ -49,46 +93,56 @@ const useLineChartZoomState = ({ data, xKey, onZoomChange })=>{
49
93
  from,
50
94
  to
51
95
  },
52
- clientX: currentDrag.clientX,
53
- clientY: currentDrag.clientY
96
+ clientX,
97
+ clientY
54
98
  });
55
99
  return null;
56
100
  });
57
101
  }, [
58
102
  data,
59
- xKey
103
+ xKey,
104
+ cancelScheduledFlush
60
105
  ]);
61
106
  const confirmZoom = useCallback(()=>{
62
107
  setZoomPending((currentPending)=>{
63
- if (currentPending) onZoomChange?.(currentPending.range);
108
+ if (currentPending) onZoomChangeRef.current?.(currentPending.range);
64
109
  return null;
65
110
  });
66
111
  }, [
67
- onZoomChange
112
+ onZoomChangeRef
68
113
  ]);
69
114
  const cancelPending = useCallback(()=>{
70
115
  setZoomPending(null);
71
116
  }, []);
72
117
  useEffect(()=>{
118
+ cancelScheduledFlush();
73
119
  setZoomDrag(null);
74
120
  setZoomPending(null);
75
121
  }, [
76
122
  data,
77
- xKey
123
+ xKey,
124
+ cancelScheduledFlush
125
+ ]);
126
+ useEffect(()=>()=>cancelScheduledFlush(), [
127
+ cancelScheduledFlush
78
128
  ]);
79
129
  const isZoomDragging = null !== zoomDrag;
80
130
  const handleDragMove = useCallback((clientX, clientY)=>{
81
- setZoomDrag((prev)=>{
82
- if (!prev) return null;
83
- if (prev.clientX === clientX && prev.clientY === clientY) return prev;
84
- return {
85
- ...prev,
86
- clientX,
87
- clientY
88
- };
89
- });
90
- }, []);
91
- const handleDragEscape = useCallback(()=>setZoomDrag(null), []);
131
+ pendingDragRef.current = {
132
+ ...pendingDragRef.current,
133
+ clientX,
134
+ clientY
135
+ };
136
+ scheduleFlush();
137
+ }, [
138
+ scheduleFlush
139
+ ]);
140
+ const handleDragEscape = useCallback(()=>{
141
+ cancelScheduledFlush();
142
+ setZoomDrag(null);
143
+ }, [
144
+ cancelScheduledFlush
145
+ ]);
92
146
  useZoomDragListeners({
93
147
  enabled: isZoomDragging,
94
148
  onMove: handleDragMove,
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.43.0",
3
- "generatedAt": "2026-05-21T14:01:47.194Z",
2
+ "version": "0.44.0",
3
+ "generatedAt": "2026-05-21T14:47:58.859Z",
4
4
  "components": [
5
5
  {
6
6
  "name": "Accordion",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wallarm-org/design-system",
3
- "version": "0.44.0",
3
+ "version": "0.44.1",
4
4
  "description": "Core design system library with React components and Storybook documentation",
5
5
  "publishConfig": {
6
6
  "access": "public",