react-sway 0.1.1 → 0.2.0
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/README.md +27 -5
- package/dist/index.cjs +314 -198
- package/dist/index.d.cts +55 -2
- package/dist/index.d.ts +55 -2
- package/dist/index.js +315 -199
- package/package.json +18 -12
- package/src/ReactSway.tsx +445 -230
- package/src/__tests__/ReactSway.test.tsx +508 -0
- package/src/__tests__/setup.ts +20 -0
- package/src/index.ts +5 -3
package/dist/index.js
CHANGED
|
@@ -1,205 +1,330 @@
|
|
|
1
1
|
// src/ReactSway.tsx
|
|
2
|
-
import {
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
-
var
|
|
5
|
-
var
|
|
6
|
-
var
|
|
4
|
+
var ARROW_KEY_VELOCITY = 15;
|
|
5
|
+
var DEFAULT_FRICTION = 0.95;
|
|
6
|
+
var DEFAULT_LAZY_ROOT_MARGIN = "100px";
|
|
7
|
+
var DEFAULT_LAZY_THRESHOLD = 0.01;
|
|
8
|
+
var DEFAULT_RESUME_DELAY = 2e3;
|
|
9
|
+
var DEFAULT_SPEED = 0.5;
|
|
10
|
+
var LOOP_SEGMENTS = 3;
|
|
7
11
|
var MAX_DELTA_TIME = 3;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
var MAX_VELOCITY = 150;
|
|
13
|
+
var MS_PER_FRAME_60FPS = 16.667;
|
|
14
|
+
var REDUCED_MOTION_SPEED_FACTOR = 0.25;
|
|
15
|
+
var RESIZE_DEBOUNCE_MS = 150;
|
|
16
|
+
var WHEEL_VELOCITY_MULTIPLIER = 0.3;
|
|
17
|
+
function ReactSway({
|
|
18
|
+
autoScroll = true,
|
|
19
|
+
children,
|
|
20
|
+
direction = "up",
|
|
21
|
+
draggable = true,
|
|
22
|
+
friction = DEFAULT_FRICTION,
|
|
23
|
+
keyboard = true,
|
|
24
|
+
lazy = true,
|
|
25
|
+
lazyRootMargin = DEFAULT_LAZY_ROOT_MARGIN,
|
|
26
|
+
lazyThreshold = DEFAULT_LAZY_THRESHOLD,
|
|
27
|
+
onPause,
|
|
28
|
+
onResume,
|
|
29
|
+
onScroll,
|
|
30
|
+
pauseOnInteraction = true,
|
|
31
|
+
resumeDelay = DEFAULT_RESUME_DELAY,
|
|
32
|
+
speed = DEFAULT_SPEED,
|
|
33
|
+
wheelEnabled = true
|
|
34
|
+
}) {
|
|
35
|
+
const normalizedFriction = useMemo(
|
|
36
|
+
() => Number.isFinite(friction) ? Math.min(Math.max(friction, 0), 1) : DEFAULT_FRICTION,
|
|
37
|
+
[friction]
|
|
38
|
+
);
|
|
39
|
+
const normalizedResumeDelay = useMemo(
|
|
40
|
+
() => Number.isFinite(resumeDelay) ? Math.max(0, resumeDelay) : DEFAULT_RESUME_DELAY,
|
|
41
|
+
[resumeDelay]
|
|
42
|
+
);
|
|
43
|
+
const normalizedSpeed = useMemo(
|
|
44
|
+
() => Number.isFinite(speed) ? Math.max(0, speed) : DEFAULT_SPEED,
|
|
45
|
+
[speed]
|
|
46
|
+
);
|
|
11
47
|
const [isDragging, setIsDragging] = useState(false);
|
|
12
48
|
const [isPaused, setIsPaused] = useState(false);
|
|
13
|
-
const [autoScrollEnabled, setAutoScrollEnabled] = useState(true);
|
|
14
49
|
const [isTabActive, setIsTabActive] = useState(true);
|
|
15
|
-
const [contentHeight, setContentHeight] = useState(0);
|
|
16
50
|
const [loopPoint, setLoopPoint] = useState(0);
|
|
17
|
-
const [,
|
|
18
|
-
|
|
51
|
+
const [prefersReducedMotion, setPrefersReducedMotion] = useState(() => {
|
|
52
|
+
if (typeof window === "undefined") return false;
|
|
53
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
54
|
+
});
|
|
19
55
|
const animationFrameRef = useRef(null);
|
|
56
|
+
const autoScrollRef = useRef({ active: autoScroll, desired: autoScroll });
|
|
57
|
+
const containerRef = useRef(null);
|
|
20
58
|
const inactivityTimerRef = useRef(null);
|
|
21
|
-
const
|
|
22
|
-
const
|
|
59
|
+
const isDraggingRef = useRef(false);
|
|
60
|
+
const isPausedRef = useRef(false);
|
|
23
61
|
const lastFrameTimeRef = useRef(0);
|
|
24
|
-
|
|
25
|
-
|
|
62
|
+
const lastMouseYRef = useRef(0);
|
|
63
|
+
const lastTouchYRef = useRef(0);
|
|
64
|
+
const loopPointRef = useRef(0);
|
|
65
|
+
const onPauseRef = useRef(onPause);
|
|
66
|
+
const onResumeRef = useRef(onResume);
|
|
67
|
+
const onScrollRef = useRef(onScroll);
|
|
68
|
+
const positionRef = useRef(0);
|
|
69
|
+
const resizeDebounceTimerRef = useRef(null);
|
|
70
|
+
const velocityRef = useRef(0);
|
|
26
71
|
useEffect(() => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
setContainerHeight(window.innerHeight);
|
|
37
|
-
}
|
|
72
|
+
onPauseRef.current = onPause;
|
|
73
|
+
onResumeRef.current = onResume;
|
|
74
|
+
onScrollRef.current = onScroll;
|
|
75
|
+
}, [onPause, onResume, onScroll]);
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (typeof window === "undefined") return;
|
|
78
|
+
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
79
|
+
const handleChange = (e) => {
|
|
80
|
+
setPrefersReducedMotion(e.matches);
|
|
38
81
|
};
|
|
39
|
-
|
|
40
|
-
calculateDimensions();
|
|
41
|
-
});
|
|
82
|
+
mediaQuery.addEventListener("change", handleChange);
|
|
42
83
|
return () => {
|
|
43
|
-
|
|
84
|
+
mediaQuery.removeEventListener("change", handleChange);
|
|
44
85
|
};
|
|
45
|
-
}, [
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
86
|
+
}, []);
|
|
87
|
+
const clearInactivityTimer = useCallback(() => {
|
|
88
|
+
if (!inactivityTimerRef.current) return;
|
|
89
|
+
clearTimeout(inactivityTimerRef.current);
|
|
90
|
+
inactivityTimerRef.current = null;
|
|
91
|
+
}, []);
|
|
92
|
+
const renderPosition = useCallback((rawPosition) => {
|
|
93
|
+
if (!containerRef.current) return;
|
|
94
|
+
const currentLoopPoint = loopPointRef.current;
|
|
95
|
+
let visualPosition = rawPosition % (currentLoopPoint || 1);
|
|
96
|
+
if (visualPosition > 0 && currentLoopPoint > 0) {
|
|
97
|
+
visualPosition -= currentLoopPoint;
|
|
50
98
|
}
|
|
99
|
+
containerRef.current.style.transform = `translate3d(0, ${visualPosition}px, 0)`;
|
|
51
100
|
}, []);
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
54
|
-
|
|
101
|
+
const commitPosition = useCallback((nextPosition) => {
|
|
102
|
+
if (positionRef.current === nextPosition) return;
|
|
103
|
+
positionRef.current = nextPosition;
|
|
104
|
+
renderPosition(nextPosition);
|
|
105
|
+
onScrollRef.current?.(nextPosition);
|
|
106
|
+
}, [renderPosition]);
|
|
107
|
+
const wrapPosition = useCallback((rawPosition) => {
|
|
108
|
+
const currentLoopPoint = loopPointRef.current;
|
|
109
|
+
if (currentLoopPoint <= 0) return rawPosition;
|
|
110
|
+
let wrappedPosition = rawPosition;
|
|
111
|
+
while (wrappedPosition > 0) {
|
|
112
|
+
wrappedPosition -= currentLoopPoint;
|
|
55
113
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
114
|
+
while (wrappedPosition < -currentLoopPoint * 2) {
|
|
115
|
+
wrappedPosition += currentLoopPoint;
|
|
116
|
+
}
|
|
117
|
+
return wrappedPosition;
|
|
59
118
|
}, []);
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}, [isDragging]);
|
|
75
|
-
const handleMouseUp = useCallback((e) => {
|
|
76
|
-
if (!isDragging) return;
|
|
77
|
-
e.preventDefault();
|
|
78
|
-
setIsDragging(false);
|
|
79
|
-
scheduleAutoScrollResume();
|
|
80
|
-
}, [isDragging, scheduleAutoScrollResume]);
|
|
81
|
-
const handleTouchStart = useCallback((e) => {
|
|
82
|
-
if (e.touches.length === 1) {
|
|
83
|
-
setIsDragging(true);
|
|
84
|
-
lastTouchYRef.current = e.touches[0].clientY;
|
|
85
|
-
setVelocity(0);
|
|
86
|
-
pauseAutoScroll();
|
|
119
|
+
const recalculateLoopPoint = useCallback(() => {
|
|
120
|
+
if (!containerRef.current) return;
|
|
121
|
+
const currentContentHeight = containerRef.current.scrollHeight;
|
|
122
|
+
if (currentContentHeight <= 0) return;
|
|
123
|
+
const nextLoopPoint = currentContentHeight / LOOP_SEGMENTS;
|
|
124
|
+
loopPointRef.current = nextLoopPoint;
|
|
125
|
+
setLoopPoint((previousLoopPoint) => Math.abs(previousLoopPoint - nextLoopPoint) < 0.5 ? previousLoopPoint : nextLoopPoint);
|
|
126
|
+
renderPosition(positionRef.current);
|
|
127
|
+
}, [renderPosition]);
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
autoScrollRef.current.desired = autoScroll;
|
|
130
|
+
if (!autoScroll) {
|
|
131
|
+
clearInactivityTimer();
|
|
132
|
+
autoScrollRef.current.active = true;
|
|
87
133
|
}
|
|
88
|
-
}, [
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
134
|
+
}, [autoScroll, clearInactivityTimer]);
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
loopPointRef.current = loopPoint;
|
|
137
|
+
renderPosition(positionRef.current);
|
|
138
|
+
}, [loopPoint, renderPosition]);
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
const rafId = requestAnimationFrame(recalculateLoopPoint);
|
|
141
|
+
return () => {
|
|
142
|
+
cancelAnimationFrame(rafId);
|
|
143
|
+
};
|
|
144
|
+
}, [children, recalculateLoopPoint]);
|
|
145
|
+
const pauseAutoScroll = useCallback(() => {
|
|
146
|
+
if (!pauseOnInteraction) return;
|
|
147
|
+
autoScrollRef.current.active = false;
|
|
148
|
+
onPauseRef.current?.();
|
|
149
|
+
clearInactivityTimer();
|
|
150
|
+
}, [clearInactivityTimer, pauseOnInteraction]);
|
|
151
|
+
const scheduleAutoScrollResume = useCallback(() => {
|
|
152
|
+
if (!pauseOnInteraction || !autoScrollRef.current.desired || isPausedRef.current) return;
|
|
153
|
+
clearInactivityTimer();
|
|
154
|
+
inactivityTimerRef.current = setTimeout(() => {
|
|
155
|
+
inactivityTimerRef.current = null;
|
|
156
|
+
if (!autoScrollRef.current.desired || isPausedRef.current) return;
|
|
157
|
+
autoScrollRef.current.active = true;
|
|
158
|
+
onResumeRef.current?.();
|
|
159
|
+
}, normalizedResumeDelay);
|
|
160
|
+
}, [clearInactivityTimer, normalizedResumeDelay, pauseOnInteraction]);
|
|
109
161
|
const togglePause = useCallback(() => {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
162
|
+
const newPaused = !isPausedRef.current;
|
|
163
|
+
isPausedRef.current = newPaused;
|
|
164
|
+
setIsPaused(newPaused);
|
|
165
|
+
if (newPaused) {
|
|
166
|
+
clearInactivityTimer();
|
|
167
|
+
autoScrollRef.current.active = false;
|
|
168
|
+
onPauseRef.current?.();
|
|
169
|
+
} else {
|
|
170
|
+
clearInactivityTimer();
|
|
171
|
+
autoScrollRef.current.active = true;
|
|
172
|
+
if (autoScrollRef.current.desired) {
|
|
173
|
+
onResumeRef.current?.();
|
|
116
174
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}, [pauseAutoScroll]);
|
|
175
|
+
}
|
|
176
|
+
}, [clearInactivityTimer]);
|
|
120
177
|
const handleKeyDown = useCallback((e) => {
|
|
178
|
+
if (!keyboard) return;
|
|
121
179
|
switch (e.key) {
|
|
122
180
|
case " ":
|
|
123
181
|
e.preventDefault();
|
|
124
182
|
togglePause();
|
|
125
183
|
break;
|
|
126
|
-
case "
|
|
184
|
+
case "ArrowDown":
|
|
127
185
|
e.preventDefault();
|
|
128
|
-
|
|
186
|
+
velocityRef.current -= ARROW_KEY_VELOCITY;
|
|
129
187
|
pauseAutoScroll();
|
|
130
188
|
scheduleAutoScrollResume();
|
|
131
189
|
break;
|
|
132
|
-
case "
|
|
190
|
+
case "ArrowUp":
|
|
133
191
|
e.preventDefault();
|
|
134
|
-
|
|
192
|
+
velocityRef.current += ARROW_KEY_VELOCITY;
|
|
135
193
|
pauseAutoScroll();
|
|
136
194
|
scheduleAutoScrollResume();
|
|
137
195
|
break;
|
|
138
|
-
case "
|
|
196
|
+
case "End":
|
|
139
197
|
e.preventDefault();
|
|
140
|
-
|
|
141
|
-
|
|
198
|
+
if (loopPointRef.current > 0) {
|
|
199
|
+
commitPosition(-loopPointRef.current);
|
|
200
|
+
}
|
|
201
|
+
velocityRef.current = 0;
|
|
142
202
|
pauseAutoScroll();
|
|
143
203
|
scheduleAutoScrollResume();
|
|
144
204
|
break;
|
|
145
|
-
case "
|
|
205
|
+
case "Home":
|
|
146
206
|
e.preventDefault();
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
setVelocity(0);
|
|
207
|
+
commitPosition(0);
|
|
208
|
+
velocityRef.current = 0;
|
|
151
209
|
pauseAutoScroll();
|
|
152
210
|
scheduleAutoScrollResume();
|
|
153
211
|
break;
|
|
154
212
|
default:
|
|
155
213
|
break;
|
|
156
214
|
}
|
|
157
|
-
}, [
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
215
|
+
}, [commitPosition, keyboard, pauseAutoScroll, scheduleAutoScrollResume, togglePause]);
|
|
216
|
+
const handleMouseDown = useCallback((e) => {
|
|
217
|
+
if (!draggable) return;
|
|
218
|
+
e.preventDefault();
|
|
219
|
+
containerRef.current?.focus();
|
|
220
|
+
setIsDragging(true);
|
|
221
|
+
isDraggingRef.current = true;
|
|
222
|
+
lastMouseYRef.current = e.clientY;
|
|
223
|
+
velocityRef.current = 0;
|
|
224
|
+
pauseAutoScroll();
|
|
225
|
+
}, [draggable, pauseAutoScroll]);
|
|
226
|
+
const handleMouseMove = useCallback((e) => {
|
|
227
|
+
if (!isDraggingRef.current) return;
|
|
228
|
+
e.preventDefault();
|
|
229
|
+
const deltaY = e.clientY - lastMouseYRef.current;
|
|
230
|
+
const nextPosition = wrapPosition(positionRef.current + deltaY);
|
|
231
|
+
commitPosition(nextPosition);
|
|
232
|
+
velocityRef.current = deltaY;
|
|
233
|
+
lastMouseYRef.current = e.clientY;
|
|
234
|
+
}, [commitPosition, wrapPosition]);
|
|
235
|
+
const handleMouseUp = useCallback((e) => {
|
|
236
|
+
if (!isDraggingRef.current) return;
|
|
237
|
+
e.preventDefault();
|
|
238
|
+
setIsDragging(false);
|
|
239
|
+
isDraggingRef.current = false;
|
|
240
|
+
scheduleAutoScrollResume();
|
|
241
|
+
}, [scheduleAutoScrollResume]);
|
|
242
|
+
const handleTouchEnd = useCallback((_e) => {
|
|
243
|
+
if (!isDraggingRef.current) return;
|
|
244
|
+
setIsDragging(false);
|
|
245
|
+
isDraggingRef.current = false;
|
|
246
|
+
scheduleAutoScrollResume();
|
|
247
|
+
}, [scheduleAutoScrollResume]);
|
|
248
|
+
const handleTouchMove = useCallback((e) => {
|
|
249
|
+
if (!isDraggingRef.current || e.touches.length !== 1) return;
|
|
250
|
+
e.preventDefault();
|
|
251
|
+
const touch = e.touches[0];
|
|
252
|
+
const deltaY = touch.clientY - lastTouchYRef.current;
|
|
253
|
+
const nextPosition = wrapPosition(positionRef.current + deltaY);
|
|
254
|
+
commitPosition(nextPosition);
|
|
255
|
+
velocityRef.current = deltaY;
|
|
256
|
+
lastTouchYRef.current = touch.clientY;
|
|
257
|
+
}, [commitPosition, wrapPosition]);
|
|
258
|
+
const handleTouchStart = useCallback((e) => {
|
|
259
|
+
if (!draggable || e.touches.length !== 1) return;
|
|
260
|
+
containerRef.current?.focus();
|
|
261
|
+
setIsDragging(true);
|
|
262
|
+
isDraggingRef.current = true;
|
|
263
|
+
lastTouchYRef.current = e.touches[0].clientY;
|
|
264
|
+
velocityRef.current = 0;
|
|
265
|
+
pauseAutoScroll();
|
|
266
|
+
}, [draggable, pauseAutoScroll]);
|
|
267
|
+
const handleWheel = useCallback((e) => {
|
|
268
|
+
if (!wheelEnabled) return;
|
|
269
|
+
e.preventDefault();
|
|
270
|
+
velocityRef.current -= e.deltaY * WHEEL_VELOCITY_MULTIPLIER;
|
|
271
|
+
velocityRef.current = Math.max(-MAX_VELOCITY, Math.min(MAX_VELOCITY, velocityRef.current));
|
|
272
|
+
pauseAutoScroll();
|
|
273
|
+
scheduleAutoScrollResume();
|
|
274
|
+
}, [pauseAutoScroll, scheduleAutoScrollResume, wheelEnabled]);
|
|
166
275
|
useEffect(() => {
|
|
167
276
|
const currentContainer = containerRef.current;
|
|
168
277
|
if (!currentContainer) return;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
278
|
+
currentContainer.addEventListener("mousedown", handleMouseDown);
|
|
279
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
280
|
+
window.addEventListener("mouseup", handleMouseUp);
|
|
281
|
+
currentContainer.addEventListener("touchstart", handleTouchStart, { passive: true });
|
|
282
|
+
window.addEventListener("touchmove", handleTouchMove, { passive: false });
|
|
283
|
+
window.addEventListener("touchend", handleTouchEnd, { passive: true });
|
|
284
|
+
currentContainer.addEventListener("wheel", handleWheel, { passive: false });
|
|
285
|
+
return () => {
|
|
286
|
+
currentContainer.removeEventListener("mousedown", handleMouseDown);
|
|
287
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
288
|
+
window.removeEventListener("mouseup", handleMouseUp);
|
|
289
|
+
currentContainer.removeEventListener("touchstart", handleTouchStart);
|
|
290
|
+
window.removeEventListener("touchmove", handleTouchMove);
|
|
291
|
+
window.removeEventListener("touchend", handleTouchEnd);
|
|
292
|
+
currentContainer.removeEventListener("wheel", handleWheel);
|
|
177
293
|
};
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
294
|
+
}, [handleMouseDown, handleMouseMove, handleMouseUp, handleTouchEnd, handleTouchMove, handleTouchStart, handleWheel]);
|
|
295
|
+
const debouncedRecalculate = useCallback(() => {
|
|
296
|
+
if (resizeDebounceTimerRef.current) {
|
|
297
|
+
clearTimeout(resizeDebounceTimerRef.current);
|
|
298
|
+
}
|
|
299
|
+
resizeDebounceTimerRef.current = setTimeout(() => {
|
|
300
|
+
resizeDebounceTimerRef.current = null;
|
|
301
|
+
recalculateLoopPoint();
|
|
302
|
+
}, RESIZE_DEBOUNCE_MS);
|
|
303
|
+
}, [recalculateLoopPoint]);
|
|
304
|
+
useEffect(() => {
|
|
305
|
+
window.addEventListener("resize", debouncedRecalculate);
|
|
185
306
|
return () => {
|
|
186
|
-
|
|
187
|
-
window.removeEventListener("mousemove", boundHandlers.mouseMove);
|
|
188
|
-
window.removeEventListener("mouseup", boundHandlers.mouseUp);
|
|
189
|
-
currentContainer.removeEventListener("touchstart", boundHandlers.touchStart);
|
|
190
|
-
window.removeEventListener("touchmove", boundHandlers.touchMove);
|
|
191
|
-
window.removeEventListener("touchend", boundHandlers.touchEnd);
|
|
192
|
-
currentContainer.removeEventListener("wheel", boundHandlers.wheel);
|
|
307
|
+
window.removeEventListener("resize", debouncedRecalculate);
|
|
193
308
|
};
|
|
194
|
-
}, [
|
|
309
|
+
}, [debouncedRecalculate]);
|
|
195
310
|
useEffect(() => {
|
|
196
|
-
|
|
197
|
-
|
|
311
|
+
if (!containerRef.current || typeof ResizeObserver === "undefined") return;
|
|
312
|
+
const observer = new ResizeObserver(() => {
|
|
313
|
+
debouncedRecalculate();
|
|
314
|
+
});
|
|
315
|
+
observer.observe(containerRef.current);
|
|
198
316
|
return () => {
|
|
199
|
-
|
|
200
|
-
window.removeEventListener("resize", handleResize);
|
|
317
|
+
observer.disconnect();
|
|
201
318
|
};
|
|
202
|
-
}, [
|
|
319
|
+
}, [debouncedRecalculate]);
|
|
320
|
+
useEffect(() => {
|
|
321
|
+
return () => {
|
|
322
|
+
if (resizeDebounceTimerRef.current) {
|
|
323
|
+
clearTimeout(resizeDebounceTimerRef.current);
|
|
324
|
+
resizeDebounceTimerRef.current = null;
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
}, []);
|
|
203
328
|
useEffect(() => {
|
|
204
329
|
const handleVisibilityChange = () => {
|
|
205
330
|
setIsTabActive(!document.hidden);
|
|
@@ -212,6 +337,11 @@ function ReactSway({ children }) {
|
|
|
212
337
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
213
338
|
};
|
|
214
339
|
}, []);
|
|
340
|
+
useEffect(() => {
|
|
341
|
+
return () => {
|
|
342
|
+
clearInactivityTimer();
|
|
343
|
+
};
|
|
344
|
+
}, [clearInactivityTimer]);
|
|
215
345
|
useEffect(() => {
|
|
216
346
|
if (!isTabActive || isPaused) {
|
|
217
347
|
if (animationFrameRef.current) {
|
|
@@ -220,41 +350,29 @@ function ReactSway({ children }) {
|
|
|
220
350
|
}
|
|
221
351
|
return;
|
|
222
352
|
}
|
|
353
|
+
lastFrameTimeRef.current = 0;
|
|
354
|
+
const directionMultiplier = direction === "down" ? 1 : -1;
|
|
223
355
|
const animate = (currentTime) => {
|
|
224
|
-
let deltaTime = lastFrameTimeRef.current ? (currentTime - lastFrameTimeRef.current) /
|
|
356
|
+
let deltaTime = lastFrameTimeRef.current ? (currentTime - lastFrameTimeRef.current) / MS_PER_FRAME_60FPS : 1;
|
|
225
357
|
deltaTime = Math.min(deltaTime, MAX_DELTA_TIME);
|
|
226
358
|
lastFrameTimeRef.current = currentTime;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return newVelocity;
|
|
245
|
-
});
|
|
246
|
-
setPosition((prevPosition) => {
|
|
247
|
-
let newPosition = prevPosition;
|
|
248
|
-
if (loopPoint > 0) {
|
|
249
|
-
while (newPosition > 0) {
|
|
250
|
-
newPosition -= loopPoint;
|
|
251
|
-
}
|
|
252
|
-
while (newPosition < -loopPoint * 2) {
|
|
253
|
-
newPosition += loopPoint;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
return newPosition;
|
|
257
|
-
});
|
|
359
|
+
if (prefersReducedMotion) {
|
|
360
|
+
velocityRef.current = 0;
|
|
361
|
+
} else if (Math.abs(velocityRef.current) > 0.1) {
|
|
362
|
+
velocityRef.current *= Math.pow(normalizedFriction, deltaTime);
|
|
363
|
+
} else {
|
|
364
|
+
velocityRef.current = 0;
|
|
365
|
+
}
|
|
366
|
+
let nextPosition = positionRef.current;
|
|
367
|
+
const effectiveSpeed = prefersReducedMotion ? normalizedSpeed * REDUCED_MOTION_SPEED_FACTOR : normalizedSpeed;
|
|
368
|
+
if (autoScrollRef.current.desired && autoScrollRef.current.active && !isDraggingRef.current) {
|
|
369
|
+
nextPosition += directionMultiplier * effectiveSpeed * deltaTime;
|
|
370
|
+
}
|
|
371
|
+
if (!isDraggingRef.current && Math.abs(velocityRef.current) > 0.1) {
|
|
372
|
+
nextPosition += velocityRef.current * deltaTime;
|
|
373
|
+
}
|
|
374
|
+
nextPosition = wrapPosition(nextPosition);
|
|
375
|
+
commitPosition(nextPosition);
|
|
258
376
|
animationFrameRef.current = requestAnimationFrame(animate);
|
|
259
377
|
};
|
|
260
378
|
animationFrameRef.current = requestAnimationFrame(animate);
|
|
@@ -263,9 +381,9 @@ function ReactSway({ children }) {
|
|
|
263
381
|
cancelAnimationFrame(animationFrameRef.current);
|
|
264
382
|
}
|
|
265
383
|
};
|
|
266
|
-
}, [isTabActive,
|
|
384
|
+
}, [commitPosition, direction, isPaused, isTabActive, normalizedFriction, normalizedSpeed, prefersReducedMotion, wrapPosition]);
|
|
267
385
|
useEffect(() => {
|
|
268
|
-
if (!containerRef.current) return;
|
|
386
|
+
if (!lazy || !containerRef.current || typeof IntersectionObserver === "undefined") return;
|
|
269
387
|
const observer = new IntersectionObserver(
|
|
270
388
|
(entries) => {
|
|
271
389
|
entries.forEach((entry) => {
|
|
@@ -276,8 +394,8 @@ function ReactSway({ children }) {
|
|
|
276
394
|
},
|
|
277
395
|
{
|
|
278
396
|
root: null,
|
|
279
|
-
rootMargin:
|
|
280
|
-
threshold:
|
|
397
|
+
rootMargin: lazyRootMargin,
|
|
398
|
+
threshold: lazyThreshold
|
|
281
399
|
}
|
|
282
400
|
);
|
|
283
401
|
const items = containerRef.current.querySelectorAll(".content-item");
|
|
@@ -286,36 +404,34 @@ function ReactSway({ children }) {
|
|
|
286
404
|
items.forEach((item) => observer.unobserve(item));
|
|
287
405
|
observer.disconnect();
|
|
288
406
|
};
|
|
289
|
-
}, [children,
|
|
407
|
+
}, [children, lazy, lazyRootMargin, lazyThreshold]);
|
|
290
408
|
return /* @__PURE__ */ jsxs(
|
|
291
409
|
"div",
|
|
292
410
|
{
|
|
293
411
|
className: "react-sway-container scroller-content",
|
|
294
412
|
ref: containerRef,
|
|
295
413
|
style: {
|
|
296
|
-
|
|
297
|
-
|
|
414
|
+
cursor: draggable ? isDragging ? "grabbing" : "grab" : "default",
|
|
415
|
+
MozUserSelect: "none",
|
|
416
|
+
msUserSelect: "none",
|
|
417
|
+
overflow: "hidden",
|
|
418
|
+
overscrollBehavior: "contain",
|
|
419
|
+
pointerEvents: "auto",
|
|
298
420
|
position: "absolute",
|
|
299
|
-
width: "100%",
|
|
300
|
-
willChange: "transform",
|
|
301
|
-
WebkitTransform: "translateZ(0)",
|
|
302
421
|
touchAction: "none",
|
|
422
|
+
transform: "translate3d(0, 0px, 0)",
|
|
303
423
|
userSelect: "none",
|
|
304
424
|
WebkitUserSelect: "none",
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
overscrollBehavior: "contain",
|
|
308
|
-
overflow: "hidden",
|
|
309
|
-
// Prevent component's content from overflowing and causing page scroll
|
|
310
|
-
// Ensure it's on top and can receive events
|
|
311
|
-
pointerEvents: "auto",
|
|
425
|
+
width: "100%",
|
|
426
|
+
willChange: "transform",
|
|
312
427
|
zIndex: 1
|
|
313
428
|
},
|
|
314
|
-
|
|
429
|
+
onKeyDown: keyboard ? handleKeyDown : void 0,
|
|
430
|
+
tabIndex: keyboard ? 0 : void 0,
|
|
315
431
|
children: [
|
|
316
432
|
/* @__PURE__ */ jsx("div", { className: "content-group original", children }),
|
|
317
|
-
/* @__PURE__ */ jsx("aside", { className: "content-group duplicate", "
|
|
318
|
-
/* @__PURE__ */ jsx("aside", { className: "content-group duplicate", "
|
|
433
|
+
/* @__PURE__ */ jsx("aside", { "aria-hidden": "true", className: "content-group duplicate", "data-duplicate": "true", role: "presentation", children }),
|
|
434
|
+
/* @__PURE__ */ jsx("aside", { "aria-hidden": "true", className: "content-group duplicate", "data-duplicate": "true", role: "presentation", children })
|
|
319
435
|
]
|
|
320
436
|
}
|
|
321
437
|
);
|