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