@choice-ui/react 1.9.4 → 1.9.7
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/numeric-input/dist/index.d.ts +2 -0
- package/dist/components/numeric-input/dist/index.js +40 -13
- package/dist/components/numeric-input/src/context/numeric-input-context.d.ts +1 -0
- package/dist/components/numeric-input/src/hooks/use-input-interactions.d.ts +3 -1
- package/dist/components/numeric-input/src/hooks/use-numeric-input.d.ts +1 -0
- package/dist/components/numeric-input/src/hooks/use-numeric-input.js +36 -13
- package/dist/components/numeric-input/src/numeric-input.d.ts +1 -0
- package/dist/components/numeric-input/src/numeric-input.js +4 -0
- 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
|
@@ -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
|
-
};
|