react-sway 0.1.0 → 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 +109 -0
- package/dist/index.cjs +314 -209
- package/dist/index.d.cts +55 -2
- package/dist/index.d.ts +55 -2
- package/dist/index.js +315 -210
- package/package.json +18 -12
- package/src/ReactSway.tsx +445 -247
- 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,206 +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
|
-
}
|
|
37
|
-
setContainerHeight(window.innerHeight);
|
|
38
|
-
}
|
|
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);
|
|
39
81
|
};
|
|
40
|
-
|
|
41
|
-
calculateDimensions();
|
|
42
|
-
});
|
|
82
|
+
mediaQuery.addEventListener("change", handleChange);
|
|
43
83
|
return () => {
|
|
44
|
-
|
|
84
|
+
mediaQuery.removeEventListener("change", handleChange);
|
|
45
85
|
};
|
|
46
|
-
}, [
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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;
|
|
51
98
|
}
|
|
99
|
+
containerRef.current.style.transform = `translate3d(0, ${visualPosition}px, 0)`;
|
|
52
100
|
}, []);
|
|
53
|
-
const
|
|
54
|
-
if (
|
|
55
|
-
|
|
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;
|
|
56
113
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
114
|
+
while (wrappedPosition < -currentLoopPoint * 2) {
|
|
115
|
+
wrappedPosition += currentLoopPoint;
|
|
116
|
+
}
|
|
117
|
+
return wrappedPosition;
|
|
60
118
|
}, []);
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}, [isDragging]);
|
|
76
|
-
const handleMouseUp = useCallback((e) => {
|
|
77
|
-
if (!isDragging) return;
|
|
78
|
-
e.preventDefault();
|
|
79
|
-
setIsDragging(false);
|
|
80
|
-
scheduleAutoScrollResume();
|
|
81
|
-
}, [isDragging, scheduleAutoScrollResume]);
|
|
82
|
-
const handleTouchStart = useCallback((e) => {
|
|
83
|
-
if (e.touches.length === 1) {
|
|
84
|
-
setIsDragging(true);
|
|
85
|
-
lastTouchYRef.current = e.touches[0].clientY;
|
|
86
|
-
setVelocity(0);
|
|
87
|
-
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;
|
|
88
133
|
}
|
|
89
|
-
}, [
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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]);
|
|
110
161
|
const togglePause = useCallback(() => {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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?.();
|
|
117
174
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}, [pauseAutoScroll]);
|
|
175
|
+
}
|
|
176
|
+
}, [clearInactivityTimer]);
|
|
121
177
|
const handleKeyDown = useCallback((e) => {
|
|
178
|
+
if (!keyboard) return;
|
|
122
179
|
switch (e.key) {
|
|
123
180
|
case " ":
|
|
124
181
|
e.preventDefault();
|
|
125
182
|
togglePause();
|
|
126
183
|
break;
|
|
127
|
-
case "
|
|
184
|
+
case "ArrowDown":
|
|
128
185
|
e.preventDefault();
|
|
129
|
-
|
|
186
|
+
velocityRef.current -= ARROW_KEY_VELOCITY;
|
|
130
187
|
pauseAutoScroll();
|
|
131
188
|
scheduleAutoScrollResume();
|
|
132
189
|
break;
|
|
133
|
-
case "
|
|
190
|
+
case "ArrowUp":
|
|
134
191
|
e.preventDefault();
|
|
135
|
-
|
|
192
|
+
velocityRef.current += ARROW_KEY_VELOCITY;
|
|
136
193
|
pauseAutoScroll();
|
|
137
194
|
scheduleAutoScrollResume();
|
|
138
195
|
break;
|
|
139
|
-
case "
|
|
196
|
+
case "End":
|
|
140
197
|
e.preventDefault();
|
|
141
|
-
|
|
142
|
-
|
|
198
|
+
if (loopPointRef.current > 0) {
|
|
199
|
+
commitPosition(-loopPointRef.current);
|
|
200
|
+
}
|
|
201
|
+
velocityRef.current = 0;
|
|
143
202
|
pauseAutoScroll();
|
|
144
203
|
scheduleAutoScrollResume();
|
|
145
204
|
break;
|
|
146
|
-
case "
|
|
205
|
+
case "Home":
|
|
147
206
|
e.preventDefault();
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
setVelocity(0);
|
|
207
|
+
commitPosition(0);
|
|
208
|
+
velocityRef.current = 0;
|
|
152
209
|
pauseAutoScroll();
|
|
153
210
|
scheduleAutoScrollResume();
|
|
154
211
|
break;
|
|
155
212
|
default:
|
|
156
213
|
break;
|
|
157
214
|
}
|
|
158
|
-
}, [
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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]);
|
|
167
275
|
useEffect(() => {
|
|
168
276
|
const currentContainer = containerRef.current;
|
|
169
277
|
if (!currentContainer) return;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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);
|
|
178
293
|
};
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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);
|
|
186
306
|
return () => {
|
|
187
|
-
|
|
188
|
-
window.removeEventListener("mousemove", boundHandlers.mouseMove);
|
|
189
|
-
window.removeEventListener("mouseup", boundHandlers.mouseUp);
|
|
190
|
-
currentContainer.removeEventListener("touchstart", boundHandlers.touchStart);
|
|
191
|
-
window.removeEventListener("touchmove", boundHandlers.touchMove);
|
|
192
|
-
window.removeEventListener("touchend", boundHandlers.touchEnd);
|
|
193
|
-
currentContainer.removeEventListener("wheel", boundHandlers.wheel);
|
|
307
|
+
window.removeEventListener("resize", debouncedRecalculate);
|
|
194
308
|
};
|
|
195
|
-
}, [
|
|
309
|
+
}, [debouncedRecalculate]);
|
|
196
310
|
useEffect(() => {
|
|
197
|
-
|
|
198
|
-
|
|
311
|
+
if (!containerRef.current || typeof ResizeObserver === "undefined") return;
|
|
312
|
+
const observer = new ResizeObserver(() => {
|
|
313
|
+
debouncedRecalculate();
|
|
314
|
+
});
|
|
315
|
+
observer.observe(containerRef.current);
|
|
199
316
|
return () => {
|
|
200
|
-
|
|
201
|
-
window.removeEventListener("resize", handleResize);
|
|
317
|
+
observer.disconnect();
|
|
202
318
|
};
|
|
203
|
-
}, [
|
|
319
|
+
}, [debouncedRecalculate]);
|
|
320
|
+
useEffect(() => {
|
|
321
|
+
return () => {
|
|
322
|
+
if (resizeDebounceTimerRef.current) {
|
|
323
|
+
clearTimeout(resizeDebounceTimerRef.current);
|
|
324
|
+
resizeDebounceTimerRef.current = null;
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
}, []);
|
|
204
328
|
useEffect(() => {
|
|
205
329
|
const handleVisibilityChange = () => {
|
|
206
330
|
setIsTabActive(!document.hidden);
|
|
@@ -213,6 +337,11 @@ function ReactSway({ children }) {
|
|
|
213
337
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
214
338
|
};
|
|
215
339
|
}, []);
|
|
340
|
+
useEffect(() => {
|
|
341
|
+
return () => {
|
|
342
|
+
clearInactivityTimer();
|
|
343
|
+
};
|
|
344
|
+
}, [clearInactivityTimer]);
|
|
216
345
|
useEffect(() => {
|
|
217
346
|
if (!isTabActive || isPaused) {
|
|
218
347
|
if (animationFrameRef.current) {
|
|
@@ -221,41 +350,29 @@ function ReactSway({ children }) {
|
|
|
221
350
|
}
|
|
222
351
|
return;
|
|
223
352
|
}
|
|
353
|
+
lastFrameTimeRef.current = 0;
|
|
354
|
+
const directionMultiplier = direction === "down" ? 1 : -1;
|
|
224
355
|
const animate = (currentTime) => {
|
|
225
|
-
let deltaTime = lastFrameTimeRef.current ? (currentTime - lastFrameTimeRef.current) /
|
|
356
|
+
let deltaTime = lastFrameTimeRef.current ? (currentTime - lastFrameTimeRef.current) / MS_PER_FRAME_60FPS : 1;
|
|
226
357
|
deltaTime = Math.min(deltaTime, MAX_DELTA_TIME);
|
|
227
358
|
lastFrameTimeRef.current = currentTime;
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
return newVelocity;
|
|
246
|
-
});
|
|
247
|
-
setPosition((prevPosition) => {
|
|
248
|
-
let newPosition = prevPosition;
|
|
249
|
-
if (loopPoint > 0) {
|
|
250
|
-
while (newPosition > 0) {
|
|
251
|
-
newPosition -= loopPoint;
|
|
252
|
-
}
|
|
253
|
-
while (newPosition < -loopPoint * 2) {
|
|
254
|
-
newPosition += loopPoint;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
return newPosition;
|
|
258
|
-
});
|
|
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);
|
|
259
376
|
animationFrameRef.current = requestAnimationFrame(animate);
|
|
260
377
|
};
|
|
261
378
|
animationFrameRef.current = requestAnimationFrame(animate);
|
|
@@ -264,9 +381,9 @@ function ReactSway({ children }) {
|
|
|
264
381
|
cancelAnimationFrame(animationFrameRef.current);
|
|
265
382
|
}
|
|
266
383
|
};
|
|
267
|
-
}, [isTabActive,
|
|
384
|
+
}, [commitPosition, direction, isPaused, isTabActive, normalizedFriction, normalizedSpeed, prefersReducedMotion, wrapPosition]);
|
|
268
385
|
useEffect(() => {
|
|
269
|
-
if (!containerRef.current) return;
|
|
386
|
+
if (!lazy || !containerRef.current || typeof IntersectionObserver === "undefined") return;
|
|
270
387
|
const observer = new IntersectionObserver(
|
|
271
388
|
(entries) => {
|
|
272
389
|
entries.forEach((entry) => {
|
|
@@ -277,8 +394,8 @@ function ReactSway({ children }) {
|
|
|
277
394
|
},
|
|
278
395
|
{
|
|
279
396
|
root: null,
|
|
280
|
-
rootMargin:
|
|
281
|
-
threshold:
|
|
397
|
+
rootMargin: lazyRootMargin,
|
|
398
|
+
threshold: lazyThreshold
|
|
282
399
|
}
|
|
283
400
|
);
|
|
284
401
|
const items = containerRef.current.querySelectorAll(".content-item");
|
|
@@ -287,46 +404,34 @@ function ReactSway({ children }) {
|
|
|
287
404
|
items.forEach((item) => observer.unobserve(item));
|
|
288
405
|
observer.disconnect();
|
|
289
406
|
};
|
|
290
|
-
}, [children,
|
|
291
|
-
useEffect(() => {
|
|
292
|
-
const originalBodyStyle = {
|
|
293
|
-
touchAction: document.body.style.touchAction,
|
|
294
|
-
overflow: document.body.style.overflow
|
|
295
|
-
};
|
|
296
|
-
document.body.style.touchAction = "none";
|
|
297
|
-
document.body.style.overflow = "hidden";
|
|
298
|
-
return () => {
|
|
299
|
-
document.body.style.touchAction = originalBodyStyle.touchAction;
|
|
300
|
-
document.body.style.overflow = originalBodyStyle.overflow;
|
|
301
|
-
};
|
|
302
|
-
}, []);
|
|
407
|
+
}, [children, lazy, lazyRootMargin, lazyThreshold]);
|
|
303
408
|
return /* @__PURE__ */ jsxs(
|
|
304
409
|
"div",
|
|
305
410
|
{
|
|
306
411
|
className: "react-sway-container scroller-content",
|
|
307
412
|
ref: containerRef,
|
|
308
413
|
style: {
|
|
309
|
-
|
|
310
|
-
|
|
414
|
+
cursor: draggable ? isDragging ? "grabbing" : "grab" : "default",
|
|
415
|
+
MozUserSelect: "none",
|
|
416
|
+
msUserSelect: "none",
|
|
417
|
+
overflow: "hidden",
|
|
418
|
+
overscrollBehavior: "contain",
|
|
419
|
+
pointerEvents: "auto",
|
|
311
420
|
position: "absolute",
|
|
312
|
-
width: "100%",
|
|
313
|
-
willChange: "transform",
|
|
314
|
-
WebkitTransform: "translateZ(0)",
|
|
315
421
|
touchAction: "none",
|
|
422
|
+
transform: "translate3d(0, 0px, 0)",
|
|
316
423
|
userSelect: "none",
|
|
317
424
|
WebkitUserSelect: "none",
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
overscrollBehavior: "contain",
|
|
321
|
-
// Ensure it's on top and can receive events
|
|
322
|
-
pointerEvents: "auto",
|
|
425
|
+
width: "100%",
|
|
426
|
+
willChange: "transform",
|
|
323
427
|
zIndex: 1
|
|
324
428
|
},
|
|
325
|
-
|
|
429
|
+
onKeyDown: keyboard ? handleKeyDown : void 0,
|
|
430
|
+
tabIndex: keyboard ? 0 : void 0,
|
|
326
431
|
children: [
|
|
327
432
|
/* @__PURE__ */ jsx("div", { className: "content-group original", children }),
|
|
328
|
-
/* @__PURE__ */ jsx("aside", { className: "content-group duplicate", "
|
|
329
|
-
/* @__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 })
|
|
330
435
|
]
|
|
331
436
|
}
|
|
332
437
|
);
|