@choice-ui/react 1.9.4 → 1.9.6
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/components/command/dist/index.d.ts +3 -0
- package/dist/components/command/src/components/command-list.d.ts +3 -0
- package/dist/components/command/src/components/command-list.js +4 -1
- package/dist/components/scroll-area/dist/index.d.ts +4 -27
- package/dist/components/scroll-area/dist/index.js +96 -123
- package/dist/components/scroll-area/src/components/scroll-area-content.js +2 -2
- package/dist/components/scroll-area/src/components/scroll-area-root.js +9 -12
- package/dist/components/scroll-area/src/components/scroll-area-scrollbar.js +14 -4
- package/dist/components/scroll-area/src/components/scroll-area-viewport.js +2 -2
- package/dist/components/scroll-area/src/context/scroll-area-context.d.ts +17 -2
- package/dist/components/scroll-area/src/context/scroll-area-context.js +23 -6
- package/dist/components/scroll-area/src/hooks/index.d.ts +0 -1
- package/dist/components/scroll-area/src/hooks/use-scroll-state-and-visibility.d.ts +2 -2
- package/dist/components/scroll-area/src/hooks/use-scroll-state-and-visibility.js +30 -75
- package/dist/components/scroll-area/src/hooks/use-thumb.d.ts +1 -1
- package/dist/components/scroll-area/src/hooks/use-thumb.js +25 -28
- package/dist/components/scroll-area/src/types.d.ts +16 -4
- package/dist/index.js +0 -2
- package/package.json +1 -1
- package/dist/components/scroll-area/src/hooks/use-scroll-performance-monitor.d.ts +0 -23
- package/dist/components/scroll-area/src/hooks/use-scroll-performance-monitor.js +0 -123
|
@@ -58,6 +58,9 @@ interface CommandItemProps extends Omit<HTMLProps<HTMLDivElement>, "onSelect"> {
|
|
|
58
58
|
interface CommandListProps extends ScrollAreaProps {
|
|
59
59
|
children: React.ReactNode;
|
|
60
60
|
className?: string;
|
|
61
|
+
classNames?: {
|
|
62
|
+
content?: string;
|
|
63
|
+
};
|
|
61
64
|
label?: string;
|
|
62
65
|
}
|
|
63
66
|
|
|
@@ -2,6 +2,9 @@ import { ScrollAreaProps } from '../../../scroll-area/src';
|
|
|
2
2
|
export interface CommandListProps extends ScrollAreaProps {
|
|
3
3
|
children: React.ReactNode;
|
|
4
4
|
className?: string;
|
|
5
|
+
classNames?: {
|
|
6
|
+
content?: string;
|
|
7
|
+
};
|
|
5
8
|
label?: string;
|
|
6
9
|
}
|
|
7
10
|
export declare const CommandList: import('react').ForwardRefExoticComponent<CommandListProps & import('react').RefAttributes<HTMLDivElement>>;
|
|
@@ -12,6 +12,9 @@ const CommandList = forwardRef((props, forwardedRef) => {
|
|
|
12
12
|
label = "Suggestions",
|
|
13
13
|
hoverBoundary = "none",
|
|
14
14
|
scrollbarMode = "padding-b",
|
|
15
|
+
classNames = {
|
|
16
|
+
content: ""
|
|
17
|
+
},
|
|
15
18
|
orientation,
|
|
16
19
|
variant,
|
|
17
20
|
type,
|
|
@@ -67,7 +70,7 @@ const CommandList = forwardRef((props, forwardedRef) => {
|
|
|
67
70
|
children: /* @__PURE__ */ jsx(
|
|
68
71
|
ScrollArea.Content,
|
|
69
72
|
{
|
|
70
|
-
className: tcx(tv.content()),
|
|
73
|
+
className: tcx(tv.content(), classNames == null ? void 0 : classNames.content),
|
|
71
74
|
ref: (el) => {
|
|
72
75
|
height.current = el;
|
|
73
76
|
if (context.listInnerRef) {
|
|
@@ -61,9 +61,9 @@ declare const ScrollArea: react.ForwardRefExoticComponent<ScrollAreaProps & reac
|
|
|
61
61
|
};
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
* Merged scroll state and visibility management hook
|
|
64
|
+
* Merged scroll state and visibility management hook
|
|
65
65
|
*/
|
|
66
|
-
declare function useScrollStateAndVisibility(viewport: HTMLDivElement | null): {
|
|
66
|
+
declare function useScrollStateAndVisibility(viewport: HTMLDivElement | null, content: HTMLDivElement | null): {
|
|
67
67
|
scrollState: ScrollState;
|
|
68
68
|
isHovering: boolean;
|
|
69
69
|
isScrolling: boolean;
|
|
@@ -86,7 +86,7 @@ declare function useThumbStyle(scrollState: ScrollState, orientation: "vertical"
|
|
|
86
86
|
top?: undefined;
|
|
87
87
|
};
|
|
88
88
|
/**
|
|
89
|
-
*
|
|
89
|
+
* High-performance thumb drag hook
|
|
90
90
|
*/
|
|
91
91
|
declare function useThumbDrag(viewport: HTMLDivElement | null, scrollState: ScrollState, orientation: "vertical" | "horizontal"): {
|
|
92
92
|
isDragging: boolean;
|
|
@@ -102,27 +102,4 @@ declare function useHasOverflow(scrollState: ScrollState, orientation: "vertical
|
|
|
102
102
|
*/
|
|
103
103
|
declare function useScrollbarShouldShow(type: ScrollbarVisibilityType, hasOverflow: boolean, isScrolling: boolean, isHovering: boolean): boolean;
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
averageFrameTime: number;
|
|
107
|
-
droppedFrames: number;
|
|
108
|
-
maxFrameTime: number;
|
|
109
|
-
scrollEventFrequency: number;
|
|
110
|
-
updateFrequency: number;
|
|
111
|
-
}
|
|
112
|
-
interface PerformanceMonitorOptions {
|
|
113
|
-
enabled?: boolean;
|
|
114
|
-
frameTimeThreshold?: number;
|
|
115
|
-
logInterval?: number;
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* 🔍 ScrollArea performance monitoring Hook
|
|
119
|
-
*
|
|
120
|
-
* Used to monitor and diagnose scroll performance issues, including:
|
|
121
|
-
* - Frame rate monitoring
|
|
122
|
-
* - Event frequency statistics
|
|
123
|
-
* - Performance bottleneck detection
|
|
124
|
-
* - Real-time performance reporting
|
|
125
|
-
*/
|
|
126
|
-
declare function useScrollPerformanceMonitor(viewport: HTMLDivElement | null, options?: PerformanceMonitorOptions): PerformanceMetrics | null;
|
|
127
|
-
|
|
128
|
-
export { ScrollArea, type ScrollAreaProps, type ScrollbarProps, type ThumbProps, useHasOverflow, useScrollPerformanceMonitor, useScrollStateAndVisibility, useScrollbarShouldShow, useThumbDrag, useThumbStyle };
|
|
105
|
+
export { ScrollArea, type ScrollAreaProps, type ScrollbarProps, type ThumbProps, useHasOverflow, useScrollStateAndVisibility, useScrollbarShouldShow, useThumbDrag, useThumbStyle };
|
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
import { forwardRef, useState, useId, useMemo, createContext, useCallback, useRef, useEffect, useContext } from "react";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { tcv, tcx } from "../../../shared/utils/tcx/tcx.js";
|
|
4
|
-
var
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
var ScrollAreaStateContext = createContext(null);
|
|
5
|
+
var ScrollAreaConfigContext = createContext(null);
|
|
6
|
+
var ERROR_MESSAGE = "ScrollArea compound components must be used within ScrollArea";
|
|
7
|
+
function useScrollAreaStateContext() {
|
|
8
|
+
const context = useContext(ScrollAreaStateContext);
|
|
9
|
+
if (!context) {
|
|
10
|
+
throw new Error(ERROR_MESSAGE);
|
|
11
|
+
}
|
|
12
|
+
return context;
|
|
13
|
+
}
|
|
14
|
+
function useScrollAreaConfigContext() {
|
|
15
|
+
const context = useContext(ScrollAreaConfigContext);
|
|
7
16
|
if (!context) {
|
|
8
|
-
throw new Error(
|
|
17
|
+
throw new Error(ERROR_MESSAGE);
|
|
9
18
|
}
|
|
10
19
|
return context;
|
|
11
20
|
}
|
|
12
|
-
function
|
|
21
|
+
function useScrollAreaContext() {
|
|
22
|
+
const state = useScrollAreaStateContext();
|
|
23
|
+
const config = useScrollAreaConfigContext();
|
|
24
|
+
return { ...state, ...config };
|
|
25
|
+
}
|
|
26
|
+
function useScrollStateAndVisibility(viewport, content) {
|
|
13
27
|
const [scrollState, setScrollState] = useState({
|
|
14
28
|
scrollLeft: 0,
|
|
15
29
|
scrollTop: 0,
|
|
@@ -23,8 +37,6 @@ function useScrollStateAndVisibility(viewport) {
|
|
|
23
37
|
const scrollTimeoutRef = useRef();
|
|
24
38
|
const rafRef = useRef();
|
|
25
39
|
const resizeObserverRef = useRef();
|
|
26
|
-
const mutationObserverRef = useRef();
|
|
27
|
-
const mutationTimeoutRef = useRef();
|
|
28
40
|
const lastUpdateTimeRef = useRef(0);
|
|
29
41
|
const minUpdateIntervalRef = useRef(16);
|
|
30
42
|
const updateScrollState = useCallback(() => {
|
|
@@ -36,36 +48,36 @@ function useScrollStateAndVisibility(viewport) {
|
|
|
36
48
|
cancelAnimationFrame(rafRef.current);
|
|
37
49
|
}
|
|
38
50
|
rafRef.current = requestAnimationFrame(() => {
|
|
51
|
+
rafRef.current = void 0;
|
|
39
52
|
updateScrollState();
|
|
40
53
|
});
|
|
41
54
|
return;
|
|
42
55
|
}
|
|
43
56
|
if (rafRef.current) {
|
|
44
57
|
cancelAnimationFrame(rafRef.current);
|
|
58
|
+
rafRef.current = void 0;
|
|
45
59
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return prevState;
|
|
68
|
-
});
|
|
60
|
+
const newState = {
|
|
61
|
+
scrollLeft: viewport.scrollLeft,
|
|
62
|
+
scrollTop: viewport.scrollTop,
|
|
63
|
+
scrollWidth: viewport.scrollWidth,
|
|
64
|
+
scrollHeight: viewport.scrollHeight,
|
|
65
|
+
clientWidth: viewport.clientWidth,
|
|
66
|
+
clientHeight: viewport.clientHeight
|
|
67
|
+
};
|
|
68
|
+
setScrollState((prevState) => {
|
|
69
|
+
const scrollLeftChanged = Math.abs(prevState.scrollLeft - newState.scrollLeft) > 0.5;
|
|
70
|
+
const scrollTopChanged = Math.abs(prevState.scrollTop - newState.scrollTop) > 0.5;
|
|
71
|
+
const scrollWidthChanged = prevState.scrollWidth !== newState.scrollWidth;
|
|
72
|
+
const scrollHeightChanged = prevState.scrollHeight !== newState.scrollHeight;
|
|
73
|
+
const clientWidthChanged = prevState.clientWidth !== newState.clientWidth;
|
|
74
|
+
const clientHeightChanged = prevState.clientHeight !== newState.clientHeight;
|
|
75
|
+
const hasChanges = scrollLeftChanged || scrollTopChanged || scrollWidthChanged || scrollHeightChanged || clientWidthChanged || clientHeightChanged;
|
|
76
|
+
if (hasChanges) {
|
|
77
|
+
lastUpdateTimeRef.current = now;
|
|
78
|
+
return newState;
|
|
79
|
+
}
|
|
80
|
+
return prevState;
|
|
69
81
|
});
|
|
70
82
|
}, [viewport]);
|
|
71
83
|
const delayedUpdateScrollState = useCallback(() => {
|
|
@@ -102,54 +114,19 @@ function useScrollStateAndVisibility(viewport) {
|
|
|
102
114
|
passive: true,
|
|
103
115
|
signal,
|
|
104
116
|
capture: false
|
|
105
|
-
// Avoid unnecessary event capture
|
|
106
117
|
});
|
|
107
118
|
window.addEventListener("resize", handleResize, {
|
|
108
119
|
passive: true,
|
|
109
120
|
signal
|
|
110
121
|
});
|
|
111
122
|
if (window.ResizeObserver) {
|
|
112
|
-
resizeObserverRef.current = new ResizeObserver((
|
|
113
|
-
|
|
114
|
-
if (entry.target === viewport) {
|
|
115
|
-
updateScrollState();
|
|
116
|
-
break;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
123
|
+
resizeObserverRef.current = new ResizeObserver(() => {
|
|
124
|
+
updateScrollState();
|
|
119
125
|
});
|
|
120
126
|
resizeObserverRef.current.observe(viewport);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const hasLayoutChanges = mutations.some((mutation) => {
|
|
125
|
-
if (mutation.type === "childList") {
|
|
126
|
-
return mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0;
|
|
127
|
-
}
|
|
128
|
-
if (mutation.type === "attributes") {
|
|
129
|
-
const attr = mutation.attributeName;
|
|
130
|
-
return attr === "style" || attr === "class";
|
|
131
|
-
}
|
|
132
|
-
return mutation.type === "characterData";
|
|
133
|
-
});
|
|
134
|
-
if (!hasLayoutChanges) return;
|
|
135
|
-
if (mutationTimeoutRef.current) {
|
|
136
|
-
clearTimeout(mutationTimeoutRef.current);
|
|
137
|
-
}
|
|
138
|
-
mutationTimeoutRef.current = window.setTimeout(() => {
|
|
139
|
-
updateScrollState();
|
|
140
|
-
}, 16);
|
|
141
|
-
});
|
|
142
|
-
mutationObserverRef.current.observe(viewport, {
|
|
143
|
-
childList: true,
|
|
144
|
-
subtree: true,
|
|
145
|
-
attributes: true,
|
|
146
|
-
attributeFilter: ["style", "class"],
|
|
147
|
-
// Only listen to attributes that affect layout
|
|
148
|
-
characterData: true,
|
|
149
|
-
characterDataOldValue: false,
|
|
150
|
-
// No need for old value, improve performance
|
|
151
|
-
attributeOldValue: false
|
|
152
|
-
});
|
|
127
|
+
if (content) {
|
|
128
|
+
resizeObserverRef.current.observe(content);
|
|
129
|
+
}
|
|
153
130
|
}
|
|
154
131
|
delayedUpdateScrollState();
|
|
155
132
|
return () => {
|
|
@@ -158,10 +135,6 @@ function useScrollStateAndVisibility(viewport) {
|
|
|
158
135
|
clearTimeout(scrollTimeoutRef.current);
|
|
159
136
|
scrollTimeoutRef.current = void 0;
|
|
160
137
|
}
|
|
161
|
-
if (mutationTimeoutRef.current) {
|
|
162
|
-
clearTimeout(mutationTimeoutRef.current);
|
|
163
|
-
mutationTimeoutRef.current = void 0;
|
|
164
|
-
}
|
|
165
138
|
if (rafRef.current) {
|
|
166
139
|
cancelAnimationFrame(rafRef.current);
|
|
167
140
|
rafRef.current = void 0;
|
|
@@ -170,12 +143,8 @@ function useScrollStateAndVisibility(viewport) {
|
|
|
170
143
|
resizeObserverRef.current.disconnect();
|
|
171
144
|
resizeObserverRef.current = void 0;
|
|
172
145
|
}
|
|
173
|
-
if (mutationObserverRef.current) {
|
|
174
|
-
mutationObserverRef.current.disconnect();
|
|
175
|
-
mutationObserverRef.current = void 0;
|
|
176
|
-
}
|
|
177
146
|
};
|
|
178
|
-
}, [viewport, handleScroll, delayedUpdateScrollState]);
|
|
147
|
+
}, [viewport, content, handleScroll, delayedUpdateScrollState, updateScrollState]);
|
|
179
148
|
const handleMouseEnter = useCallback(() => setIsHovering(true), []);
|
|
180
149
|
const handleMouseLeave = useCallback(() => setIsHovering(false), []);
|
|
181
150
|
return {
|
|
@@ -229,16 +198,13 @@ function useThumbDrag(viewport, scrollState, orientation) {
|
|
|
229
198
|
const isDragging = useRef(false);
|
|
230
199
|
const startPos = useRef(0);
|
|
231
200
|
const startScroll = useRef(0);
|
|
232
|
-
const rafId = useRef();
|
|
233
201
|
const cleanupRef = useRef(null);
|
|
202
|
+
const scrollStateRef = useRef(scrollState);
|
|
203
|
+
scrollStateRef.current = scrollState;
|
|
234
204
|
const dragContextRef = useRef(null);
|
|
235
205
|
useEffect(() => {
|
|
236
206
|
return () => {
|
|
237
207
|
isDragging.current = false;
|
|
238
|
-
if (rafId.current) {
|
|
239
|
-
cancelAnimationFrame(rafId.current);
|
|
240
|
-
rafId.current = void 0;
|
|
241
|
-
}
|
|
242
208
|
if (cleanupRef.current) {
|
|
243
209
|
cleanupRef.current();
|
|
244
210
|
cleanupRef.current = null;
|
|
@@ -248,13 +214,21 @@ function useThumbDrag(viewport, scrollState, orientation) {
|
|
|
248
214
|
const handleMouseDown = useCallback(
|
|
249
215
|
(e) => {
|
|
250
216
|
if (!viewport) return;
|
|
217
|
+
const currentScrollState = scrollStateRef.current;
|
|
251
218
|
const target = e.currentTarget;
|
|
252
219
|
const scrollbar = target.closest('[role="scrollbar"]');
|
|
253
220
|
if (!scrollbar) return;
|
|
254
221
|
const scrollbarRect = scrollbar.getBoundingClientRect();
|
|
255
|
-
const scrollableRange = orientation === "vertical" ? Math.max(0,
|
|
222
|
+
const scrollableRange = orientation === "vertical" ? Math.max(0, currentScrollState.scrollHeight - currentScrollState.clientHeight) : Math.max(0, currentScrollState.scrollWidth - currentScrollState.clientWidth);
|
|
256
223
|
const scrollbarRange = orientation === "vertical" ? scrollbarRect.height : scrollbarRect.width;
|
|
257
224
|
if (scrollableRange <= 0 || scrollbarRange <= 0) return;
|
|
225
|
+
const thumbFraction = Math.max(
|
|
226
|
+
0.1,
|
|
227
|
+
orientation === "vertical" ? currentScrollState.clientHeight / currentScrollState.scrollHeight : currentScrollState.clientWidth / currentScrollState.scrollWidth
|
|
228
|
+
);
|
|
229
|
+
const thumbSizePixels = scrollbarRange * thumbFraction;
|
|
230
|
+
const effectiveTrackRange = scrollbarRange - thumbSizePixels;
|
|
231
|
+
if (effectiveTrackRange <= 0) return;
|
|
258
232
|
dragContextRef.current = {
|
|
259
233
|
scrollbarRect,
|
|
260
234
|
scrollableRange,
|
|
@@ -262,34 +236,26 @@ function useThumbDrag(viewport, scrollState, orientation) {
|
|
|
262
236
|
};
|
|
263
237
|
isDragging.current = true;
|
|
264
238
|
startPos.current = orientation === "vertical" ? e.clientY : e.clientX;
|
|
265
|
-
startScroll.current = orientation === "vertical" ?
|
|
266
|
-
const scrollRatio = scrollableRange /
|
|
239
|
+
startScroll.current = orientation === "vertical" ? currentScrollState.scrollTop : currentScrollState.scrollLeft;
|
|
240
|
+
const scrollRatio = scrollableRange / effectiveTrackRange;
|
|
267
241
|
const handleMouseMove = (e2) => {
|
|
268
242
|
if (!isDragging.current || !viewport || !dragContextRef.current) return;
|
|
269
|
-
|
|
270
|
-
|
|
243
|
+
const currentPos = orientation === "vertical" ? e2.clientY : e2.clientX;
|
|
244
|
+
const delta = currentPos - startPos.current;
|
|
245
|
+
const scrollDelta = delta * scrollRatio;
|
|
246
|
+
const newScrollValue = Math.max(
|
|
247
|
+
0,
|
|
248
|
+
Math.min(startScroll.current + scrollDelta, dragContextRef.current.scrollableRange)
|
|
249
|
+
);
|
|
250
|
+
if (orientation === "vertical") {
|
|
251
|
+
viewport.scrollTop = newScrollValue;
|
|
252
|
+
} else {
|
|
253
|
+
viewport.scrollLeft = newScrollValue;
|
|
271
254
|
}
|
|
272
|
-
rafId.current = requestAnimationFrame(() => {
|
|
273
|
-
const currentPos = orientation === "vertical" ? e2.clientY : e2.clientX;
|
|
274
|
-
const delta = currentPos - startPos.current;
|
|
275
|
-
const scrollDelta = delta * scrollRatio;
|
|
276
|
-
const newScrollValue = Math.max(
|
|
277
|
-
0,
|
|
278
|
-
Math.min(startScroll.current + scrollDelta, dragContextRef.current.scrollableRange)
|
|
279
|
-
);
|
|
280
|
-
if (orientation === "vertical") {
|
|
281
|
-
viewport.scrollTop = newScrollValue;
|
|
282
|
-
} else {
|
|
283
|
-
viewport.scrollLeft = newScrollValue;
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
255
|
};
|
|
287
256
|
const handleMouseUp = () => {
|
|
288
257
|
isDragging.current = false;
|
|
289
258
|
dragContextRef.current = null;
|
|
290
|
-
if (rafId.current) {
|
|
291
|
-
cancelAnimationFrame(rafId.current);
|
|
292
|
-
}
|
|
293
259
|
document.removeEventListener("mousemove", handleMouseMove);
|
|
294
260
|
document.removeEventListener("mouseup", handleMouseUp);
|
|
295
261
|
cleanupRef.current = null;
|
|
@@ -303,7 +269,7 @@ function useThumbDrag(viewport, scrollState, orientation) {
|
|
|
303
269
|
document.addEventListener("mouseup", handleMouseUp, { passive: true });
|
|
304
270
|
e.preventDefault();
|
|
305
271
|
},
|
|
306
|
-
[viewport, orientation
|
|
272
|
+
[viewport, orientation]
|
|
307
273
|
);
|
|
308
274
|
return {
|
|
309
275
|
isDragging: isDragging.current,
|
|
@@ -609,6 +575,8 @@ var ScrollAreaScrollbar = forwardRef(
|
|
|
609
575
|
scrollbarXId,
|
|
610
576
|
scrollbarYId
|
|
611
577
|
} = useScrollAreaContext();
|
|
578
|
+
const scrollStateRef = useRef(scrollState);
|
|
579
|
+
scrollStateRef.current = scrollState;
|
|
612
580
|
const hasOverflow = useHasOverflow(scrollState, orientation);
|
|
613
581
|
const shouldShow = useScrollbarShouldShow(type, hasOverflow, isScrolling, isHovering);
|
|
614
582
|
const scrollPercentage = useMemo(() => {
|
|
@@ -619,15 +587,23 @@ var ScrollAreaScrollbar = forwardRef(
|
|
|
619
587
|
const maxScroll = scrollState.scrollWidth - scrollState.clientWidth;
|
|
620
588
|
return maxScroll > 0 ? Math.round(scrollState.scrollLeft / maxScroll * 100) : 0;
|
|
621
589
|
}
|
|
622
|
-
}, [
|
|
590
|
+
}, [
|
|
591
|
+
orientation,
|
|
592
|
+
scrollState.scrollTop,
|
|
593
|
+
scrollState.scrollLeft,
|
|
594
|
+
scrollState.scrollHeight,
|
|
595
|
+
scrollState.clientHeight,
|
|
596
|
+
scrollState.scrollWidth,
|
|
597
|
+
scrollState.clientWidth
|
|
598
|
+
]);
|
|
623
599
|
const handleTrackClick = useCallback(
|
|
624
600
|
(e) => {
|
|
625
601
|
if (!viewport) return;
|
|
626
602
|
if (e.target === e.currentTarget) {
|
|
627
|
-
handleScrollbarTrackClick(e, viewport,
|
|
603
|
+
handleScrollbarTrackClick(e, viewport, scrollStateRef.current, orientation);
|
|
628
604
|
}
|
|
629
605
|
},
|
|
630
|
-
[viewport,
|
|
606
|
+
[viewport, orientation]
|
|
631
607
|
);
|
|
632
608
|
const tv = useMemo(
|
|
633
609
|
() => ScrollTv({
|
|
@@ -746,7 +722,7 @@ var ScrollAreaRoot = forwardRef(
|
|
|
746
722
|
const viewportId = `scroll-viewport${reactId}`;
|
|
747
723
|
const scrollbarXId = `scroll-x${reactId}`;
|
|
748
724
|
const scrollbarYId = `scroll-y${reactId}`;
|
|
749
|
-
const { scrollState, isHovering, isScrolling, handleMouseEnter, handleMouseLeave } = useScrollStateAndVisibility(viewport);
|
|
725
|
+
const { scrollState, isHovering, isScrolling, handleMouseEnter, handleMouseLeave } = useScrollStateAndVisibility(viewport, content);
|
|
750
726
|
const staticConfig = useMemo(
|
|
751
727
|
() => ({
|
|
752
728
|
orientation,
|
|
@@ -757,11 +733,14 @@ var ScrollAreaRoot = forwardRef(
|
|
|
757
733
|
}),
|
|
758
734
|
[orientation, scrollbarMode, hoverBoundary, variant, type]
|
|
759
735
|
);
|
|
760
|
-
const
|
|
736
|
+
const stateValue = useMemo(
|
|
737
|
+
() => ({ scrollState, isHovering, isScrolling }),
|
|
738
|
+
[scrollState, isHovering, isScrolling]
|
|
739
|
+
);
|
|
740
|
+
const configValue = useMemo(
|
|
761
741
|
() => ({
|
|
762
742
|
content,
|
|
763
743
|
orientation: staticConfig.orientation,
|
|
764
|
-
scrollState,
|
|
765
744
|
scrollbarMode: staticConfig.scrollbarMode,
|
|
766
745
|
hoverBoundary: staticConfig.hoverBoundary,
|
|
767
746
|
scrollbarX,
|
|
@@ -777,9 +756,6 @@ var ScrollAreaRoot = forwardRef(
|
|
|
777
756
|
variant: staticConfig.variant,
|
|
778
757
|
viewport,
|
|
779
758
|
type: staticConfig.type,
|
|
780
|
-
isHovering,
|
|
781
|
-
isScrolling,
|
|
782
|
-
// Add ID-related values
|
|
783
759
|
rootId,
|
|
784
760
|
viewportId,
|
|
785
761
|
scrollbarXId,
|
|
@@ -787,14 +763,11 @@ var ScrollAreaRoot = forwardRef(
|
|
|
787
763
|
}),
|
|
788
764
|
[
|
|
789
765
|
content,
|
|
790
|
-
scrollState,
|
|
791
766
|
scrollbarX,
|
|
792
767
|
scrollbarY,
|
|
793
768
|
thumbX,
|
|
794
769
|
thumbY,
|
|
795
770
|
viewport,
|
|
796
|
-
isHovering,
|
|
797
|
-
isScrolling,
|
|
798
771
|
staticConfig,
|
|
799
772
|
rootId,
|
|
800
773
|
viewportId,
|
|
@@ -859,7 +832,7 @@ var ScrollAreaRoot = forwardRef(
|
|
|
859
832
|
}
|
|
860
833
|
return scrollbars;
|
|
861
834
|
}, [orientation]);
|
|
862
|
-
return /* @__PURE__ */ jsx(
|
|
835
|
+
return /* @__PURE__ */ jsx(ScrollAreaConfigContext.Provider, { value: configValue, children: /* @__PURE__ */ jsx(ScrollAreaStateContext.Provider, { value: stateValue, children: /* @__PURE__ */ jsxs(
|
|
863
836
|
"div",
|
|
864
837
|
{
|
|
865
838
|
ref,
|
|
@@ -875,13 +848,13 @@ var ScrollAreaRoot = forwardRef(
|
|
|
875
848
|
autoScrollbars
|
|
876
849
|
]
|
|
877
850
|
}
|
|
878
|
-
) });
|
|
851
|
+
) }) });
|
|
879
852
|
}
|
|
880
853
|
);
|
|
881
854
|
ScrollAreaRoot.displayName = "ScrollArea.Root";
|
|
882
855
|
var ScrollAreaViewport = forwardRef(
|
|
883
856
|
({ className, children, ...props }, ref) => {
|
|
884
|
-
const { setViewport, orientation, viewportId } =
|
|
857
|
+
const { setViewport, orientation, viewportId } = useScrollAreaConfigContext();
|
|
885
858
|
const scrollClass = useMemo(() => {
|
|
886
859
|
switch (orientation) {
|
|
887
860
|
case "horizontal":
|
|
@@ -918,7 +891,7 @@ var ScrollAreaViewport = forwardRef(
|
|
|
918
891
|
ScrollAreaViewport.displayName = "ScrollArea.Viewport";
|
|
919
892
|
var ScrollAreaContent = forwardRef(
|
|
920
893
|
({ as: As = "div", className, children, ...props }, ref) => {
|
|
921
|
-
const { setContent, orientation } =
|
|
894
|
+
const { setContent, orientation } = useScrollAreaConfigContext();
|
|
922
895
|
const setRef = useCallback(
|
|
923
896
|
(node) => {
|
|
924
897
|
setContent(node);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, useCallback, useMemo } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import { useScrollAreaConfigContext } from "../context/scroll-area-context.js";
|
|
4
4
|
import { tcx } from "../../../../shared/utils/tcx/tcx.js";
|
|
5
5
|
const ScrollAreaContent = forwardRef(
|
|
6
6
|
({ as: As = "div", className, children, ...props }, ref) => {
|
|
7
|
-
const { setContent, orientation } =
|
|
7
|
+
const { setContent, orientation } = useScrollAreaConfigContext();
|
|
8
8
|
const setRef = useCallback(
|
|
9
9
|
(node) => {
|
|
10
10
|
setContent(node);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, useState, useId, useMemo } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import { ScrollAreaConfigContext, ScrollAreaStateContext } from "../context/scroll-area-context.js";
|
|
4
4
|
import { ScrollTv } from "../tv.js";
|
|
5
5
|
import { ScrollAreaCorner } from "./scroll-area-corner.js";
|
|
6
6
|
import { ScrollAreaScrollbar } from "./scroll-area-scrollbar.js";
|
|
@@ -32,7 +32,7 @@ const ScrollAreaRoot = forwardRef(
|
|
|
32
32
|
const viewportId = `scroll-viewport${reactId}`;
|
|
33
33
|
const scrollbarXId = `scroll-x${reactId}`;
|
|
34
34
|
const scrollbarYId = `scroll-y${reactId}`;
|
|
35
|
-
const { scrollState, isHovering, isScrolling, handleMouseEnter, handleMouseLeave } = useScrollStateAndVisibility(viewport);
|
|
35
|
+
const { scrollState, isHovering, isScrolling, handleMouseEnter, handleMouseLeave } = useScrollStateAndVisibility(viewport, content);
|
|
36
36
|
const staticConfig = useMemo(
|
|
37
37
|
() => ({
|
|
38
38
|
orientation,
|
|
@@ -43,11 +43,14 @@ const ScrollAreaRoot = forwardRef(
|
|
|
43
43
|
}),
|
|
44
44
|
[orientation, scrollbarMode, hoverBoundary, variant, type]
|
|
45
45
|
);
|
|
46
|
-
const
|
|
46
|
+
const stateValue = useMemo(
|
|
47
|
+
() => ({ scrollState, isHovering, isScrolling }),
|
|
48
|
+
[scrollState, isHovering, isScrolling]
|
|
49
|
+
);
|
|
50
|
+
const configValue = useMemo(
|
|
47
51
|
() => ({
|
|
48
52
|
content,
|
|
49
53
|
orientation: staticConfig.orientation,
|
|
50
|
-
scrollState,
|
|
51
54
|
scrollbarMode: staticConfig.scrollbarMode,
|
|
52
55
|
hoverBoundary: staticConfig.hoverBoundary,
|
|
53
56
|
scrollbarX,
|
|
@@ -63,9 +66,6 @@ const ScrollAreaRoot = forwardRef(
|
|
|
63
66
|
variant: staticConfig.variant,
|
|
64
67
|
viewport,
|
|
65
68
|
type: staticConfig.type,
|
|
66
|
-
isHovering,
|
|
67
|
-
isScrolling,
|
|
68
|
-
// Add ID-related values
|
|
69
69
|
rootId,
|
|
70
70
|
viewportId,
|
|
71
71
|
scrollbarXId,
|
|
@@ -73,14 +73,11 @@ const ScrollAreaRoot = forwardRef(
|
|
|
73
73
|
}),
|
|
74
74
|
[
|
|
75
75
|
content,
|
|
76
|
-
scrollState,
|
|
77
76
|
scrollbarX,
|
|
78
77
|
scrollbarY,
|
|
79
78
|
thumbX,
|
|
80
79
|
thumbY,
|
|
81
80
|
viewport,
|
|
82
|
-
isHovering,
|
|
83
|
-
isScrolling,
|
|
84
81
|
staticConfig,
|
|
85
82
|
rootId,
|
|
86
83
|
viewportId,
|
|
@@ -145,7 +142,7 @@ const ScrollAreaRoot = forwardRef(
|
|
|
145
142
|
}
|
|
146
143
|
return scrollbars;
|
|
147
144
|
}, [orientation]);
|
|
148
|
-
return /* @__PURE__ */ jsx(
|
|
145
|
+
return /* @__PURE__ */ jsx(ScrollAreaConfigContext.Provider, { value: configValue, children: /* @__PURE__ */ jsx(ScrollAreaStateContext.Provider, { value: stateValue, children: /* @__PURE__ */ jsxs(
|
|
149
146
|
"div",
|
|
150
147
|
{
|
|
151
148
|
ref,
|
|
@@ -161,7 +158,7 @@ const ScrollAreaRoot = forwardRef(
|
|
|
161
158
|
autoScrollbars
|
|
162
159
|
]
|
|
163
160
|
}
|
|
164
|
-
) });
|
|
161
|
+
) }) });
|
|
165
162
|
}
|
|
166
163
|
);
|
|
167
164
|
ScrollAreaRoot.displayName = "ScrollArea.Root";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef, useMemo, useCallback } from "react";
|
|
2
|
+
import { forwardRef, useRef, useMemo, useCallback } from "react";
|
|
3
3
|
import { useScrollAreaContext } from "../context/scroll-area-context.js";
|
|
4
4
|
import { ScrollTv } from "../tv.js";
|
|
5
5
|
import { handleScrollbarTrackClick, getScrollbarPositionStyle } from "../utils/index.js";
|
|
@@ -22,6 +22,8 @@ const ScrollAreaScrollbar = forwardRef(
|
|
|
22
22
|
scrollbarXId,
|
|
23
23
|
scrollbarYId
|
|
24
24
|
} = useScrollAreaContext();
|
|
25
|
+
const scrollStateRef = useRef(scrollState);
|
|
26
|
+
scrollStateRef.current = scrollState;
|
|
25
27
|
const hasOverflow = useHasOverflow(scrollState, orientation);
|
|
26
28
|
const shouldShow = useScrollbarShouldShow(type, hasOverflow, isScrolling, isHovering);
|
|
27
29
|
const scrollPercentage = useMemo(() => {
|
|
@@ -32,15 +34,23 @@ const ScrollAreaScrollbar = forwardRef(
|
|
|
32
34
|
const maxScroll = scrollState.scrollWidth - scrollState.clientWidth;
|
|
33
35
|
return maxScroll > 0 ? Math.round(scrollState.scrollLeft / maxScroll * 100) : 0;
|
|
34
36
|
}
|
|
35
|
-
}, [
|
|
37
|
+
}, [
|
|
38
|
+
orientation,
|
|
39
|
+
scrollState.scrollTop,
|
|
40
|
+
scrollState.scrollLeft,
|
|
41
|
+
scrollState.scrollHeight,
|
|
42
|
+
scrollState.clientHeight,
|
|
43
|
+
scrollState.scrollWidth,
|
|
44
|
+
scrollState.clientWidth
|
|
45
|
+
]);
|
|
36
46
|
const handleTrackClick = useCallback(
|
|
37
47
|
(e) => {
|
|
38
48
|
if (!viewport) return;
|
|
39
49
|
if (e.target === e.currentTarget) {
|
|
40
|
-
handleScrollbarTrackClick(e, viewport,
|
|
50
|
+
handleScrollbarTrackClick(e, viewport, scrollStateRef.current, orientation);
|
|
41
51
|
}
|
|
42
52
|
},
|
|
43
|
-
[viewport,
|
|
53
|
+
[viewport, orientation]
|
|
44
54
|
);
|
|
45
55
|
const tv = useMemo(
|
|
46
56
|
() => ScrollTv({
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, useMemo, useCallback } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import { useScrollAreaConfigContext } from "../context/scroll-area-context.js";
|
|
4
4
|
import { tcx } from "../../../../shared/utils/tcx/tcx.js";
|
|
5
5
|
const ScrollAreaViewport = forwardRef(
|
|
6
6
|
({ className, children, ...props }, ref) => {
|
|
7
|
-
const { setViewport, orientation, viewportId } =
|
|
7
|
+
const { setViewport, orientation, viewportId } = useScrollAreaConfigContext();
|
|
8
8
|
const scrollClass = useMemo(() => {
|
|
9
9
|
switch (orientation) {
|
|
10
10
|
case "horizontal":
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
-
import { ScrollAreaContextType } from '../types';
|
|
2
|
-
export declare const
|
|
1
|
+
import { ScrollAreaConfigContextType, ScrollAreaContextType, ScrollAreaStateContextType } from '../types';
|
|
2
|
+
export declare const ScrollAreaStateContext: import('react').Context<ScrollAreaStateContextType | null>;
|
|
3
|
+
export declare const ScrollAreaConfigContext: import('react').Context<ScrollAreaConfigContextType | null>;
|
|
4
|
+
/**
|
|
5
|
+
* Access only the frequently-changing state (scrollState, isHovering, isScrolling).
|
|
6
|
+
* Use this in components that need to react to scroll position changes.
|
|
7
|
+
*/
|
|
8
|
+
export declare function useScrollAreaStateContext(): ScrollAreaStateContextType;
|
|
9
|
+
/**
|
|
10
|
+
* Access only the rarely-changing config (orientation, setters, refs, IDs).
|
|
11
|
+
* Use this in components that do NOT need scrollState — they won't re-render on scroll.
|
|
12
|
+
*/
|
|
13
|
+
export declare function useScrollAreaConfigContext(): ScrollAreaConfigContextType;
|
|
14
|
+
/**
|
|
15
|
+
* Combined hook for components that need both state and config (Scrollbar, Thumb, Corner).
|
|
16
|
+
* Maintains backward compatibility with existing useScrollAreaContext() usage.
|
|
17
|
+
*/
|
|
3
18
|
export declare function useScrollAreaContext(): ScrollAreaContextType;
|
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
import { createContext, useContext } from "react";
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
const ScrollAreaStateContext = createContext(null);
|
|
3
|
+
const ScrollAreaConfigContext = createContext(null);
|
|
4
|
+
const ERROR_MESSAGE = "ScrollArea compound components must be used within ScrollArea";
|
|
5
|
+
function useScrollAreaStateContext() {
|
|
6
|
+
const context = useContext(ScrollAreaStateContext);
|
|
5
7
|
if (!context) {
|
|
6
|
-
throw new Error(
|
|
8
|
+
throw new Error(ERROR_MESSAGE);
|
|
7
9
|
}
|
|
8
10
|
return context;
|
|
9
11
|
}
|
|
12
|
+
function useScrollAreaConfigContext() {
|
|
13
|
+
const context = useContext(ScrollAreaConfigContext);
|
|
14
|
+
if (!context) {
|
|
15
|
+
throw new Error(ERROR_MESSAGE);
|
|
16
|
+
}
|
|
17
|
+
return context;
|
|
18
|
+
}
|
|
19
|
+
function useScrollAreaContext() {
|
|
20
|
+
const state = useScrollAreaStateContext();
|
|
21
|
+
const config = useScrollAreaConfigContext();
|
|
22
|
+
return { ...state, ...config };
|
|
23
|
+
}
|
|
10
24
|
export {
|
|
11
|
-
|
|
12
|
-
|
|
25
|
+
ScrollAreaConfigContext,
|
|
26
|
+
ScrollAreaStateContext,
|
|
27
|
+
useScrollAreaConfigContext,
|
|
28
|
+
useScrollAreaContext,
|
|
29
|
+
useScrollAreaStateContext
|
|
13
30
|
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ScrollState } from '../types';
|
|
2
2
|
/**
|
|
3
|
-
* Merged scroll state and visibility management hook
|
|
3
|
+
* Merged scroll state and visibility management hook
|
|
4
4
|
*/
|
|
5
|
-
export declare function useScrollStateAndVisibility(viewport: HTMLDivElement | null): {
|
|
5
|
+
export declare function useScrollStateAndVisibility(viewport: HTMLDivElement | null, content: HTMLDivElement | null): {
|
|
6
6
|
scrollState: ScrollState;
|
|
7
7
|
isHovering: boolean;
|
|
8
8
|
isScrolling: boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useRef, useCallback, useEffect } from "react";
|
|
2
|
-
function useScrollStateAndVisibility(viewport) {
|
|
2
|
+
function useScrollStateAndVisibility(viewport, content) {
|
|
3
3
|
const [scrollState, setScrollState] = useState({
|
|
4
4
|
scrollLeft: 0,
|
|
5
5
|
scrollTop: 0,
|
|
@@ -13,8 +13,6 @@ function useScrollStateAndVisibility(viewport) {
|
|
|
13
13
|
const scrollTimeoutRef = useRef();
|
|
14
14
|
const rafRef = useRef();
|
|
15
15
|
const resizeObserverRef = useRef();
|
|
16
|
-
const mutationObserverRef = useRef();
|
|
17
|
-
const mutationTimeoutRef = useRef();
|
|
18
16
|
const lastUpdateTimeRef = useRef(0);
|
|
19
17
|
const minUpdateIntervalRef = useRef(16);
|
|
20
18
|
const updateScrollState = useCallback(() => {
|
|
@@ -26,36 +24,36 @@ function useScrollStateAndVisibility(viewport) {
|
|
|
26
24
|
cancelAnimationFrame(rafRef.current);
|
|
27
25
|
}
|
|
28
26
|
rafRef.current = requestAnimationFrame(() => {
|
|
27
|
+
rafRef.current = void 0;
|
|
29
28
|
updateScrollState();
|
|
30
29
|
});
|
|
31
30
|
return;
|
|
32
31
|
}
|
|
33
32
|
if (rafRef.current) {
|
|
34
33
|
cancelAnimationFrame(rafRef.current);
|
|
34
|
+
rafRef.current = void 0;
|
|
35
35
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return prevState;
|
|
58
|
-
});
|
|
36
|
+
const newState = {
|
|
37
|
+
scrollLeft: viewport.scrollLeft,
|
|
38
|
+
scrollTop: viewport.scrollTop,
|
|
39
|
+
scrollWidth: viewport.scrollWidth,
|
|
40
|
+
scrollHeight: viewport.scrollHeight,
|
|
41
|
+
clientWidth: viewport.clientWidth,
|
|
42
|
+
clientHeight: viewport.clientHeight
|
|
43
|
+
};
|
|
44
|
+
setScrollState((prevState) => {
|
|
45
|
+
const scrollLeftChanged = Math.abs(prevState.scrollLeft - newState.scrollLeft) > 0.5;
|
|
46
|
+
const scrollTopChanged = Math.abs(prevState.scrollTop - newState.scrollTop) > 0.5;
|
|
47
|
+
const scrollWidthChanged = prevState.scrollWidth !== newState.scrollWidth;
|
|
48
|
+
const scrollHeightChanged = prevState.scrollHeight !== newState.scrollHeight;
|
|
49
|
+
const clientWidthChanged = prevState.clientWidth !== newState.clientWidth;
|
|
50
|
+
const clientHeightChanged = prevState.clientHeight !== newState.clientHeight;
|
|
51
|
+
const hasChanges = scrollLeftChanged || scrollTopChanged || scrollWidthChanged || scrollHeightChanged || clientWidthChanged || clientHeightChanged;
|
|
52
|
+
if (hasChanges) {
|
|
53
|
+
lastUpdateTimeRef.current = now;
|
|
54
|
+
return newState;
|
|
55
|
+
}
|
|
56
|
+
return prevState;
|
|
59
57
|
});
|
|
60
58
|
}, [viewport]);
|
|
61
59
|
const delayedUpdateScrollState = useCallback(() => {
|
|
@@ -92,54 +90,19 @@ function useScrollStateAndVisibility(viewport) {
|
|
|
92
90
|
passive: true,
|
|
93
91
|
signal,
|
|
94
92
|
capture: false
|
|
95
|
-
// Avoid unnecessary event capture
|
|
96
93
|
});
|
|
97
94
|
window.addEventListener("resize", handleResize, {
|
|
98
95
|
passive: true,
|
|
99
96
|
signal
|
|
100
97
|
});
|
|
101
98
|
if (window.ResizeObserver) {
|
|
102
|
-
resizeObserverRef.current = new ResizeObserver((
|
|
103
|
-
|
|
104
|
-
if (entry.target === viewport) {
|
|
105
|
-
updateScrollState();
|
|
106
|
-
break;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
99
|
+
resizeObserverRef.current = new ResizeObserver(() => {
|
|
100
|
+
updateScrollState();
|
|
109
101
|
});
|
|
110
102
|
resizeObserverRef.current.observe(viewport);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const hasLayoutChanges = mutations.some((mutation) => {
|
|
115
|
-
if (mutation.type === "childList") {
|
|
116
|
-
return mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0;
|
|
117
|
-
}
|
|
118
|
-
if (mutation.type === "attributes") {
|
|
119
|
-
const attr = mutation.attributeName;
|
|
120
|
-
return attr === "style" || attr === "class";
|
|
121
|
-
}
|
|
122
|
-
return mutation.type === "characterData";
|
|
123
|
-
});
|
|
124
|
-
if (!hasLayoutChanges) return;
|
|
125
|
-
if (mutationTimeoutRef.current) {
|
|
126
|
-
clearTimeout(mutationTimeoutRef.current);
|
|
127
|
-
}
|
|
128
|
-
mutationTimeoutRef.current = window.setTimeout(() => {
|
|
129
|
-
updateScrollState();
|
|
130
|
-
}, 16);
|
|
131
|
-
});
|
|
132
|
-
mutationObserverRef.current.observe(viewport, {
|
|
133
|
-
childList: true,
|
|
134
|
-
subtree: true,
|
|
135
|
-
attributes: true,
|
|
136
|
-
attributeFilter: ["style", "class"],
|
|
137
|
-
// Only listen to attributes that affect layout
|
|
138
|
-
characterData: true,
|
|
139
|
-
characterDataOldValue: false,
|
|
140
|
-
// No need for old value, improve performance
|
|
141
|
-
attributeOldValue: false
|
|
142
|
-
});
|
|
103
|
+
if (content) {
|
|
104
|
+
resizeObserverRef.current.observe(content);
|
|
105
|
+
}
|
|
143
106
|
}
|
|
144
107
|
delayedUpdateScrollState();
|
|
145
108
|
return () => {
|
|
@@ -148,10 +111,6 @@ function useScrollStateAndVisibility(viewport) {
|
|
|
148
111
|
clearTimeout(scrollTimeoutRef.current);
|
|
149
112
|
scrollTimeoutRef.current = void 0;
|
|
150
113
|
}
|
|
151
|
-
if (mutationTimeoutRef.current) {
|
|
152
|
-
clearTimeout(mutationTimeoutRef.current);
|
|
153
|
-
mutationTimeoutRef.current = void 0;
|
|
154
|
-
}
|
|
155
114
|
if (rafRef.current) {
|
|
156
115
|
cancelAnimationFrame(rafRef.current);
|
|
157
116
|
rafRef.current = void 0;
|
|
@@ -160,12 +119,8 @@ function useScrollStateAndVisibility(viewport) {
|
|
|
160
119
|
resizeObserverRef.current.disconnect();
|
|
161
120
|
resizeObserverRef.current = void 0;
|
|
162
121
|
}
|
|
163
|
-
if (mutationObserverRef.current) {
|
|
164
|
-
mutationObserverRef.current.disconnect();
|
|
165
|
-
mutationObserverRef.current = void 0;
|
|
166
|
-
}
|
|
167
122
|
};
|
|
168
|
-
}, [viewport, handleScroll, delayedUpdateScrollState]);
|
|
123
|
+
}, [viewport, content, handleScroll, delayedUpdateScrollState, updateScrollState]);
|
|
169
124
|
const handleMouseEnter = useCallback(() => setIsHovering(true), []);
|
|
170
125
|
const handleMouseLeave = useCallback(() => setIsHovering(false), []);
|
|
171
126
|
return {
|
|
@@ -14,7 +14,7 @@ export declare function useThumbStyle(scrollState: ScrollState, orientation: "ve
|
|
|
14
14
|
top?: undefined;
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
17
|
+
* High-performance thumb drag hook
|
|
18
18
|
*/
|
|
19
19
|
export declare function useThumbDrag(viewport: HTMLDivElement | null, scrollState: ScrollState, orientation: "vertical" | "horizontal"): {
|
|
20
20
|
isDragging: boolean;
|
|
@@ -42,16 +42,13 @@ function useThumbDrag(viewport, scrollState, orientation) {
|
|
|
42
42
|
const isDragging = useRef(false);
|
|
43
43
|
const startPos = useRef(0);
|
|
44
44
|
const startScroll = useRef(0);
|
|
45
|
-
const rafId = useRef();
|
|
46
45
|
const cleanupRef = useRef(null);
|
|
46
|
+
const scrollStateRef = useRef(scrollState);
|
|
47
|
+
scrollStateRef.current = scrollState;
|
|
47
48
|
const dragContextRef = useRef(null);
|
|
48
49
|
useEffect(() => {
|
|
49
50
|
return () => {
|
|
50
51
|
isDragging.current = false;
|
|
51
|
-
if (rafId.current) {
|
|
52
|
-
cancelAnimationFrame(rafId.current);
|
|
53
|
-
rafId.current = void 0;
|
|
54
|
-
}
|
|
55
52
|
if (cleanupRef.current) {
|
|
56
53
|
cleanupRef.current();
|
|
57
54
|
cleanupRef.current = null;
|
|
@@ -61,13 +58,21 @@ function useThumbDrag(viewport, scrollState, orientation) {
|
|
|
61
58
|
const handleMouseDown = useCallback(
|
|
62
59
|
(e) => {
|
|
63
60
|
if (!viewport) return;
|
|
61
|
+
const currentScrollState = scrollStateRef.current;
|
|
64
62
|
const target = e.currentTarget;
|
|
65
63
|
const scrollbar = target.closest('[role="scrollbar"]');
|
|
66
64
|
if (!scrollbar) return;
|
|
67
65
|
const scrollbarRect = scrollbar.getBoundingClientRect();
|
|
68
|
-
const scrollableRange = orientation === "vertical" ? Math.max(0,
|
|
66
|
+
const scrollableRange = orientation === "vertical" ? Math.max(0, currentScrollState.scrollHeight - currentScrollState.clientHeight) : Math.max(0, currentScrollState.scrollWidth - currentScrollState.clientWidth);
|
|
69
67
|
const scrollbarRange = orientation === "vertical" ? scrollbarRect.height : scrollbarRect.width;
|
|
70
68
|
if (scrollableRange <= 0 || scrollbarRange <= 0) return;
|
|
69
|
+
const thumbFraction = Math.max(
|
|
70
|
+
0.1,
|
|
71
|
+
orientation === "vertical" ? currentScrollState.clientHeight / currentScrollState.scrollHeight : currentScrollState.clientWidth / currentScrollState.scrollWidth
|
|
72
|
+
);
|
|
73
|
+
const thumbSizePixels = scrollbarRange * thumbFraction;
|
|
74
|
+
const effectiveTrackRange = scrollbarRange - thumbSizePixels;
|
|
75
|
+
if (effectiveTrackRange <= 0) return;
|
|
71
76
|
dragContextRef.current = {
|
|
72
77
|
scrollbarRect,
|
|
73
78
|
scrollableRange,
|
|
@@ -75,34 +80,26 @@ function useThumbDrag(viewport, scrollState, orientation) {
|
|
|
75
80
|
};
|
|
76
81
|
isDragging.current = true;
|
|
77
82
|
startPos.current = orientation === "vertical" ? e.clientY : e.clientX;
|
|
78
|
-
startScroll.current = orientation === "vertical" ?
|
|
79
|
-
const scrollRatio = scrollableRange /
|
|
83
|
+
startScroll.current = orientation === "vertical" ? currentScrollState.scrollTop : currentScrollState.scrollLeft;
|
|
84
|
+
const scrollRatio = scrollableRange / effectiveTrackRange;
|
|
80
85
|
const handleMouseMove = (e2) => {
|
|
81
86
|
if (!isDragging.current || !viewport || !dragContextRef.current) return;
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
const currentPos = orientation === "vertical" ? e2.clientY : e2.clientX;
|
|
88
|
+
const delta = currentPos - startPos.current;
|
|
89
|
+
const scrollDelta = delta * scrollRatio;
|
|
90
|
+
const newScrollValue = Math.max(
|
|
91
|
+
0,
|
|
92
|
+
Math.min(startScroll.current + scrollDelta, dragContextRef.current.scrollableRange)
|
|
93
|
+
);
|
|
94
|
+
if (orientation === "vertical") {
|
|
95
|
+
viewport.scrollTop = newScrollValue;
|
|
96
|
+
} else {
|
|
97
|
+
viewport.scrollLeft = newScrollValue;
|
|
84
98
|
}
|
|
85
|
-
rafId.current = requestAnimationFrame(() => {
|
|
86
|
-
const currentPos = orientation === "vertical" ? e2.clientY : e2.clientX;
|
|
87
|
-
const delta = currentPos - startPos.current;
|
|
88
|
-
const scrollDelta = delta * scrollRatio;
|
|
89
|
-
const newScrollValue = Math.max(
|
|
90
|
-
0,
|
|
91
|
-
Math.min(startScroll.current + scrollDelta, dragContextRef.current.scrollableRange)
|
|
92
|
-
);
|
|
93
|
-
if (orientation === "vertical") {
|
|
94
|
-
viewport.scrollTop = newScrollValue;
|
|
95
|
-
} else {
|
|
96
|
-
viewport.scrollLeft = newScrollValue;
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
99
|
};
|
|
100
100
|
const handleMouseUp = () => {
|
|
101
101
|
isDragging.current = false;
|
|
102
102
|
dragContextRef.current = null;
|
|
103
|
-
if (rafId.current) {
|
|
104
|
-
cancelAnimationFrame(rafId.current);
|
|
105
|
-
}
|
|
106
103
|
document.removeEventListener("mousemove", handleMouseMove);
|
|
107
104
|
document.removeEventListener("mouseup", handleMouseUp);
|
|
108
105
|
cleanupRef.current = null;
|
|
@@ -116,7 +113,7 @@ function useThumbDrag(viewport, scrollState, orientation) {
|
|
|
116
113
|
document.addEventListener("mouseup", handleMouseUp, { passive: true });
|
|
117
114
|
e.preventDefault();
|
|
118
115
|
},
|
|
119
|
-
[viewport, orientation
|
|
116
|
+
[viewport, orientation]
|
|
120
117
|
);
|
|
121
118
|
return {
|
|
122
119
|
isDragging: isDragging.current,
|
|
@@ -24,14 +24,22 @@ export interface ScrollPosition {
|
|
|
24
24
|
* Render prop function type
|
|
25
25
|
*/
|
|
26
26
|
export type ScrollAreaRenderProp = (position: ScrollPosition) => React.ReactNode;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Frequently changing state — triggers re-renders on scroll
|
|
29
|
+
*/
|
|
30
|
+
export interface ScrollAreaStateContextType {
|
|
30
31
|
isHovering: boolean;
|
|
31
32
|
isScrolling: boolean;
|
|
33
|
+
scrollState: ScrollState;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Rarely changing config, refs, setters — does NOT change on scroll
|
|
37
|
+
*/
|
|
38
|
+
export interface ScrollAreaConfigContextType {
|
|
39
|
+
content: HTMLDivElement | null;
|
|
40
|
+
hoverBoundary: HoverBoundary;
|
|
32
41
|
orientation: ScrollOrientation;
|
|
33
42
|
rootId: string;
|
|
34
|
-
scrollState: ScrollState;
|
|
35
43
|
scrollbarMode: ScrollbarMode;
|
|
36
44
|
scrollbarX: HTMLDivElement | null;
|
|
37
45
|
scrollbarXId: string;
|
|
@@ -50,6 +58,10 @@ export interface ScrollAreaContextType {
|
|
|
50
58
|
viewport: HTMLDivElement | null;
|
|
51
59
|
viewportId: string;
|
|
52
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Combined context type for components that need both state and config
|
|
63
|
+
*/
|
|
64
|
+
export type ScrollAreaContextType = ScrollAreaStateContextType & ScrollAreaConfigContextType;
|
|
53
65
|
export interface ScrollAreaProps extends Omit<React.ComponentPropsWithoutRef<"div">, "children"> {
|
|
54
66
|
/** Accessible name */
|
|
55
67
|
"aria-label"?: string;
|
package/dist/index.js
CHANGED
|
@@ -233,7 +233,6 @@ import { ScrollArea } from "./components/scroll-area/src/scroll-area.js";
|
|
|
233
233
|
import { useScrollStateAndVisibility } from "./components/scroll-area/src/hooks/use-scroll-state-and-visibility.js";
|
|
234
234
|
import { useThumbDrag, useThumbStyle } from "./components/scroll-area/src/hooks/use-thumb.js";
|
|
235
235
|
import { useHasOverflow, useScrollbarShouldShow } from "./components/scroll-area/src/hooks/use-scrollbar.js";
|
|
236
|
-
import { useScrollPerformanceMonitor } from "./components/scroll-area/src/hooks/use-scroll-performance-monitor.js";
|
|
237
236
|
import { SearchInput } from "./components/search-input/src/search-input.js";
|
|
238
237
|
import { Segmented } from "./components/segmented/src/segmented.js";
|
|
239
238
|
import { Select } from "./components/select/src/select.js";
|
|
@@ -770,7 +769,6 @@ export {
|
|
|
770
769
|
useRangeContext,
|
|
771
770
|
useRangeTupleContext,
|
|
772
771
|
useRenderData,
|
|
773
|
-
useScrollPerformanceMonitor,
|
|
774
772
|
useScrollStateAndVisibility,
|
|
775
773
|
useScrollbarShouldShow,
|
|
776
774
|
useSlot,
|
package/package.json
CHANGED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
interface PerformanceMetrics {
|
|
2
|
-
averageFrameTime: number;
|
|
3
|
-
droppedFrames: number;
|
|
4
|
-
maxFrameTime: number;
|
|
5
|
-
scrollEventFrequency: number;
|
|
6
|
-
updateFrequency: number;
|
|
7
|
-
}
|
|
8
|
-
interface PerformanceMonitorOptions {
|
|
9
|
-
enabled?: boolean;
|
|
10
|
-
frameTimeThreshold?: number;
|
|
11
|
-
logInterval?: number;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* 🔍 ScrollArea performance monitoring Hook
|
|
15
|
-
*
|
|
16
|
-
* Used to monitor and diagnose scroll performance issues, including:
|
|
17
|
-
* - Frame rate monitoring
|
|
18
|
-
* - Event frequency statistics
|
|
19
|
-
* - Performance bottleneck detection
|
|
20
|
-
* - Real-time performance reporting
|
|
21
|
-
*/
|
|
22
|
-
export declare function useScrollPerformanceMonitor(viewport: HTMLDivElement | null, options?: PerformanceMonitorOptions): PerformanceMetrics | null;
|
|
23
|
-
export {};
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { useState, useRef, useEffect } from "react";
|
|
2
|
-
function useScrollPerformanceMonitor(viewport, options = {}) {
|
|
3
|
-
const {
|
|
4
|
-
enabled = false,
|
|
5
|
-
// Default disabled, only enabled in development
|
|
6
|
-
logInterval = 5e3,
|
|
7
|
-
// Report every 5 seconds
|
|
8
|
-
frameTimeThreshold = 16.67
|
|
9
|
-
// 60fps threshold
|
|
10
|
-
} = options;
|
|
11
|
-
const [metrics, setMetrics] = useState({
|
|
12
|
-
averageFrameTime: 0,
|
|
13
|
-
maxFrameTime: 0,
|
|
14
|
-
droppedFrames: 0,
|
|
15
|
-
scrollEventFrequency: 0,
|
|
16
|
-
updateFrequency: 0
|
|
17
|
-
});
|
|
18
|
-
const countersRef = useRef({
|
|
19
|
-
frameCount: 0,
|
|
20
|
-
totalFrameTime: 0,
|
|
21
|
-
scrollEventCount: 0,
|
|
22
|
-
updateCount: 0,
|
|
23
|
-
lastReportTime: 0,
|
|
24
|
-
maxFrameTime: 0,
|
|
25
|
-
droppedFrames: 0
|
|
26
|
-
});
|
|
27
|
-
const lastFrameTimeRef = useRef(0);
|
|
28
|
-
const reportIntervalRef = useRef();
|
|
29
|
-
const updateIntervalRef = useRef();
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
if (!enabled || !viewport) return;
|
|
32
|
-
const startTime = performance.now();
|
|
33
|
-
countersRef.current.lastReportTime = startTime;
|
|
34
|
-
const handleScroll = () => {
|
|
35
|
-
countersRef.current.scrollEventCount++;
|
|
36
|
-
};
|
|
37
|
-
const monitorFrame = () => {
|
|
38
|
-
const now = performance.now();
|
|
39
|
-
if (lastFrameTimeRef.current > 0) {
|
|
40
|
-
const frameTime = now - lastFrameTimeRef.current;
|
|
41
|
-
countersRef.current.totalFrameTime += frameTime;
|
|
42
|
-
countersRef.current.frameCount++;
|
|
43
|
-
if (frameTime > countersRef.current.maxFrameTime) {
|
|
44
|
-
countersRef.current.maxFrameTime = frameTime;
|
|
45
|
-
}
|
|
46
|
-
if (frameTime > frameTimeThreshold) {
|
|
47
|
-
countersRef.current.droppedFrames++;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
lastFrameTimeRef.current = now;
|
|
51
|
-
countersRef.current.updateCount++;
|
|
52
|
-
requestAnimationFrame(monitorFrame);
|
|
53
|
-
};
|
|
54
|
-
viewport.addEventListener("scroll", handleScroll, { passive: true });
|
|
55
|
-
requestAnimationFrame(monitorFrame);
|
|
56
|
-
updateIntervalRef.current = window.setInterval(() => {
|
|
57
|
-
const now = performance.now();
|
|
58
|
-
const timeElapsed = Math.max(1, now - (countersRef.current.lastReportTime || startTime));
|
|
59
|
-
const currentMetrics = {
|
|
60
|
-
averageFrameTime: countersRef.current.frameCount > 0 ? countersRef.current.totalFrameTime / countersRef.current.frameCount : 0,
|
|
61
|
-
maxFrameTime: countersRef.current.maxFrameTime,
|
|
62
|
-
droppedFrames: countersRef.current.droppedFrames,
|
|
63
|
-
scrollEventFrequency: countersRef.current.scrollEventCount / timeElapsed * 1e3,
|
|
64
|
-
updateFrequency: countersRef.current.updateCount / timeElapsed * 1e3
|
|
65
|
-
};
|
|
66
|
-
setMetrics(currentMetrics);
|
|
67
|
-
}, 500);
|
|
68
|
-
reportIntervalRef.current = window.setInterval(() => {
|
|
69
|
-
const now = performance.now();
|
|
70
|
-
const timeElapsed = now - countersRef.current.lastReportTime;
|
|
71
|
-
const reportMetrics = {
|
|
72
|
-
averageFrameTime: countersRef.current.frameCount > 0 ? countersRef.current.totalFrameTime / countersRef.current.frameCount : 0,
|
|
73
|
-
maxFrameTime: countersRef.current.maxFrameTime,
|
|
74
|
-
droppedFrames: countersRef.current.droppedFrames,
|
|
75
|
-
scrollEventFrequency: countersRef.current.scrollEventCount / timeElapsed * 1e3,
|
|
76
|
-
updateFrequency: countersRef.current.updateCount / timeElapsed * 1e3
|
|
77
|
-
};
|
|
78
|
-
console.group("🔍 ScrollArea Performance Report");
|
|
79
|
-
console.log("📊 Frame Performance:");
|
|
80
|
-
console.log(` • Average frame time: ${reportMetrics.averageFrameTime.toFixed(2)}ms`);
|
|
81
|
-
console.log(` • Max frame time: ${reportMetrics.maxFrameTime.toFixed(2)}ms`);
|
|
82
|
-
console.log(` • Dropped frames: ${reportMetrics.droppedFrames}`);
|
|
83
|
-
console.log(` • Current FPS: ${(1e3 / reportMetrics.averageFrameTime).toFixed(1)}`);
|
|
84
|
-
console.log("⚡ Event Frequency:");
|
|
85
|
-
console.log(` • Scroll events/sec: ${reportMetrics.scrollEventFrequency.toFixed(1)}`);
|
|
86
|
-
console.log(` • Updates/sec: ${reportMetrics.updateFrequency.toFixed(1)}`);
|
|
87
|
-
if (reportMetrics.averageFrameTime > frameTimeThreshold) {
|
|
88
|
-
console.warn("⚠️ Performance Warning: Average frame time exceeds 60fps threshold");
|
|
89
|
-
}
|
|
90
|
-
if (reportMetrics.droppedFrames > 10) {
|
|
91
|
-
console.warn("⚠️ Performance Warning: High number of dropped frames detected");
|
|
92
|
-
}
|
|
93
|
-
if (reportMetrics.scrollEventFrequency > 200) {
|
|
94
|
-
console.warn(
|
|
95
|
-
"⚠️ Performance Warning: Very high scroll event frequency, consider throttling"
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
console.groupEnd();
|
|
99
|
-
countersRef.current = {
|
|
100
|
-
frameCount: 0,
|
|
101
|
-
totalFrameTime: 0,
|
|
102
|
-
scrollEventCount: 0,
|
|
103
|
-
updateCount: 0,
|
|
104
|
-
lastReportTime: now,
|
|
105
|
-
maxFrameTime: 0,
|
|
106
|
-
droppedFrames: 0
|
|
107
|
-
};
|
|
108
|
-
}, logInterval);
|
|
109
|
-
return () => {
|
|
110
|
-
viewport.removeEventListener("scroll", handleScroll);
|
|
111
|
-
if (reportIntervalRef.current) {
|
|
112
|
-
clearInterval(reportIntervalRef.current);
|
|
113
|
-
}
|
|
114
|
-
if (updateIntervalRef.current) {
|
|
115
|
-
clearInterval(updateIntervalRef.current);
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
}, [enabled, viewport, logInterval, frameTimeThreshold]);
|
|
119
|
-
return enabled ? metrics : null;
|
|
120
|
-
}
|
|
121
|
-
export {
|
|
122
|
-
useScrollPerformanceMonitor
|
|
123
|
-
};
|