react-sway 0.4.2 → 0.5.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/CHANGELOG.md +17 -0
- package/README.md +48 -10
- package/dist/ReactSway.d.ts +14 -6
- package/dist/depth-position.d.ts +9 -0
- package/dist/index.cjs +215 -76
- package/dist/index.d.ts +1 -0
- package/dist/index.js +193 -78
- package/package.json +15 -13
package/dist/index.js
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
|
-
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import React, { Children, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, version } from "react";
|
|
2
|
+
// src/depth-position.ts
|
|
3
|
+
var DEFAULT_DEPTH_PERSPECTIVE = 800;
|
|
4
|
+
var DEFAULT_DEPTH_SPACING = 420;
|
|
5
|
+
/** Bounds pixel lengths so periods and fade denominators remain finite and nonzero. */
|
|
6
|
+
function normalizeDepthLength(value, fallback) {
|
|
7
|
+
return Number.isFinite(value) && value > 0 ? Math.min(Number.MAX_SAFE_INTEGER, Math.max(1, value)) : fallback;
|
|
8
|
+
}
|
|
9
|
+
function getDepthItemState(position, index, count, perspective = 800, spacing = 420) {
|
|
10
|
+
if (count <= 0) return {
|
|
11
|
+
opacity: 0,
|
|
12
|
+
z: 0
|
|
13
|
+
};
|
|
14
|
+
const period = count * spacing;
|
|
15
|
+
const phase = ((index * spacing + position) % period + period) % period;
|
|
16
|
+
const fade = Math.min(1, Math.min(phase, period - phase) / (spacing * .45));
|
|
17
|
+
return {
|
|
18
|
+
opacity: fade * fade * (3 - 2 * fade),
|
|
19
|
+
z: perspective * .7 - phase
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
// src/edge-hover-velocity.ts
|
|
7
23
|
/** Default edge-hover activation thickness in pixels. */
|
|
8
24
|
var DEFAULT_EDGE_HOVER_SIZE = 96;
|
|
9
25
|
/** Default maximum edge-hover speed relative to the configured base speed. */
|
|
@@ -49,10 +65,10 @@ function getEdgeHoverVelocityScale(intensity, speedMultiplier = 6) {
|
|
|
49
65
|
if (depth < EDGE_HOVER_ENTRY_FEATHER_INTENSITY) return direction * smoothUnitStep(depth / EDGE_HOVER_ENTRY_FEATHER_INTENSITY);
|
|
50
66
|
const physicalEdgeScale = Math.min(EDGE_HOVER_PHYSICAL_EDGE_SPEED_MULTIPLIER, maximumScale);
|
|
51
67
|
if (depth <= EDGE_HOVER_PHYSICAL_EDGE_INTENSITY) {
|
|
52
|
-
const physicalEdgeProgress = (depth - EDGE_HOVER_ENTRY_FEATHER_INTENSITY) /
|
|
68
|
+
const physicalEdgeProgress = (depth - EDGE_HOVER_ENTRY_FEATHER_INTENSITY) / .42;
|
|
53
69
|
return direction * (1 + (physicalEdgeScale - 1) * smoothUnitStep(physicalEdgeProgress));
|
|
54
70
|
}
|
|
55
|
-
const beyondEdgeProgress = (depth - EDGE_HOVER_PHYSICAL_EDGE_INTENSITY) /
|
|
71
|
+
const beyondEdgeProgress = (depth - EDGE_HOVER_PHYSICAL_EDGE_INTENSITY) / .5;
|
|
56
72
|
return direction * (physicalEdgeScale + (maximumScale - physicalEdgeScale) * smoothUnitStep(beyondEdgeProgress));
|
|
57
73
|
}
|
|
58
74
|
/**
|
|
@@ -78,9 +94,7 @@ function getEdgeHoverIntensity(pointerPosition, visibleInterval, edgeHoverSize)
|
|
|
78
94
|
}
|
|
79
95
|
return 0;
|
|
80
96
|
}
|
|
81
|
-
|
|
82
|
-
//#region src/wheel-input.ts
|
|
83
|
-
/** Fallback pixel height for wheel events reported in line units. */
|
|
97
|
+
// src/wheel-input.ts
|
|
84
98
|
var WHEEL_LINE_HEIGHT_FALLBACK_PX = 16;
|
|
85
99
|
/** WheelEvent deltaMode value for line-based deltas. */
|
|
86
100
|
var WHEEL_MODE_LINE = 1;
|
|
@@ -114,8 +128,10 @@ function normalizeWheelDelta(event, container, axis) {
|
|
|
114
128
|
* the gesture. This partial function is the wheel-ownership boundary.
|
|
115
129
|
*/
|
|
116
130
|
function getOwnedWheelDelta(event, container, isHorizontal, wheelMode) {
|
|
131
|
+
if (event.ctrlKey) return null;
|
|
117
132
|
const deltaX = normalizeWheelDelta(event, container, "x");
|
|
118
133
|
const deltaY = normalizeWheelDelta(event, container, "y");
|
|
134
|
+
if (!Number.isFinite(deltaX) || !Number.isFinite(deltaY) || deltaX === 0 && deltaY === 0) return null;
|
|
119
135
|
const absDeltaX = Math.abs(deltaX);
|
|
120
136
|
const absDeltaY = Math.abs(deltaY);
|
|
121
137
|
if (isHorizontal) {
|
|
@@ -125,11 +141,7 @@ function getOwnedWheelDelta(event, container, isHorizontal, wheelMode) {
|
|
|
125
141
|
if (wheelMode === "axis" && absDeltaX > absDeltaY) return null;
|
|
126
142
|
return deltaY;
|
|
127
143
|
}
|
|
128
|
-
|
|
129
|
-
//#region src/ReactSway.tsx
|
|
130
|
-
/**
|
|
131
|
-
* Core ReactSway component implementing axis-aware infinite scrolling interactions.
|
|
132
|
-
*/
|
|
144
|
+
// src/ReactSway.tsx
|
|
133
145
|
/** Velocity applied per arrow key press (pixels). */
|
|
134
146
|
var ARROW_KEY_VELOCITY = 15;
|
|
135
147
|
/** Default friction coefficient applied to velocity each frame. */
|
|
@@ -160,6 +172,10 @@ var WHEEL_VELOCITY_MULTIPLIER = .14;
|
|
|
160
172
|
var WHEEL_IMMEDIATE_DELTA_FACTOR = .14;
|
|
161
173
|
/** Minimum touch movement before a gesture is classified by axis. */
|
|
162
174
|
var TOUCH_AXIS_LOCK_THRESHOLD_PX = 8;
|
|
175
|
+
var INERT_ATTRIBUTE = { inert: Number.parseInt(version, 10) >= 19 ? true : "" };
|
|
176
|
+
function isInteractiveTarget(target) {
|
|
177
|
+
return target instanceof Element && Boolean(target.closest("a, button, input, select, textarea, [contenteditable]:not([contenteditable=\"false\"])"));
|
|
178
|
+
}
|
|
163
179
|
function getVisibleEdgeInterval(rect, isHorizontal) {
|
|
164
180
|
const rectEnd = isHorizontal ? rect.right : rect.bottom;
|
|
165
181
|
const rectStart = isHorizontal ? rect.left : rect.top;
|
|
@@ -179,11 +195,11 @@ function getVisibleEdgeInterval(rect, isHorizontal) {
|
|
|
179
195
|
* Renders children in a continuously looping scroll area with support for
|
|
180
196
|
* auto-scrolling, mouse drag, touch swipe, wheel, keyboard interactions,
|
|
181
197
|
* and optional edge-hover scrolling.
|
|
182
|
-
*
|
|
198
|
+
* Planar content is duplicated to create a seamless loop effect. Duplicate content
|
|
183
199
|
* is wrapped in `<aside>` elements with `aria-hidden="true"` for accessibility.
|
|
184
200
|
*
|
|
185
|
-
*
|
|
186
|
-
* and
|
|
201
|
+
* Reduced motion stops depth auto-scroll, lowers planar auto-scroll speed,
|
|
202
|
+
* and disables momentum effects.
|
|
187
203
|
*
|
|
188
204
|
* @example
|
|
189
205
|
* ```tsx
|
|
@@ -193,8 +209,14 @@ function getVisibleEdgeInterval(rect, isHorizontal) {
|
|
|
193
209
|
* </ReactSway>
|
|
194
210
|
* ```
|
|
195
211
|
*/
|
|
196
|
-
var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content", axis, autoScroll = true, children, className, direction = "up", draggable = true, edgeHoverInputMode = "hybrid", edgeHoverScroll = false, edgeHoverSize = 96, edgeHoverSpeedMultiplier = 6, friction = DEFAULT_FRICTION, keyboard = true, lazy = true, lazyRootMargin = DEFAULT_LAZY_ROOT_MARGIN, lazyThreshold = DEFAULT_LAZY_THRESHOLD, onPause, onResume, onScroll, pauseOnInteraction = true, resumeDelay = DEFAULT_RESUME_DELAY, speed = DEFAULT_SPEED, wheelEnabled = true, wheelMode = "axis" }, ref) {
|
|
197
|
-
const
|
|
212
|
+
var ReactSway = forwardRef(function ReactSway({ active = true, ariaLabel = "Scrollable content", axis, autoScroll = true, children, className, depthPerspective = 800, depthSpacing = 420, direction = "up", draggable = true, edgeHoverInputMode = "hybrid", edgeHoverScroll = false, edgeHoverSize = 96, edgeHoverSpeedMultiplier = 6, friction = DEFAULT_FRICTION, keyboard = true, lazy = true, lazyRootMargin = DEFAULT_LAZY_ROOT_MARGIN, lazyThreshold = DEFAULT_LAZY_THRESHOLD, onPause, onResume, onScroll, pauseOnInteraction = true, reducedMotion = false, resumeDelay = DEFAULT_RESUME_DELAY, speed = DEFAULT_SPEED, wheelEnabled = true, wheelMode = "axis" }, ref) {
|
|
213
|
+
const scrollAxis = useMemo(() => axis ?? (direction === "toward" || direction === "away" ? "depth" : direction === "left" || direction === "right" ? "horizontal" : "vertical"), [axis, direction]);
|
|
214
|
+
const isHorizontal = scrollAxis === "horizontal";
|
|
215
|
+
const isDepth = scrollAxis === "depth";
|
|
216
|
+
const depthChildren = useMemo(() => Children.toArray(children), [children]);
|
|
217
|
+
const enabled = active && (!isDepth || depthChildren.length > 0);
|
|
218
|
+
const normalizedDepthPerspective = normalizeDepthLength(depthPerspective, 800);
|
|
219
|
+
const normalizedDepthSpacing = normalizeDepthLength(depthSpacing, 420);
|
|
198
220
|
const normalizedEdgeHoverSize = useMemo(() => Number.isFinite(edgeHoverSize) ? Math.max(1, edgeHoverSize) : 96, [edgeHoverSize]);
|
|
199
221
|
const normalizedEdgeHoverSpeedMultiplier = useMemo(() => normalizeEdgeHoverSpeedMultiplier(edgeHoverSpeedMultiplier), [edgeHoverSpeedMultiplier]);
|
|
200
222
|
const normalizedFriction = useMemo(() => Number.isFinite(friction) ? Math.min(Math.max(friction, 0), 1) : DEFAULT_FRICTION, [friction]);
|
|
@@ -202,17 +224,22 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
202
224
|
const normalizedResumeDelay = useMemo(() => Number.isFinite(resumeDelay) ? Math.max(0, resumeDelay) : DEFAULT_RESUME_DELAY, [resumeDelay]);
|
|
203
225
|
const normalizedSpeed = useMemo(() => Number.isFinite(speed) ? Math.max(0, speed) : DEFAULT_SPEED, [speed]);
|
|
204
226
|
const touchAction = useMemo(() => {
|
|
205
|
-
if (!draggable) return "auto";
|
|
227
|
+
if (!enabled || !draggable) return "auto";
|
|
206
228
|
return isHorizontal ? "pan-y" : "pan-x";
|
|
207
|
-
}, [
|
|
208
|
-
|
|
229
|
+
}, [
|
|
230
|
+
draggable,
|
|
231
|
+
enabled,
|
|
232
|
+
isHorizontal
|
|
233
|
+
]);
|
|
234
|
+
const overscrollBehavior = enabled && wheelEnabled && wheelMode === "capture" ? "contain" : "auto";
|
|
209
235
|
const [isPaused, setIsPaused] = useState(false);
|
|
210
236
|
const [isTabActive, setIsTabActive] = useState(() => typeof document === "undefined" || !document.hidden);
|
|
211
237
|
const [loopSegmentCount, setLoopSegmentCount] = useState(MIN_LOOP_SEGMENTS);
|
|
212
|
-
const [
|
|
238
|
+
const [systemReducedMotion, setSystemReducedMotion] = useState(() => {
|
|
213
239
|
if (typeof window === "undefined") return false;
|
|
214
240
|
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
215
241
|
});
|
|
242
|
+
const prefersReducedMotion = systemReducedMotion || reducedMotion;
|
|
216
243
|
const animationFrameRef = useRef(null);
|
|
217
244
|
const animationLoopRef = useRef(null);
|
|
218
245
|
const autoScrollRef = useRef({
|
|
@@ -259,7 +286,7 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
259
286
|
if (typeof window === "undefined") return;
|
|
260
287
|
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
261
288
|
const handleChange = (e) => {
|
|
262
|
-
|
|
289
|
+
setSystemReducedMotion(e.matches);
|
|
263
290
|
};
|
|
264
291
|
mediaQuery.addEventListener("change", handleChange);
|
|
265
292
|
return () => {
|
|
@@ -299,11 +326,28 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
299
326
|
}, [isHorizontal]);
|
|
300
327
|
const renderPosition = useCallback((rawPosition) => {
|
|
301
328
|
if (!containerRef.current) return;
|
|
329
|
+
if (isDepth) {
|
|
330
|
+
containerRef.current.style.transform = "translate3d(0, 0, 0)";
|
|
331
|
+
const items = originalGroupRef.current?.children;
|
|
332
|
+
if (!items) return;
|
|
333
|
+
Array.from(items).forEach((item, index) => {
|
|
334
|
+
const { opacity, z } = getDepthItemState(rawPosition, index, items.length, normalizedDepthPerspective, normalizedDepthSpacing);
|
|
335
|
+
const element = item;
|
|
336
|
+
element.style.transform = `translate3d(0, 0, ${z}px)`;
|
|
337
|
+
element.style.opacity = String(opacity);
|
|
338
|
+
});
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
302
341
|
const currentLoopPoint = loopPointRef.current;
|
|
303
342
|
let visualPosition = rawPosition % (currentLoopPoint || 1);
|
|
304
343
|
if (visualPosition > 0 && currentLoopPoint > 0) visualPosition -= currentLoopPoint;
|
|
305
344
|
containerRef.current.style.transform = isHorizontal ? `translate3d(${visualPosition}px, 0, 0)` : `translate3d(0, ${visualPosition}px, 0)`;
|
|
306
|
-
}, [
|
|
345
|
+
}, [
|
|
346
|
+
isDepth,
|
|
347
|
+
isHorizontal,
|
|
348
|
+
normalizedDepthPerspective,
|
|
349
|
+
normalizedDepthSpacing
|
|
350
|
+
]);
|
|
307
351
|
const commitPosition = useCallback((nextPosition) => {
|
|
308
352
|
if (positionRef.current === nextPosition) return;
|
|
309
353
|
positionRef.current = nextPosition;
|
|
@@ -324,11 +368,14 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
324
368
|
}, [draggable]);
|
|
325
369
|
const recalculateLoopPoint = useCallback(() => {
|
|
326
370
|
if (!containerRef.current || !originalGroupRef.current) return;
|
|
327
|
-
const originalContentSize = isHorizontal ? originalGroupRef.current.scrollWidth : originalGroupRef.current.scrollHeight;
|
|
328
|
-
if (originalContentSize <= 0)
|
|
371
|
+
const originalContentSize = isDepth ? depthChildren.length * normalizedDepthSpacing : isHorizontal ? originalGroupRef.current.scrollWidth : originalGroupRef.current.scrollHeight;
|
|
372
|
+
if (originalContentSize <= 0) {
|
|
373
|
+
if (isDepth) loopPointRef.current = 0;
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
329
376
|
const viewport = containerRef.current.parentElement ?? containerRef.current;
|
|
330
377
|
const viewportSize = isHorizontal ? Math.max(viewport.clientWidth, 1) : Math.max(viewport.clientHeight, 1);
|
|
331
|
-
const nextLoopSegmentCount = Math.max(MIN_LOOP_SEGMENTS, Math.ceil(viewportSize / originalContentSize) + 1);
|
|
378
|
+
const nextLoopSegmentCount = isDepth ? 1 : Math.max(MIN_LOOP_SEGMENTS, Math.ceil(viewportSize / originalContentSize) + 1);
|
|
332
379
|
const nextLoopPoint = originalContentSize;
|
|
333
380
|
const previousMeasurement = lastLoopMeasurementRef.current;
|
|
334
381
|
const nextMeasurement = {
|
|
@@ -341,7 +388,13 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
341
388
|
setLoopSegmentCount((previousLoopSegmentCount) => previousLoopSegmentCount === nextLoopSegmentCount ? previousLoopSegmentCount : nextLoopSegmentCount);
|
|
342
389
|
loopPointRef.current = nextLoopPoint;
|
|
343
390
|
renderPosition(positionRef.current);
|
|
344
|
-
}, [
|
|
391
|
+
}, [
|
|
392
|
+
depthChildren.length,
|
|
393
|
+
isDepth,
|
|
394
|
+
isHorizontal,
|
|
395
|
+
normalizedDepthSpacing,
|
|
396
|
+
renderPosition
|
|
397
|
+
]);
|
|
345
398
|
useEffect(() => {
|
|
346
399
|
autoScrollRef.current.desired = autoScroll;
|
|
347
400
|
if (!autoScroll) {
|
|
@@ -366,6 +419,7 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
366
419
|
scheduleLoopPointRecalculation();
|
|
367
420
|
}, [
|
|
368
421
|
children,
|
|
422
|
+
isDepth,
|
|
369
423
|
isHorizontal,
|
|
370
424
|
scheduleLoopPointRecalculation
|
|
371
425
|
]);
|
|
@@ -420,7 +474,14 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
420
474
|
touchDragListenerCleanupRef.current = null;
|
|
421
475
|
}, []);
|
|
422
476
|
const handleKeyDown = useCallback((e) => {
|
|
423
|
-
if (!keyboard) return;
|
|
477
|
+
if (!enabled || !keyboard || isInteractiveTarget(e.target)) return;
|
|
478
|
+
const arrowDelta = isHorizontal ? e.key === "ArrowLeft" ? ARROW_KEY_VELOCITY : e.key === "ArrowRight" ? -15 : 0 : e.key === "ArrowUp" ? ARROW_KEY_VELOCITY : e.key === "ArrowDown" ? -15 : 0;
|
|
479
|
+
if (prefersReducedMotion && arrowDelta !== 0) {
|
|
480
|
+
e.preventDefault();
|
|
481
|
+
commitPosition(wrapPosition(positionRef.current + arrowDelta));
|
|
482
|
+
velocityRef.current = 0;
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
424
485
|
switch (e.key) {
|
|
425
486
|
case " ":
|
|
426
487
|
e.preventDefault();
|
|
@@ -467,18 +528,19 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
467
528
|
velocityRef.current = 0;
|
|
468
529
|
pauseAutoScroll();
|
|
469
530
|
scheduleAutoScrollResume();
|
|
470
|
-
break;
|
|
471
|
-
default: break;
|
|
472
531
|
}
|
|
473
532
|
startAnimationLoop();
|
|
474
533
|
}, [
|
|
475
534
|
commitPosition,
|
|
535
|
+
enabled,
|
|
476
536
|
isHorizontal,
|
|
477
537
|
keyboard,
|
|
478
538
|
pauseAutoScroll,
|
|
539
|
+
prefersReducedMotion,
|
|
479
540
|
scheduleAutoScrollResume,
|
|
480
541
|
startAnimationLoop,
|
|
481
|
-
togglePause
|
|
542
|
+
togglePause,
|
|
543
|
+
wrapPosition
|
|
482
544
|
]);
|
|
483
545
|
const handleMouseMove = useCallback((e) => {
|
|
484
546
|
if (!isDraggingRef.current) return;
|
|
@@ -536,7 +598,7 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
536
598
|
handleMouseUp
|
|
537
599
|
]);
|
|
538
600
|
const handleMouseDown = useCallback((e) => {
|
|
539
|
-
if (!draggable) return;
|
|
601
|
+
if (!enabled || !draggable || e.button !== 0 || isInteractiveTarget(e.target)) return;
|
|
540
602
|
e.preventDefault();
|
|
541
603
|
containerRef.current?.focus();
|
|
542
604
|
setDraggingState(true);
|
|
@@ -549,6 +611,7 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
549
611
|
}, [
|
|
550
612
|
attachMouseDragListeners,
|
|
551
613
|
draggable,
|
|
614
|
+
enabled,
|
|
552
615
|
pauseAutoScroll,
|
|
553
616
|
setDraggingState,
|
|
554
617
|
startAnimationLoop
|
|
@@ -572,6 +635,7 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
572
635
|
if (touchInteractionStateRef.current === "idle" || touchInteractionStateRef.current === "ignored") return;
|
|
573
636
|
if (e.touches.length !== 1) return;
|
|
574
637
|
const touch = e.touches[0];
|
|
638
|
+
if (!touch) return;
|
|
575
639
|
if (touchInteractionStateRef.current === "pending") {
|
|
576
640
|
const totalDeltaX = touch.clientX - touchStartXRef.current;
|
|
577
641
|
const totalDeltaY = touch.clientY - touchStartYRef.current;
|
|
@@ -616,18 +680,24 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
616
680
|
handleTouchMove
|
|
617
681
|
]);
|
|
618
682
|
const handleTouchStart = useCallback((e) => {
|
|
619
|
-
if (!draggable || e.touches.length !== 1) return;
|
|
683
|
+
if (!enabled || !draggable || e.touches.length !== 1 || isInteractiveTarget(e.target)) return;
|
|
684
|
+
const touch = e.touches[0];
|
|
685
|
+
if (!touch) return;
|
|
620
686
|
containerRef.current?.focus();
|
|
621
687
|
touchInteractionStateRef.current = "pending";
|
|
622
|
-
lastTouchXRef.current =
|
|
623
|
-
lastTouchYRef.current =
|
|
624
|
-
touchStartXRef.current =
|
|
625
|
-
touchStartYRef.current =
|
|
688
|
+
lastTouchXRef.current = touch.clientX;
|
|
689
|
+
lastTouchYRef.current = touch.clientY;
|
|
690
|
+
touchStartXRef.current = touch.clientX;
|
|
691
|
+
touchStartYRef.current = touch.clientY;
|
|
626
692
|
velocityRef.current = 0;
|
|
627
693
|
attachTouchDragListeners();
|
|
628
|
-
}, [
|
|
694
|
+
}, [
|
|
695
|
+
attachTouchDragListeners,
|
|
696
|
+
draggable,
|
|
697
|
+
enabled
|
|
698
|
+
]);
|
|
629
699
|
const handleWheel = useCallback((e) => {
|
|
630
|
-
if (!wheelEnabled) return false;
|
|
700
|
+
if (!enabled || !wheelEnabled || e.defaultPrevented) return false;
|
|
631
701
|
const currentContainer = e.currentTarget instanceof HTMLElement ? e.currentTarget : containerRef.current;
|
|
632
702
|
if (!currentContainer) return false;
|
|
633
703
|
const ownedDelta = getOwnedWheelDelta(e, currentContainer, isHorizontal, wheelMode);
|
|
@@ -644,6 +714,7 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
644
714
|
return true;
|
|
645
715
|
}, [
|
|
646
716
|
commitPosition,
|
|
717
|
+
enabled,
|
|
647
718
|
isHorizontal,
|
|
648
719
|
pauseAutoScroll,
|
|
649
720
|
scheduleAutoScrollResume,
|
|
@@ -653,7 +724,7 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
653
724
|
wrapPosition
|
|
654
725
|
]);
|
|
655
726
|
const applyEdgeHoverPoint = useCallback((point) => {
|
|
656
|
-
if (!edgeHoverScroll) return false;
|
|
727
|
+
if (!enabled || !edgeHoverScroll) return false;
|
|
657
728
|
const viewport = containerRef.current?.parentElement ?? containerRef.current;
|
|
658
729
|
if (!viewport) return false;
|
|
659
730
|
const visibleInterval = readVisibleEdgeInterval(viewport);
|
|
@@ -666,6 +737,7 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
666
737
|
return true;
|
|
667
738
|
}, [
|
|
668
739
|
edgeHoverScroll,
|
|
740
|
+
enabled,
|
|
669
741
|
isHorizontal,
|
|
670
742
|
normalizedEdgeHoverSize,
|
|
671
743
|
readVisibleEdgeInterval,
|
|
@@ -680,6 +752,9 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
680
752
|
externalEdgeHoverLeaseRef.current = false;
|
|
681
753
|
setEdgeHoverIntensity(0);
|
|
682
754
|
}, [setEdgeHoverIntensity]);
|
|
755
|
+
useEffect(() => {
|
|
756
|
+
if (!edgeHoverScroll) clearEdgeHover();
|
|
757
|
+
}, [clearEdgeHover, edgeHoverScroll]);
|
|
683
758
|
useImperativeHandle(ref, () => ({
|
|
684
759
|
clearEdgeHover,
|
|
685
760
|
handleEdgeHover,
|
|
@@ -699,7 +774,7 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
699
774
|
}, [setEdgeHoverIntensity]);
|
|
700
775
|
useEffect(() => {
|
|
701
776
|
const currentContainer = containerRef.current;
|
|
702
|
-
if (!currentContainer) return;
|
|
777
|
+
if (!currentContainer || !enabled) return;
|
|
703
778
|
if (draggable) {
|
|
704
779
|
currentContainer.addEventListener("mousedown", handleMouseDown);
|
|
705
780
|
currentContainer.addEventListener("touchstart", handleTouchStart, { passive: true });
|
|
@@ -737,6 +812,7 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
737
812
|
detachMouseDragListeners,
|
|
738
813
|
detachTouchDragListeners,
|
|
739
814
|
draggable,
|
|
815
|
+
enabled,
|
|
740
816
|
edgeHoverScroll,
|
|
741
817
|
edgeHoverInputMode,
|
|
742
818
|
clearEdgeHover,
|
|
@@ -747,6 +823,24 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
747
823
|
handleWheel,
|
|
748
824
|
wheelEnabled
|
|
749
825
|
]);
|
|
826
|
+
useEffect(() => {
|
|
827
|
+
if (enabled) return;
|
|
828
|
+
clearInactivityTimer();
|
|
829
|
+
clearEdgeHover();
|
|
830
|
+
detachMouseDragListeners();
|
|
831
|
+
detachTouchDragListeners();
|
|
832
|
+
touchInteractionStateRef.current = "idle";
|
|
833
|
+
setDraggingState(false);
|
|
834
|
+
velocityRef.current = 0;
|
|
835
|
+
autoScrollRef.current.active = autoScrollRef.current.desired;
|
|
836
|
+
}, [
|
|
837
|
+
clearEdgeHover,
|
|
838
|
+
clearInactivityTimer,
|
|
839
|
+
detachMouseDragListeners,
|
|
840
|
+
detachTouchDragListeners,
|
|
841
|
+
enabled,
|
|
842
|
+
setDraggingState
|
|
843
|
+
]);
|
|
750
844
|
useEffect(() => {
|
|
751
845
|
window.addEventListener("resize", scheduleLoopPointRecalculation);
|
|
752
846
|
return () => {
|
|
@@ -795,7 +889,7 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
795
889
|
};
|
|
796
890
|
}, [clearInactivityTimer]);
|
|
797
891
|
useEffect(() => {
|
|
798
|
-
if (!isTabActive || isPaused) {
|
|
892
|
+
if (!enabled || !isTabActive || isPaused) {
|
|
799
893
|
if (animationFrameRef.current !== null) {
|
|
800
894
|
cancelAnimationFrame(animationFrameRef.current);
|
|
801
895
|
animationFrameRef.current = null;
|
|
@@ -804,7 +898,7 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
804
898
|
return;
|
|
805
899
|
}
|
|
806
900
|
lastFrameTimeRef.current = 0;
|
|
807
|
-
const directionMultiplier = direction === "down" || direction === "right" ? 1 : -1;
|
|
901
|
+
const directionMultiplier = direction === "away" || direction === "down" || direction === "right" ? 1 : -1;
|
|
808
902
|
const animate = (currentTime) => {
|
|
809
903
|
let deltaTime = lastFrameTimeRef.current ? (currentTime - lastFrameTimeRef.current) / MS_PER_FRAME_60FPS : 1;
|
|
810
904
|
deltaTime = Math.min(deltaTime, MAX_DELTA_TIME);
|
|
@@ -815,7 +909,7 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
815
909
|
let nextPosition = positionRef.current;
|
|
816
910
|
const effectiveSpeed = prefersReducedMotion ? normalizedSpeed * REDUCED_MOTION_SPEED_FACTOR : normalizedSpeed;
|
|
817
911
|
const edgeHoverMultiplier = edgeHoverScroll ? getEdgeHoverVelocityScale(edgeHoverIntensityRef.current, normalizedEdgeHoverSpeedMultiplier) : directionMultiplier;
|
|
818
|
-
const shouldAutoScroll = (autoScrollRef.current.desired && autoScrollRef.current.active || externalEdgeHoverLeaseRef.current) && !isDraggingRef.current && (!edgeHoverScroll || edgeHoverMultiplier !== 0);
|
|
912
|
+
const shouldAutoScroll = (autoScrollRef.current.desired && autoScrollRef.current.active || externalEdgeHoverLeaseRef.current) && !isDraggingRef.current && !(isDepth && prefersReducedMotion) && (!edgeHoverScroll || edgeHoverMultiplier !== 0);
|
|
819
913
|
if (shouldAutoScroll) {
|
|
820
914
|
const autoScrollDeltaTime = edgeHoverScroll ? Math.min(deltaTime, MAX_EDGE_HOVER_DELTA_TIME) : deltaTime;
|
|
821
915
|
nextPosition += edgeHoverMultiplier * effectiveSpeed * autoScrollDeltaTime;
|
|
@@ -844,6 +938,8 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
844
938
|
commitPosition,
|
|
845
939
|
direction,
|
|
846
940
|
edgeHoverScroll,
|
|
941
|
+
enabled,
|
|
942
|
+
isDepth,
|
|
847
943
|
isPaused,
|
|
848
944
|
isTabActive,
|
|
849
945
|
normalizedFriction,
|
|
@@ -876,54 +972,73 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
876
972
|
lazyRootMargin,
|
|
877
973
|
normalizedLazyThreshold
|
|
878
974
|
]);
|
|
879
|
-
return /* @__PURE__ */
|
|
975
|
+
return /* @__PURE__ */ React.createElement("section", {
|
|
880
976
|
"aria-label": ariaLabel,
|
|
881
977
|
className: `react-sway-container scroller-content${className ? ` ${className}` : ""}`,
|
|
882
978
|
ref: containerRef,
|
|
883
979
|
style: {
|
|
884
|
-
cursor: draggable ? isDraggingRef.current ? "grabbing" : "grab" : "default",
|
|
980
|
+
cursor: enabled && draggable ? isDraggingRef.current ? "grabbing" : "grab" : "default",
|
|
885
981
|
display: isHorizontal ? "flex" : void 0,
|
|
886
982
|
flexDirection: isHorizontal ? "row" : void 0,
|
|
887
|
-
height: isHorizontal ? "100%" : "max-content",
|
|
983
|
+
height: isHorizontal || isDepth ? "100%" : "max-content",
|
|
888
984
|
MozUserSelect: "none",
|
|
889
985
|
msUserSelect: "none",
|
|
890
986
|
minHeight: isHorizontal ? void 0 : "100%",
|
|
891
|
-
overflow: "hidden",
|
|
987
|
+
overflow: isDepth ? "visible" : "hidden",
|
|
988
|
+
perspective: isDepth ? normalizedDepthPerspective : void 0,
|
|
892
989
|
overscrollBehavior,
|
|
893
990
|
pointerEvents: "auto",
|
|
894
991
|
position: "absolute",
|
|
895
992
|
touchAction,
|
|
896
993
|
transform: "translate3d(0, 0, 0)",
|
|
994
|
+
transformStyle: isDepth ? "preserve-3d" : void 0,
|
|
897
995
|
userSelect: "none",
|
|
898
996
|
WebkitUserSelect: "none",
|
|
899
997
|
width: isHorizontal ? "max-content" : "100%",
|
|
900
|
-
willChange: "transform",
|
|
998
|
+
willChange: isDepth ? void 0 : "transform",
|
|
901
999
|
zIndex: 1
|
|
902
1000
|
},
|
|
903
1001
|
onKeyDown: keyboard ? handleKeyDown : void 0,
|
|
904
|
-
tabIndex: keyboard ? 0 : void 0
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1002
|
+
tabIndex: enabled && keyboard ? 0 : void 0
|
|
1003
|
+
}, /* @__PURE__ */ React.createElement("div", {
|
|
1004
|
+
className: "content-group original",
|
|
1005
|
+
ref: originalGroupRef,
|
|
1006
|
+
style: isDepth ? {
|
|
1007
|
+
height: "100%",
|
|
1008
|
+
position: "relative",
|
|
1009
|
+
transformStyle: "preserve-3d"
|
|
1010
|
+
} : isHorizontal ? {
|
|
1011
|
+
display: "flex",
|
|
1012
|
+
flex: "0 0 auto"
|
|
1013
|
+
} : void 0
|
|
1014
|
+
}, isDepth ? depthChildren.map((child, index) => {
|
|
1015
|
+
const { opacity, z } = getDepthItemState(0, index, depthChildren.length, normalizedDepthPerspective, normalizedDepthSpacing);
|
|
1016
|
+
return /* @__PURE__ */ React.createElement("div", {
|
|
1017
|
+
className: "react-sway-depth-item",
|
|
1018
|
+
key: typeof child === "object" && "key" in child ? child.key : index,
|
|
1019
|
+
style: {
|
|
1020
|
+
height: 0,
|
|
1021
|
+
left: "50%",
|
|
1022
|
+
opacity,
|
|
1023
|
+
position: "absolute",
|
|
1024
|
+
top: "50%",
|
|
1025
|
+
transform: `translate3d(0, 0, ${z}px)`,
|
|
1026
|
+
width: 0,
|
|
1027
|
+
willChange: "transform, opacity"
|
|
1028
|
+
}
|
|
1029
|
+
}, child);
|
|
1030
|
+
}) : children), Array.from({ length: isDepth ? 0 : loopSegmentCount - 1 }, (_, duplicateIndex) => /* @__PURE__ */ React.createElement("aside", {
|
|
1031
|
+
"aria-hidden": "true",
|
|
1032
|
+
className: "content-group duplicate",
|
|
1033
|
+
"data-duplicate": "true",
|
|
1034
|
+
...INERT_ATTRIBUTE,
|
|
1035
|
+
key: `duplicate-${duplicateIndex}`,
|
|
1036
|
+
role: "presentation",
|
|
1037
|
+
style: isHorizontal ? {
|
|
1038
|
+
display: "flex",
|
|
1039
|
+
flex: "0 0 auto",
|
|
1040
|
+
pointerEvents: "none"
|
|
1041
|
+
} : { pointerEvents: "none" }
|
|
1042
|
+
}, children)));
|
|
927
1043
|
});
|
|
928
|
-
|
|
929
|
-
export { DEFAULT_EDGE_HOVER_SIZE, DEFAULT_EDGE_HOVER_SPEED_MULTIPLIER, ReactSway };
|
|
1044
|
+
export { DEFAULT_DEPTH_PERSPECTIVE, DEFAULT_DEPTH_SPACING, DEFAULT_EDGE_HOVER_SIZE, DEFAULT_EDGE_HOVER_SPEED_MULTIPLIER, ReactSway };
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/lafittemehdy/sway/issues"
|
|
8
8
|
},
|
|
9
|
-
"description": "
|
|
9
|
+
"description": "Infinite horizontal, vertical, and perspective depth scrolling for React, with automatic motion and accessible manual interactions.",
|
|
10
10
|
"exports": {
|
|
11
11
|
".": {
|
|
12
12
|
"types": "./dist/index.d.ts",
|
|
@@ -16,18 +16,18 @@
|
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@testing-library/jest-dom": "^
|
|
20
|
-
"@testing-library/react": "^16.3.
|
|
21
|
-
"@types/react": "^19.
|
|
22
|
-
"@types/react-dom": "^19.
|
|
23
|
-
"eslint": "^10.
|
|
19
|
+
"@testing-library/jest-dom": "^7.0.1",
|
|
20
|
+
"@testing-library/react": "^16.3.3",
|
|
21
|
+
"@types/react": "^19.3.0",
|
|
22
|
+
"@types/react-dom": "^19.3.0",
|
|
23
|
+
"eslint": "^10.14.0",
|
|
24
24
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
25
|
-
"jsdom": "^
|
|
26
|
-
"react": "^19.
|
|
27
|
-
"react-dom": "^19.
|
|
28
|
-
"typescript": "
|
|
29
|
-
"vite": "^8.
|
|
30
|
-
"vitest": "^
|
|
25
|
+
"jsdom": "^31.0.0",
|
|
26
|
+
"react": "^19.3.1",
|
|
27
|
+
"react-dom": "^19.3.1",
|
|
28
|
+
"typescript": "~6.0.3",
|
|
29
|
+
"vite": "^8.3.6",
|
|
30
|
+
"vitest": "^5.1.0"
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
33
|
"CHANGELOG.md",
|
|
@@ -37,9 +37,11 @@
|
|
|
37
37
|
"keywords": [
|
|
38
38
|
"content-stream",
|
|
39
39
|
"continuous-scroll",
|
|
40
|
+
"depth-scroll",
|
|
40
41
|
"frontend",
|
|
41
42
|
"infinite-scroll",
|
|
42
43
|
"javascript",
|
|
44
|
+
"perspective",
|
|
43
45
|
"react",
|
|
44
46
|
"react-component",
|
|
45
47
|
"scroller",
|
|
@@ -74,5 +76,5 @@
|
|
|
74
76
|
},
|
|
75
77
|
"type": "module",
|
|
76
78
|
"types": "./dist/index.d.ts",
|
|
77
|
-
"version": "0.
|
|
79
|
+
"version": "0.5.0"
|
|
78
80
|
}
|