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