react-sway 0.3.1 → 0.4.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 +19 -0
- package/LICENSE +21 -0
- package/README.md +196 -48
- package/dist/ReactSway.d.ts +46 -4
- package/dist/edge-hover-velocity.d.ts +30 -0
- package/dist/index.cjs +428 -161
- package/dist/index.d.ts +2 -1
- package/dist/index.js +428 -163
- package/dist/wheel-input.d.ts +14 -0
- package/package.json +20 -15
- package/src/ReactSway.tsx +0 -914
- package/src/index.ts +0 -5
package/dist/index.js
CHANGED
|
@@ -1,35 +1,85 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
1
|
+
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
//#region src/
|
|
3
|
+
//#region src/edge-hover-velocity.ts
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Defines the bounded spatial mapping from edge-hover depth to Sway velocity.
|
|
6
6
|
*/
|
|
7
|
-
/** Velocity applied per arrow key press (pixels). */
|
|
8
|
-
var ARROW_KEY_VELOCITY = 15;
|
|
9
7
|
/** Default edge-hover activation thickness in pixels. */
|
|
10
8
|
var DEFAULT_EDGE_HOVER_SIZE = 96;
|
|
11
|
-
/** Default
|
|
12
|
-
var
|
|
13
|
-
/**
|
|
14
|
-
var
|
|
15
|
-
/**
|
|
16
|
-
var
|
|
17
|
-
/**
|
|
18
|
-
var
|
|
19
|
-
/**
|
|
20
|
-
var
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
9
|
+
/** Default maximum edge-hover speed relative to the configured base speed. */
|
|
10
|
+
var DEFAULT_EDGE_HOVER_SPEED_MULTIPLIER = 6;
|
|
11
|
+
/** Upper bound for consumer-configured edge-hover speed amplification. */
|
|
12
|
+
var MAX_EDGE_HOVER_SPEED_MULTIPLIER = 10;
|
|
13
|
+
/** Normalized ingress band used to remove the zero-to-baseline velocity jump. */
|
|
14
|
+
var EDGE_HOVER_ENTRY_FEATHER_INTENSITY = .08;
|
|
15
|
+
/** Intensity reached at the physical edge of the visible viewport. */
|
|
16
|
+
var EDGE_HOVER_PHYSICAL_EDGE_INTENSITY = .5;
|
|
17
|
+
/** Historical physical-edge response retained before beyond-edge acceleration. */
|
|
18
|
+
var EDGE_HOVER_PHYSICAL_EDGE_SPEED_MULTIPLIER = 3;
|
|
19
|
+
function clampUnit(value) {
|
|
20
|
+
if (!Number.isFinite(value)) return 0;
|
|
21
|
+
return Math.max(0, Math.min(1, value));
|
|
22
|
+
}
|
|
23
|
+
function smoothUnitStep(value) {
|
|
24
|
+
const unitValue = clampUnit(value);
|
|
25
|
+
return unitValue * unitValue * (3 - 2 * unitValue);
|
|
26
|
+
}
|
|
27
|
+
function normalizeEdgeHoverIntensity(value) {
|
|
28
|
+
if (!Number.isFinite(value)) return 0;
|
|
29
|
+
const clampedValue = Math.max(-1, Math.min(1, value));
|
|
30
|
+
return clampedValue === 0 ? 0 : clampedValue;
|
|
31
|
+
}
|
|
32
|
+
function normalizeEdgeHoverSpeedMultiplier(value) {
|
|
33
|
+
if (!Number.isFinite(value)) return 6;
|
|
34
|
+
return Math.max(1, Math.min(MAX_EDGE_HOVER_SPEED_MULTIPLIER, value));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Maps signed edge depth onto a bounded velocity scale.
|
|
38
|
+
*
|
|
39
|
+
* Intensity carries direction and lies in [-1, 1]. The speed multiplier is
|
|
40
|
+
* the maximum amplification reached one activation-zone width beyond the
|
|
41
|
+
* visible edge.
|
|
42
|
+
*/
|
|
43
|
+
function getEdgeHoverVelocityScale(intensity, speedMultiplier = 6) {
|
|
44
|
+
const normalizedIntensity = normalizeEdgeHoverIntensity(intensity);
|
|
45
|
+
if (normalizedIntensity === 0) return 0;
|
|
46
|
+
const direction = Math.sign(normalizedIntensity);
|
|
47
|
+
const depth = Math.abs(normalizedIntensity);
|
|
48
|
+
const maximumScale = normalizeEdgeHoverSpeedMultiplier(speedMultiplier);
|
|
49
|
+
if (depth < EDGE_HOVER_ENTRY_FEATHER_INTENSITY) return direction * smoothUnitStep(depth / EDGE_HOVER_ENTRY_FEATHER_INTENSITY);
|
|
50
|
+
const physicalEdgeScale = Math.min(EDGE_HOVER_PHYSICAL_EDGE_SPEED_MULTIPLIER, maximumScale);
|
|
51
|
+
if (depth <= EDGE_HOVER_PHYSICAL_EDGE_INTENSITY) {
|
|
52
|
+
const physicalEdgeProgress = (depth - EDGE_HOVER_ENTRY_FEATHER_INTENSITY) / (EDGE_HOVER_PHYSICAL_EDGE_INTENSITY - EDGE_HOVER_ENTRY_FEATHER_INTENSITY);
|
|
53
|
+
return direction * (1 + (physicalEdgeScale - 1) * smoothUnitStep(physicalEdgeProgress));
|
|
54
|
+
}
|
|
55
|
+
const beyondEdgeProgress = (depth - EDGE_HOVER_PHYSICAL_EDGE_INTENSITY) / (1 - EDGE_HOVER_PHYSICAL_EDGE_INTENSITY);
|
|
56
|
+
return direction * (physicalEdgeScale + (maximumScale - physicalEdgeScale) * smoothUnitStep(beyondEdgeProgress));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Computes signed edge depth using a symmetric linear ramp.
|
|
60
|
+
*
|
|
61
|
+
* The inner edge of either activation zone maps to zero, the physical edge
|
|
62
|
+
* maps to half intensity, and motion keeps accelerating outside the viewport
|
|
63
|
+
* until it reaches the cap one activation-zone width beyond the visible edge.
|
|
64
|
+
*/
|
|
65
|
+
function getEdgeHoverIntensity(pointerPosition, visibleInterval, edgeHoverSize) {
|
|
66
|
+
const effectiveEdgeHoverSize = Math.min(Math.max(edgeHoverSize, 0), visibleInterval.size / 2);
|
|
67
|
+
if (effectiveEdgeHoverSize <= 0) return 0;
|
|
68
|
+
const accelerationDistance = effectiveEdgeHoverSize * 2;
|
|
69
|
+
const leadingEdgeEnd = visibleInterval.start + effectiveEdgeHoverSize;
|
|
70
|
+
if (pointerPosition <= leadingEdgeEnd) {
|
|
71
|
+
const leadingIntensity = clampUnit((leadingEdgeEnd - pointerPosition) / accelerationDistance);
|
|
72
|
+
if (leadingIntensity > 0) return leadingIntensity;
|
|
73
|
+
}
|
|
74
|
+
const trailingEdgeStart = visibleInterval.end - effectiveEdgeHoverSize;
|
|
75
|
+
if (pointerPosition >= trailingEdgeStart) {
|
|
76
|
+
const trailingIntensity = clampUnit((pointerPosition - trailingEdgeStart) / accelerationDistance);
|
|
77
|
+
if (trailingIntensity > 0) return -trailingIntensity;
|
|
78
|
+
}
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/wheel-input.ts
|
|
33
83
|
/** Fallback pixel height for wheel events reported in line units. */
|
|
34
84
|
var WHEEL_LINE_HEIGHT_FALLBACK_PX = 16;
|
|
35
85
|
/** WheelEvent deltaMode value for line-based deltas. */
|
|
@@ -38,24 +88,15 @@ var WHEEL_MODE_LINE = 1;
|
|
|
38
88
|
var WHEEL_MODE_PAGE = 2;
|
|
39
89
|
/** Minimum pixel impulse for discrete wheel hardware reporting tiny pixel deltas. */
|
|
40
90
|
var WHEEL_PIXEL_MIN_DELTA_PX = 48;
|
|
41
|
-
/** Multiplier applied to wheel deltaY to convert to scroll velocity. */
|
|
42
|
-
var WHEEL_VELOCITY_MULTIPLIER = .14;
|
|
43
|
-
/** Small immediate wheel movement used to keep input responsive without jumping. */
|
|
44
|
-
var WHEEL_IMMEDIATE_DELTA_FACTOR = .14;
|
|
45
91
|
/** Legacy wheelDelta magnitude that usually represents one physical wheel notch. */
|
|
46
92
|
var WHEEL_LEGACY_NOTCH_DELTA = 120;
|
|
47
|
-
/** Minimum touch movement before a gesture is classified by axis. */
|
|
48
|
-
var TOUCH_AXIS_LOCK_THRESHOLD_PX = 8;
|
|
49
93
|
function getLegacyWheelPixelDeltaY(event) {
|
|
50
94
|
const { wheelDelta, wheelDeltaY } = event;
|
|
51
95
|
const legacyWheelDelta = typeof wheelDeltaY === "number" ? wheelDeltaY : wheelDelta;
|
|
52
96
|
if (typeof legacyWheelDelta !== "number" || !Number.isFinite(legacyWheelDelta)) return 0;
|
|
53
97
|
return -(legacyWheelDelta / WHEEL_LEGACY_NOTCH_DELTA) * WHEEL_PIXEL_MIN_DELTA_PX;
|
|
54
98
|
}
|
|
55
|
-
/**
|
|
56
|
-
* Converts wheel deltas to pixels so mouse wheels, touchpads, and page-wheel
|
|
57
|
-
* devices feed the same Sway velocity system.
|
|
58
|
-
*/
|
|
99
|
+
/** Converts line, page, modern pixel, and legacy wheel deltas to pixels. */
|
|
59
100
|
function normalizeWheelDelta(event, container, axis) {
|
|
60
101
|
const rawDelta = axis === "x" ? event.deltaX : event.deltaY;
|
|
61
102
|
if (event.deltaMode === WHEEL_MODE_LINE) {
|
|
@@ -68,6 +109,10 @@ function normalizeWheelDelta(event, container, axis) {
|
|
|
68
109
|
if (Math.abs(rawDelta) < WHEEL_PIXEL_MIN_DELTA_PX && Math.abs(legacyWheelPixelDeltaY) >= WHEEL_PIXEL_MIN_DELTA_PX) return legacyWheelPixelDeltaY;
|
|
69
110
|
return rawDelta;
|
|
70
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Returns Sway's signed pixel delta, or null when native scroll chaining owns
|
|
114
|
+
* the gesture. This partial function is the wheel-ownership boundary.
|
|
115
|
+
*/
|
|
71
116
|
function getOwnedWheelDelta(event, container, isHorizontal, wheelMode) {
|
|
72
117
|
const deltaX = normalizeWheelDelta(event, container, "x");
|
|
73
118
|
const deltaY = normalizeWheelDelta(event, container, "y");
|
|
@@ -80,6 +125,41 @@ function getOwnedWheelDelta(event, container, isHorizontal, wheelMode) {
|
|
|
80
125
|
if (wheelMode === "axis" && absDeltaX > absDeltaY) return null;
|
|
81
126
|
return deltaY;
|
|
82
127
|
}
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/ReactSway.tsx
|
|
130
|
+
/**
|
|
131
|
+
* Core ReactSway component implementing axis-aware infinite scrolling interactions.
|
|
132
|
+
*/
|
|
133
|
+
/** Velocity applied per arrow key press (pixels). */
|
|
134
|
+
var ARROW_KEY_VELOCITY = 15;
|
|
135
|
+
/** Default friction coefficient applied to velocity each frame. */
|
|
136
|
+
var DEFAULT_FRICTION = .95;
|
|
137
|
+
/** Default IntersectionObserver rootMargin for lazy visibility detection. */
|
|
138
|
+
var DEFAULT_LAZY_ROOT_MARGIN = "100px";
|
|
139
|
+
/** Default IntersectionObserver threshold for lazy visibility detection. */
|
|
140
|
+
var DEFAULT_LAZY_THRESHOLD = .01;
|
|
141
|
+
/** Default delay in milliseconds before auto-scroll resumes after user interaction. */
|
|
142
|
+
var DEFAULT_RESUME_DELAY = 2e3;
|
|
143
|
+
/** Default scroll speed in pixels per frame at 60fps. */
|
|
144
|
+
var DEFAULT_SPEED = .5;
|
|
145
|
+
/** Minimum stacked content groups needed to cover a seamless loop phase. */
|
|
146
|
+
var MIN_LOOP_SEGMENTS = 2;
|
|
147
|
+
/** Maximum deltaTime cap to prevent physics instability during frame drops. */
|
|
148
|
+
var MAX_DELTA_TIME = 3;
|
|
149
|
+
/** Edge-hover catch-up cap; larger jumps read as stalls followed by snapping. */
|
|
150
|
+
var MAX_EDGE_HOVER_DELTA_TIME = 1;
|
|
151
|
+
/** Maximum allowed velocity magnitude to prevent runaway scrolling. */
|
|
152
|
+
var MAX_VELOCITY = 150;
|
|
153
|
+
/** Duration of a single frame at 60fps in milliseconds. */
|
|
154
|
+
var MS_PER_FRAME_60FPS = 16.667;
|
|
155
|
+
/** Speed multiplier when user prefers reduced motion (25% of normal). */
|
|
156
|
+
var REDUCED_MOTION_SPEED_FACTOR = .25;
|
|
157
|
+
/** Multiplier applied to wheel deltaY to convert to scroll velocity. */
|
|
158
|
+
var WHEEL_VELOCITY_MULTIPLIER = .14;
|
|
159
|
+
/** Small immediate wheel movement used to keep input responsive without jumping. */
|
|
160
|
+
var WHEEL_IMMEDIATE_DELTA_FACTOR = .14;
|
|
161
|
+
/** Minimum touch movement before a gesture is classified by axis. */
|
|
162
|
+
var TOUCH_AXIS_LOCK_THRESHOLD_PX = 8;
|
|
83
163
|
function getVisibleEdgeInterval(rect, isHorizontal) {
|
|
84
164
|
const rectEnd = isHorizontal ? rect.right : rect.bottom;
|
|
85
165
|
const rectStart = isHorizontal ? rect.left : rect.top;
|
|
@@ -113,10 +193,12 @@ function getVisibleEdgeInterval(rect, isHorizontal) {
|
|
|
113
193
|
* </ReactSway>
|
|
114
194
|
* ```
|
|
115
195
|
*/
|
|
116
|
-
function ReactSway({ axis, autoScroll = true, children, direction = "up", draggable = true, edgeHoverScroll = false, edgeHoverSize =
|
|
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) {
|
|
117
197
|
const isHorizontal = useMemo(() => axis ?? (direction === "left" || direction === "right" ? "horizontal" : "vertical"), [axis, direction]) === "horizontal";
|
|
118
|
-
const normalizedEdgeHoverSize = useMemo(() => Number.isFinite(edgeHoverSize) ? Math.max(1, edgeHoverSize) :
|
|
198
|
+
const normalizedEdgeHoverSize = useMemo(() => Number.isFinite(edgeHoverSize) ? Math.max(1, edgeHoverSize) : 96, [edgeHoverSize]);
|
|
199
|
+
const normalizedEdgeHoverSpeedMultiplier = useMemo(() => normalizeEdgeHoverSpeedMultiplier(edgeHoverSpeedMultiplier), [edgeHoverSpeedMultiplier]);
|
|
119
200
|
const normalizedFriction = useMemo(() => Number.isFinite(friction) ? Math.min(Math.max(friction, 0), 1) : DEFAULT_FRICTION, [friction]);
|
|
201
|
+
const normalizedLazyThreshold = useMemo(() => Number.isFinite(lazyThreshold) ? Math.min(Math.max(lazyThreshold, 0), 1) : DEFAULT_LAZY_THRESHOLD, [lazyThreshold]);
|
|
120
202
|
const normalizedResumeDelay = useMemo(() => Number.isFinite(resumeDelay) ? Math.max(0, resumeDelay) : DEFAULT_RESUME_DELAY, [resumeDelay]);
|
|
121
203
|
const normalizedSpeed = useMemo(() => Number.isFinite(speed) ? Math.max(0, speed) : DEFAULT_SPEED, [speed]);
|
|
122
204
|
const touchAction = useMemo(() => {
|
|
@@ -124,16 +206,15 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
|
|
|
124
206
|
return isHorizontal ? "pan-y" : "pan-x";
|
|
125
207
|
}, [draggable, isHorizontal]);
|
|
126
208
|
const overscrollBehavior = wheelEnabled && wheelMode === "capture" ? "contain" : "auto";
|
|
127
|
-
const [isDragging, setIsDragging] = useState(false);
|
|
128
209
|
const [isPaused, setIsPaused] = useState(false);
|
|
129
|
-
const [isTabActive, setIsTabActive] = useState(
|
|
130
|
-
const [loopPoint, setLoopPoint] = useState(0);
|
|
210
|
+
const [isTabActive, setIsTabActive] = useState(() => typeof document === "undefined" || !document.hidden);
|
|
131
211
|
const [loopSegmentCount, setLoopSegmentCount] = useState(MIN_LOOP_SEGMENTS);
|
|
132
212
|
const [prefersReducedMotion, setPrefersReducedMotion] = useState(() => {
|
|
133
213
|
if (typeof window === "undefined") return false;
|
|
134
214
|
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
135
215
|
});
|
|
136
216
|
const animationFrameRef = useRef(null);
|
|
217
|
+
const animationLoopRef = useRef(null);
|
|
137
218
|
const autoScrollRef = useRef({
|
|
138
219
|
active: autoScroll,
|
|
139
220
|
desired: autoScroll
|
|
@@ -148,13 +229,19 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
|
|
|
148
229
|
const lastTouchXRef = useRef(0);
|
|
149
230
|
const lastTouchYRef = useRef(0);
|
|
150
231
|
const loopPointRef = useRef(0);
|
|
151
|
-
const
|
|
232
|
+
const edgeHoverIntervalCacheFrameRef = useRef(null);
|
|
233
|
+
const edgeHoverIntervalCacheRef = useRef(null);
|
|
234
|
+
const edgeHoverIntensityRef = useRef(0);
|
|
235
|
+
const externalEdgeHoverLeaseRef = useRef(false);
|
|
152
236
|
const onPauseRef = useRef(onPause);
|
|
153
237
|
const onResumeRef = useRef(onResume);
|
|
154
238
|
const onScrollRef = useRef(onScroll);
|
|
155
239
|
const originalGroupRef = useRef(null);
|
|
156
240
|
const positionRef = useRef(0);
|
|
157
|
-
const
|
|
241
|
+
const loopMeasurementFrameRef = useRef(null);
|
|
242
|
+
const lastLoopMeasurementRef = useRef(null);
|
|
243
|
+
const mouseDragListenerCleanupRef = useRef(null);
|
|
244
|
+
const touchDragListenerCleanupRef = useRef(null);
|
|
158
245
|
const touchInteractionStateRef = useRef("idle");
|
|
159
246
|
const touchStartXRef = useRef(0);
|
|
160
247
|
const touchStartYRef = useRef(0);
|
|
@@ -184,6 +271,32 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
|
|
|
184
271
|
clearTimeout(inactivityTimerRef.current);
|
|
185
272
|
inactivityTimerRef.current = null;
|
|
186
273
|
}, []);
|
|
274
|
+
const startAnimationLoop = useCallback(() => {
|
|
275
|
+
if (animationFrameRef.current !== null || !animationLoopRef.current) return;
|
|
276
|
+
animationFrameRef.current = requestAnimationFrame(animationLoopRef.current);
|
|
277
|
+
}, []);
|
|
278
|
+
const clearEdgeHoverIntervalCache = useCallback(() => {
|
|
279
|
+
edgeHoverIntervalCacheRef.current = null;
|
|
280
|
+
if (edgeHoverIntervalCacheFrameRef.current !== null) {
|
|
281
|
+
cancelAnimationFrame(edgeHoverIntervalCacheFrameRef.current);
|
|
282
|
+
edgeHoverIntervalCacheFrameRef.current = null;
|
|
283
|
+
}
|
|
284
|
+
}, []);
|
|
285
|
+
const readVisibleEdgeInterval = useCallback((viewport) => {
|
|
286
|
+
const cachedInterval = edgeHoverIntervalCacheRef.current;
|
|
287
|
+
if (cachedInterval && cachedInterval.viewport === viewport && cachedInterval.isHorizontal === isHorizontal) return cachedInterval.interval;
|
|
288
|
+
const interval = getVisibleEdgeInterval(viewport.getBoundingClientRect(), isHorizontal);
|
|
289
|
+
edgeHoverIntervalCacheRef.current = {
|
|
290
|
+
interval,
|
|
291
|
+
isHorizontal,
|
|
292
|
+
viewport
|
|
293
|
+
};
|
|
294
|
+
if (edgeHoverIntervalCacheFrameRef.current === null) edgeHoverIntervalCacheFrameRef.current = requestAnimationFrame(() => {
|
|
295
|
+
edgeHoverIntervalCacheFrameRef.current = null;
|
|
296
|
+
edgeHoverIntervalCacheRef.current = null;
|
|
297
|
+
});
|
|
298
|
+
return interval;
|
|
299
|
+
}, [isHorizontal]);
|
|
187
300
|
const renderPosition = useCallback((rawPosition) => {
|
|
188
301
|
if (!containerRef.current) return;
|
|
189
302
|
const currentLoopPoint = loopPointRef.current;
|
|
@@ -200,22 +313,33 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
|
|
|
200
313
|
const wrapPosition = useCallback((rawPosition) => {
|
|
201
314
|
const currentLoopPoint = loopPointRef.current;
|
|
202
315
|
if (currentLoopPoint <= 0) return rawPosition;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
316
|
+
if (rawPosition <= 0 && rawPosition >= -currentLoopPoint * 2) return rawPosition;
|
|
317
|
+
let wrappedPosition = rawPosition % currentLoopPoint;
|
|
318
|
+
if (wrappedPosition > 0) wrappedPosition -= currentLoopPoint;
|
|
206
319
|
return wrappedPosition;
|
|
207
320
|
}, []);
|
|
321
|
+
const setDraggingState = useCallback((nextIsDragging) => {
|
|
322
|
+
isDraggingRef.current = nextIsDragging;
|
|
323
|
+
if (containerRef.current && draggable) containerRef.current.style.cursor = nextIsDragging ? "grabbing" : "grab";
|
|
324
|
+
}, [draggable]);
|
|
208
325
|
const recalculateLoopPoint = useCallback(() => {
|
|
209
326
|
if (!containerRef.current || !originalGroupRef.current) return;
|
|
210
327
|
const originalContentSize = isHorizontal ? originalGroupRef.current.scrollWidth : originalGroupRef.current.scrollHeight;
|
|
211
328
|
if (originalContentSize <= 0) return;
|
|
212
329
|
const viewport = containerRef.current.parentElement ?? containerRef.current;
|
|
213
330
|
const viewportSize = isHorizontal ? Math.max(viewport.clientWidth, 1) : Math.max(viewport.clientHeight, 1);
|
|
214
|
-
const nextLoopSegmentCount = Math.max(MIN_LOOP_SEGMENTS, Math.ceil(viewportSize / originalContentSize) +
|
|
331
|
+
const nextLoopSegmentCount = Math.max(MIN_LOOP_SEGMENTS, Math.ceil(viewportSize / originalContentSize) + 1);
|
|
215
332
|
const nextLoopPoint = originalContentSize;
|
|
333
|
+
const previousMeasurement = lastLoopMeasurementRef.current;
|
|
334
|
+
const nextMeasurement = {
|
|
335
|
+
contentSize: originalContentSize,
|
|
336
|
+
isHorizontal,
|
|
337
|
+
viewportSize
|
|
338
|
+
};
|
|
339
|
+
if (previousMeasurement?.contentSize === nextMeasurement.contentSize && previousMeasurement.isHorizontal === nextMeasurement.isHorizontal && previousMeasurement.viewportSize === nextMeasurement.viewportSize) return;
|
|
340
|
+
lastLoopMeasurementRef.current = nextMeasurement;
|
|
216
341
|
setLoopSegmentCount((previousLoopSegmentCount) => previousLoopSegmentCount === nextLoopSegmentCount ? previousLoopSegmentCount : nextLoopSegmentCount);
|
|
217
342
|
loopPointRef.current = nextLoopPoint;
|
|
218
|
-
setLoopPoint((previousLoopPoint) => Math.abs(previousLoopPoint - nextLoopPoint) < .5 ? previousLoopPoint : nextLoopPoint);
|
|
219
343
|
renderPosition(positionRef.current);
|
|
220
344
|
}, [isHorizontal, renderPosition]);
|
|
221
345
|
useEffect(() => {
|
|
@@ -223,20 +347,30 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
|
|
|
223
347
|
if (!autoScroll) {
|
|
224
348
|
clearInactivityTimer();
|
|
225
349
|
autoScrollRef.current.active = true;
|
|
226
|
-
}
|
|
227
|
-
}, [
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
350
|
+
} else startAnimationLoop();
|
|
351
|
+
}, [
|
|
352
|
+
autoScroll,
|
|
353
|
+
clearInactivityTimer,
|
|
354
|
+
startAnimationLoop
|
|
355
|
+
]);
|
|
356
|
+
const scheduleLoopPointRecalculation = useCallback(() => {
|
|
357
|
+
clearEdgeHoverIntervalCache();
|
|
358
|
+
if (loopMeasurementFrameRef.current !== null) return;
|
|
359
|
+
loopMeasurementFrameRef.current = requestAnimationFrame(() => {
|
|
360
|
+
loopMeasurementFrameRef.current = null;
|
|
361
|
+
recalculateLoopPoint();
|
|
362
|
+
});
|
|
363
|
+
}, [clearEdgeHoverIntervalCache, recalculateLoopPoint]);
|
|
232
364
|
useEffect(() => {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
365
|
+
lastLoopMeasurementRef.current = null;
|
|
366
|
+
scheduleLoopPointRecalculation();
|
|
367
|
+
}, [
|
|
368
|
+
children,
|
|
369
|
+
isHorizontal,
|
|
370
|
+
scheduleLoopPointRecalculation
|
|
371
|
+
]);
|
|
238
372
|
const pauseAutoScroll = useCallback(() => {
|
|
239
|
-
if (!pauseOnInteraction) return;
|
|
373
|
+
if (!pauseOnInteraction || !autoScrollRef.current.desired || !autoScrollRef.current.active) return;
|
|
240
374
|
autoScrollRef.current.active = false;
|
|
241
375
|
onPauseRef.current?.();
|
|
242
376
|
clearInactivityTimer();
|
|
@@ -249,11 +383,13 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
|
|
|
249
383
|
if (!autoScrollRef.current.desired || isPausedRef.current) return;
|
|
250
384
|
autoScrollRef.current.active = true;
|
|
251
385
|
onResumeRef.current?.();
|
|
386
|
+
startAnimationLoop();
|
|
252
387
|
}, normalizedResumeDelay);
|
|
253
388
|
}, [
|
|
254
389
|
clearInactivityTimer,
|
|
255
390
|
normalizedResumeDelay,
|
|
256
|
-
pauseOnInteraction
|
|
391
|
+
pauseOnInteraction,
|
|
392
|
+
startAnimationLoop
|
|
257
393
|
]);
|
|
258
394
|
const togglePause = useCallback(() => {
|
|
259
395
|
const newPaused = !isPausedRef.current;
|
|
@@ -267,8 +403,22 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
|
|
|
267
403
|
clearInactivityTimer();
|
|
268
404
|
autoScrollRef.current.active = true;
|
|
269
405
|
if (autoScrollRef.current.desired) onResumeRef.current?.();
|
|
406
|
+
startAnimationLoop();
|
|
270
407
|
}
|
|
271
|
-
}, [clearInactivityTimer]);
|
|
408
|
+
}, [clearInactivityTimer, startAnimationLoop]);
|
|
409
|
+
const setEdgeHoverIntensity = useCallback((nextIntensityValue) => {
|
|
410
|
+
const nextIntensity = normalizeEdgeHoverIntensity(nextIntensityValue);
|
|
411
|
+
edgeHoverIntensityRef.current = nextIntensity;
|
|
412
|
+
if (nextIntensity !== 0) startAnimationLoop();
|
|
413
|
+
}, [startAnimationLoop]);
|
|
414
|
+
const detachMouseDragListeners = useCallback(() => {
|
|
415
|
+
mouseDragListenerCleanupRef.current?.();
|
|
416
|
+
mouseDragListenerCleanupRef.current = null;
|
|
417
|
+
}, []);
|
|
418
|
+
const detachTouchDragListeners = useCallback(() => {
|
|
419
|
+
touchDragListenerCleanupRef.current?.();
|
|
420
|
+
touchDragListenerCleanupRef.current = null;
|
|
421
|
+
}, []);
|
|
272
422
|
const handleKeyDown = useCallback((e) => {
|
|
273
423
|
if (!keyboard) return;
|
|
274
424
|
switch (e.key) {
|
|
@@ -320,25 +470,16 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
|
|
|
320
470
|
break;
|
|
321
471
|
default: break;
|
|
322
472
|
}
|
|
473
|
+
startAnimationLoop();
|
|
323
474
|
}, [
|
|
324
475
|
commitPosition,
|
|
325
476
|
isHorizontal,
|
|
326
477
|
keyboard,
|
|
327
478
|
pauseAutoScroll,
|
|
328
479
|
scheduleAutoScrollResume,
|
|
480
|
+
startAnimationLoop,
|
|
329
481
|
togglePause
|
|
330
482
|
]);
|
|
331
|
-
const handleMouseDown = useCallback((e) => {
|
|
332
|
-
if (!draggable) return;
|
|
333
|
-
e.preventDefault();
|
|
334
|
-
containerRef.current?.focus();
|
|
335
|
-
setIsDragging(true);
|
|
336
|
-
isDraggingRef.current = true;
|
|
337
|
-
lastMouseXRef.current = e.clientX;
|
|
338
|
-
lastMouseYRef.current = e.clientY;
|
|
339
|
-
velocityRef.current = 0;
|
|
340
|
-
pauseAutoScroll();
|
|
341
|
-
}, [draggable, pauseAutoScroll]);
|
|
342
483
|
const handleMouseMove = useCallback((e) => {
|
|
343
484
|
if (!isDraggingRef.current) return;
|
|
344
485
|
e.preventDefault();
|
|
@@ -356,19 +497,77 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
|
|
|
356
497
|
const handleMouseUp = useCallback((e) => {
|
|
357
498
|
if (!isDraggingRef.current) return;
|
|
358
499
|
e.preventDefault();
|
|
359
|
-
|
|
360
|
-
|
|
500
|
+
setDraggingState(false);
|
|
501
|
+
detachMouseDragListeners();
|
|
502
|
+
scheduleAutoScrollResume();
|
|
503
|
+
startAnimationLoop();
|
|
504
|
+
}, [
|
|
505
|
+
detachMouseDragListeners,
|
|
506
|
+
scheduleAutoScrollResume,
|
|
507
|
+
setDraggingState,
|
|
508
|
+
startAnimationLoop
|
|
509
|
+
]);
|
|
510
|
+
const handleMouseCancel = useCallback(() => {
|
|
511
|
+
if (!isDraggingRef.current) return;
|
|
512
|
+
setDraggingState(false);
|
|
513
|
+
detachMouseDragListeners();
|
|
361
514
|
scheduleAutoScrollResume();
|
|
362
|
-
|
|
515
|
+
startAnimationLoop();
|
|
516
|
+
}, [
|
|
517
|
+
detachMouseDragListeners,
|
|
518
|
+
scheduleAutoScrollResume,
|
|
519
|
+
setDraggingState,
|
|
520
|
+
startAnimationLoop
|
|
521
|
+
]);
|
|
522
|
+
const attachMouseDragListeners = useCallback(() => {
|
|
523
|
+
detachMouseDragListeners();
|
|
524
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
525
|
+
window.addEventListener("mouseup", handleMouseUp);
|
|
526
|
+
window.addEventListener("blur", handleMouseCancel);
|
|
527
|
+
mouseDragListenerCleanupRef.current = () => {
|
|
528
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
529
|
+
window.removeEventListener("mouseup", handleMouseUp);
|
|
530
|
+
window.removeEventListener("blur", handleMouseCancel);
|
|
531
|
+
};
|
|
532
|
+
}, [
|
|
533
|
+
detachMouseDragListeners,
|
|
534
|
+
handleMouseCancel,
|
|
535
|
+
handleMouseMove,
|
|
536
|
+
handleMouseUp
|
|
537
|
+
]);
|
|
538
|
+
const handleMouseDown = useCallback((e) => {
|
|
539
|
+
if (!draggable) return;
|
|
540
|
+
e.preventDefault();
|
|
541
|
+
containerRef.current?.focus();
|
|
542
|
+
setDraggingState(true);
|
|
543
|
+
lastMouseXRef.current = e.clientX;
|
|
544
|
+
lastMouseYRef.current = e.clientY;
|
|
545
|
+
velocityRef.current = 0;
|
|
546
|
+
pauseAutoScroll();
|
|
547
|
+
attachMouseDragListeners();
|
|
548
|
+
startAnimationLoop();
|
|
549
|
+
}, [
|
|
550
|
+
attachMouseDragListeners,
|
|
551
|
+
draggable,
|
|
552
|
+
pauseAutoScroll,
|
|
553
|
+
setDraggingState,
|
|
554
|
+
startAnimationLoop
|
|
555
|
+
]);
|
|
363
556
|
const handleTouchEnd = useCallback((_e) => {
|
|
364
557
|
const wasActive = touchInteractionStateRef.current === "active";
|
|
365
558
|
touchInteractionStateRef.current = "idle";
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
559
|
+
detachTouchDragListeners();
|
|
560
|
+
if (isDraggingRef.current) setDraggingState(false);
|
|
561
|
+
if (wasActive) {
|
|
562
|
+
scheduleAutoScrollResume();
|
|
563
|
+
startAnimationLoop();
|
|
369
564
|
}
|
|
370
|
-
|
|
371
|
-
|
|
565
|
+
}, [
|
|
566
|
+
detachTouchDragListeners,
|
|
567
|
+
scheduleAutoScrollResume,
|
|
568
|
+
setDraggingState,
|
|
569
|
+
startAnimationLoop
|
|
570
|
+
]);
|
|
372
571
|
const handleTouchMove = useCallback((e) => {
|
|
373
572
|
if (touchInteractionStateRef.current === "idle" || touchInteractionStateRef.current === "ignored") return;
|
|
374
573
|
if (e.touches.length !== 1) return;
|
|
@@ -384,8 +583,7 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
|
|
|
384
583
|
return;
|
|
385
584
|
}
|
|
386
585
|
touchInteractionStateRef.current = "active";
|
|
387
|
-
|
|
388
|
-
isDraggingRef.current = true;
|
|
586
|
+
setDraggingState(true);
|
|
389
587
|
pauseAutoScroll();
|
|
390
588
|
}
|
|
391
589
|
e.preventDefault();
|
|
@@ -399,8 +597,24 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
|
|
|
399
597
|
commitPosition,
|
|
400
598
|
isHorizontal,
|
|
401
599
|
pauseAutoScroll,
|
|
600
|
+
setDraggingState,
|
|
402
601
|
wrapPosition
|
|
403
602
|
]);
|
|
603
|
+
const attachTouchDragListeners = useCallback(() => {
|
|
604
|
+
detachTouchDragListeners();
|
|
605
|
+
window.addEventListener("touchmove", handleTouchMove, { passive: false });
|
|
606
|
+
window.addEventListener("touchend", handleTouchEnd, { passive: true });
|
|
607
|
+
window.addEventListener("touchcancel", handleTouchEnd, { passive: true });
|
|
608
|
+
touchDragListenerCleanupRef.current = () => {
|
|
609
|
+
window.removeEventListener("touchmove", handleTouchMove);
|
|
610
|
+
window.removeEventListener("touchend", handleTouchEnd);
|
|
611
|
+
window.removeEventListener("touchcancel", handleTouchEnd);
|
|
612
|
+
};
|
|
613
|
+
}, [
|
|
614
|
+
detachTouchDragListeners,
|
|
615
|
+
handleTouchEnd,
|
|
616
|
+
handleTouchMove
|
|
617
|
+
]);
|
|
404
618
|
const handleTouchStart = useCallback((e) => {
|
|
405
619
|
if (!draggable || e.touches.length !== 1) return;
|
|
406
620
|
containerRef.current?.focus();
|
|
@@ -410,13 +624,14 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
|
|
|
410
624
|
touchStartXRef.current = e.touches[0].clientX;
|
|
411
625
|
touchStartYRef.current = e.touches[0].clientY;
|
|
412
626
|
velocityRef.current = 0;
|
|
413
|
-
|
|
627
|
+
attachTouchDragListeners();
|
|
628
|
+
}, [attachTouchDragListeners, draggable]);
|
|
414
629
|
const handleWheel = useCallback((e) => {
|
|
415
|
-
if (!wheelEnabled) return;
|
|
630
|
+
if (!wheelEnabled) return false;
|
|
416
631
|
const currentContainer = e.currentTarget instanceof HTMLElement ? e.currentTarget : containerRef.current;
|
|
417
|
-
if (!currentContainer) return;
|
|
632
|
+
if (!currentContainer) return false;
|
|
418
633
|
const ownedDelta = getOwnedWheelDelta(e, currentContainer, isHorizontal, wheelMode);
|
|
419
|
-
if (ownedDelta === null) return;
|
|
634
|
+
if (ownedDelta === null) return false;
|
|
420
635
|
e.preventDefault();
|
|
421
636
|
const wheelDelta = -ownedDelta;
|
|
422
637
|
const nextPosition = wrapPosition(positionRef.current + wheelDelta * WHEEL_IMMEDIATE_DELTA_FACTOR);
|
|
@@ -425,125 +640,155 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
|
|
|
425
640
|
velocityRef.current = Math.max(-150, Math.min(MAX_VELOCITY, velocityRef.current));
|
|
426
641
|
pauseAutoScroll();
|
|
427
642
|
scheduleAutoScrollResume();
|
|
643
|
+
startAnimationLoop();
|
|
644
|
+
return true;
|
|
428
645
|
}, [
|
|
429
646
|
commitPosition,
|
|
430
647
|
isHorizontal,
|
|
431
648
|
pauseAutoScroll,
|
|
432
649
|
scheduleAutoScrollResume,
|
|
650
|
+
startAnimationLoop,
|
|
433
651
|
wheelEnabled,
|
|
434
652
|
wheelMode,
|
|
435
653
|
wrapPosition
|
|
436
654
|
]);
|
|
437
|
-
const
|
|
438
|
-
if (!edgeHoverScroll) return;
|
|
655
|
+
const applyEdgeHoverPoint = useCallback((point) => {
|
|
656
|
+
if (!edgeHoverScroll) return false;
|
|
439
657
|
const viewport = containerRef.current?.parentElement ?? containerRef.current;
|
|
440
|
-
if (!viewport) return;
|
|
441
|
-
const visibleInterval =
|
|
658
|
+
if (!viewport) return false;
|
|
659
|
+
const visibleInterval = readVisibleEdgeInterval(viewport);
|
|
442
660
|
if (!visibleInterval) {
|
|
443
|
-
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
const pointerPosition = isHorizontal ? e.clientX : e.clientY;
|
|
447
|
-
const effectiveEdgeHoverSize = Math.min(normalizedEdgeHoverSize, visibleInterval.size / 2);
|
|
448
|
-
if (pointerPosition <= visibleInterval.start + effectiveEdgeHoverSize) {
|
|
449
|
-
edgeHoverDirectionRef.current = 1;
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
|
-
if (pointerPosition >= visibleInterval.end - effectiveEdgeHoverSize) {
|
|
453
|
-
edgeHoverDirectionRef.current = -1;
|
|
454
|
-
return;
|
|
661
|
+
setEdgeHoverIntensity(0);
|
|
662
|
+
return false;
|
|
455
663
|
}
|
|
456
|
-
|
|
664
|
+
const nextIntensity = getEdgeHoverIntensity(isHorizontal ? point.clientX : point.clientY, visibleInterval, normalizedEdgeHoverSize);
|
|
665
|
+
setEdgeHoverIntensity(nextIntensity);
|
|
666
|
+
return true;
|
|
457
667
|
}, [
|
|
458
668
|
edgeHoverScroll,
|
|
459
669
|
isHorizontal,
|
|
460
|
-
normalizedEdgeHoverSize
|
|
670
|
+
normalizedEdgeHoverSize,
|
|
671
|
+
readVisibleEdgeInterval,
|
|
672
|
+
setEdgeHoverIntensity
|
|
461
673
|
]);
|
|
674
|
+
const handleEdgeHover = useCallback((point) => {
|
|
675
|
+
const accepted = applyEdgeHoverPoint(point);
|
|
676
|
+
externalEdgeHoverLeaseRef.current = accepted;
|
|
677
|
+
return accepted;
|
|
678
|
+
}, [applyEdgeHoverPoint]);
|
|
679
|
+
const clearEdgeHover = useCallback(() => {
|
|
680
|
+
externalEdgeHoverLeaseRef.current = false;
|
|
681
|
+
setEdgeHoverIntensity(0);
|
|
682
|
+
}, [setEdgeHoverIntensity]);
|
|
683
|
+
useImperativeHandle(ref, () => ({
|
|
684
|
+
clearEdgeHover,
|
|
685
|
+
handleEdgeHover,
|
|
686
|
+
handleWheel
|
|
687
|
+
}), [
|
|
688
|
+
clearEdgeHover,
|
|
689
|
+
handleEdgeHover,
|
|
690
|
+
handleWheel
|
|
691
|
+
]);
|
|
692
|
+
const handleEdgeHoverMove = useCallback((e) => {
|
|
693
|
+
if (externalEdgeHoverLeaseRef.current) return;
|
|
694
|
+
applyEdgeHoverPoint(e);
|
|
695
|
+
}, [applyEdgeHoverPoint]);
|
|
462
696
|
const handleEdgeHoverLeave = useCallback(() => {
|
|
463
|
-
|
|
464
|
-
|
|
697
|
+
if (externalEdgeHoverLeaseRef.current) return;
|
|
698
|
+
setEdgeHoverIntensity(0);
|
|
699
|
+
}, [setEdgeHoverIntensity]);
|
|
465
700
|
useEffect(() => {
|
|
466
701
|
const currentContainer = containerRef.current;
|
|
467
702
|
if (!currentContainer) return;
|
|
468
|
-
currentContainer.addEventListener("mousedown", handleMouseDown);
|
|
469
|
-
window.addEventListener("mousemove", handleMouseMove);
|
|
470
|
-
window.addEventListener("mouseup", handleMouseUp);
|
|
471
703
|
if (draggable) {
|
|
704
|
+
currentContainer.addEventListener("mousedown", handleMouseDown);
|
|
472
705
|
currentContainer.addEventListener("touchstart", handleTouchStart, { passive: true });
|
|
473
|
-
window.addEventListener("touchmove", handleTouchMove, { passive: false });
|
|
474
|
-
window.addEventListener("touchend", handleTouchEnd, { passive: true });
|
|
475
706
|
}
|
|
476
707
|
if (wheelEnabled) currentContainer.addEventListener("wheel", handleWheel, { passive: false });
|
|
477
708
|
const edgeHoverTarget = currentContainer.parentElement ?? currentContainer;
|
|
478
|
-
|
|
479
|
-
|
|
709
|
+
const usesPointerEdgeHover = typeof window.PointerEvent === "function";
|
|
710
|
+
const usesNativeEdgeHover = edgeHoverScroll && edgeHoverInputMode === "hybrid";
|
|
711
|
+
if (usesNativeEdgeHover && usesPointerEdgeHover) {
|
|
712
|
+
edgeHoverTarget.addEventListener("pointermove", handleEdgeHoverMove);
|
|
713
|
+
edgeHoverTarget.addEventListener("pointerleave", handleEdgeHoverLeave);
|
|
714
|
+
edgeHoverTarget.addEventListener("pointercancel", clearEdgeHover);
|
|
715
|
+
} else if (usesNativeEdgeHover) {
|
|
716
|
+
edgeHoverTarget.addEventListener("mousemove", handleEdgeHoverMove);
|
|
717
|
+
edgeHoverTarget.addEventListener("mouseleave", handleEdgeHoverLeave);
|
|
718
|
+
}
|
|
480
719
|
return () => {
|
|
481
|
-
currentContainer.removeEventListener("mousedown", handleMouseDown);
|
|
482
|
-
window.removeEventListener("mousemove", handleMouseMove);
|
|
483
|
-
window.removeEventListener("mouseup", handleMouseUp);
|
|
484
720
|
if (draggable) {
|
|
721
|
+
currentContainer.removeEventListener("mousedown", handleMouseDown);
|
|
485
722
|
currentContainer.removeEventListener("touchstart", handleTouchStart);
|
|
486
|
-
window.removeEventListener("touchmove", handleTouchMove);
|
|
487
|
-
window.removeEventListener("touchend", handleTouchEnd);
|
|
488
723
|
}
|
|
489
724
|
if (wheelEnabled) currentContainer.removeEventListener("wheel", handleWheel);
|
|
490
|
-
|
|
491
|
-
|
|
725
|
+
if (usesNativeEdgeHover && usesPointerEdgeHover) {
|
|
726
|
+
edgeHoverTarget.removeEventListener("pointermove", handleEdgeHoverMove);
|
|
727
|
+
edgeHoverTarget.removeEventListener("pointerleave", handleEdgeHoverLeave);
|
|
728
|
+
edgeHoverTarget.removeEventListener("pointercancel", clearEdgeHover);
|
|
729
|
+
} else if (usesNativeEdgeHover) {
|
|
730
|
+
edgeHoverTarget.removeEventListener("mousemove", handleEdgeHoverMove);
|
|
731
|
+
edgeHoverTarget.removeEventListener("mouseleave", handleEdgeHoverLeave);
|
|
732
|
+
}
|
|
733
|
+
detachMouseDragListeners();
|
|
734
|
+
detachTouchDragListeners();
|
|
492
735
|
};
|
|
493
736
|
}, [
|
|
737
|
+
detachMouseDragListeners,
|
|
738
|
+
detachTouchDragListeners,
|
|
494
739
|
draggable,
|
|
740
|
+
edgeHoverScroll,
|
|
741
|
+
edgeHoverInputMode,
|
|
742
|
+
clearEdgeHover,
|
|
495
743
|
handleEdgeHoverLeave,
|
|
496
744
|
handleEdgeHoverMove,
|
|
497
745
|
handleMouseDown,
|
|
498
|
-
handleMouseMove,
|
|
499
|
-
handleMouseUp,
|
|
500
|
-
handleTouchEnd,
|
|
501
|
-
handleTouchMove,
|
|
502
746
|
handleTouchStart,
|
|
503
747
|
handleWheel,
|
|
504
748
|
wheelEnabled
|
|
505
749
|
]);
|
|
506
|
-
const debouncedRecalculate = useCallback(() => {
|
|
507
|
-
if (resizeDebounceTimerRef.current) clearTimeout(resizeDebounceTimerRef.current);
|
|
508
|
-
resizeDebounceTimerRef.current = setTimeout(() => {
|
|
509
|
-
resizeDebounceTimerRef.current = null;
|
|
510
|
-
recalculateLoopPoint();
|
|
511
|
-
}, RESIZE_DEBOUNCE_MS);
|
|
512
|
-
}, [recalculateLoopPoint]);
|
|
513
750
|
useEffect(() => {
|
|
514
|
-
window.addEventListener("resize",
|
|
751
|
+
window.addEventListener("resize", scheduleLoopPointRecalculation);
|
|
515
752
|
return () => {
|
|
516
|
-
window.removeEventListener("resize",
|
|
753
|
+
window.removeEventListener("resize", scheduleLoopPointRecalculation);
|
|
517
754
|
};
|
|
518
|
-
}, [
|
|
755
|
+
}, [scheduleLoopPointRecalculation]);
|
|
519
756
|
useEffect(() => {
|
|
520
|
-
if (!containerRef.current || typeof ResizeObserver === "undefined") return;
|
|
757
|
+
if (!containerRef.current || !originalGroupRef.current || typeof ResizeObserver === "undefined") return;
|
|
758
|
+
const currentContainer = containerRef.current;
|
|
759
|
+
const originalGroup = originalGroupRef.current;
|
|
760
|
+
const viewport = currentContainer.parentElement ?? currentContainer;
|
|
521
761
|
const observer = new ResizeObserver(() => {
|
|
522
|
-
|
|
762
|
+
scheduleLoopPointRecalculation();
|
|
523
763
|
});
|
|
524
|
-
observer.observe(
|
|
764
|
+
observer.observe(originalGroup);
|
|
765
|
+
if (viewport !== originalGroup) observer.observe(viewport);
|
|
525
766
|
return () => {
|
|
526
767
|
observer.disconnect();
|
|
527
768
|
};
|
|
528
|
-
}, [
|
|
769
|
+
}, [scheduleLoopPointRecalculation]);
|
|
529
770
|
useEffect(() => {
|
|
530
771
|
return () => {
|
|
531
|
-
if (
|
|
532
|
-
|
|
533
|
-
|
|
772
|
+
if (loopMeasurementFrameRef.current !== null) {
|
|
773
|
+
cancelAnimationFrame(loopMeasurementFrameRef.current);
|
|
774
|
+
loopMeasurementFrameRef.current = null;
|
|
534
775
|
}
|
|
776
|
+
clearEdgeHoverIntervalCache();
|
|
535
777
|
};
|
|
536
|
-
}, []);
|
|
778
|
+
}, [clearEdgeHoverIntervalCache]);
|
|
537
779
|
useEffect(() => {
|
|
538
780
|
const handleVisibilityChange = () => {
|
|
539
781
|
setIsTabActive(!document.hidden);
|
|
540
|
-
if (!document.hidden)
|
|
782
|
+
if (!document.hidden) {
|
|
783
|
+
lastFrameTimeRef.current = performance.now();
|
|
784
|
+
startAnimationLoop();
|
|
785
|
+
}
|
|
541
786
|
};
|
|
542
787
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
543
788
|
return () => {
|
|
544
789
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
545
790
|
};
|
|
546
|
-
}, []);
|
|
791
|
+
}, [startAnimationLoop]);
|
|
547
792
|
useEffect(() => {
|
|
548
793
|
return () => {
|
|
549
794
|
clearInactivityTimer();
|
|
@@ -551,10 +796,11 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
|
|
|
551
796
|
}, [clearInactivityTimer]);
|
|
552
797
|
useEffect(() => {
|
|
553
798
|
if (!isTabActive || isPaused) {
|
|
554
|
-
if (animationFrameRef.current) {
|
|
799
|
+
if (animationFrameRef.current !== null) {
|
|
555
800
|
cancelAnimationFrame(animationFrameRef.current);
|
|
556
801
|
animationFrameRef.current = null;
|
|
557
802
|
}
|
|
803
|
+
animationLoopRef.current = null;
|
|
558
804
|
return;
|
|
559
805
|
}
|
|
560
806
|
lastFrameTimeRef.current = 0;
|
|
@@ -568,27 +814,43 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
|
|
|
568
814
|
else velocityRef.current = 0;
|
|
569
815
|
let nextPosition = positionRef.current;
|
|
570
816
|
const effectiveSpeed = prefersReducedMotion ? normalizedSpeed * REDUCED_MOTION_SPEED_FACTOR : normalizedSpeed;
|
|
571
|
-
const edgeHoverMultiplier = edgeHoverScroll ?
|
|
572
|
-
|
|
573
|
-
if (
|
|
817
|
+
const edgeHoverMultiplier = edgeHoverScroll ? getEdgeHoverVelocityScale(edgeHoverIntensityRef.current, normalizedEdgeHoverSpeedMultiplier) : directionMultiplier;
|
|
818
|
+
const shouldAutoScroll = (autoScrollRef.current.desired && autoScrollRef.current.active || externalEdgeHoverLeaseRef.current) && !isDraggingRef.current && (!edgeHoverScroll || edgeHoverMultiplier !== 0);
|
|
819
|
+
if (shouldAutoScroll) {
|
|
820
|
+
const autoScrollDeltaTime = edgeHoverScroll ? Math.min(deltaTime, MAX_EDGE_HOVER_DELTA_TIME) : deltaTime;
|
|
821
|
+
nextPosition += edgeHoverMultiplier * effectiveSpeed * autoScrollDeltaTime;
|
|
822
|
+
}
|
|
823
|
+
const shouldApplyMomentum = !isDraggingRef.current && Math.abs(velocityRef.current) > .1;
|
|
824
|
+
if (shouldApplyMomentum) nextPosition += velocityRef.current * deltaTime;
|
|
574
825
|
nextPosition = wrapPosition(nextPosition);
|
|
575
826
|
commitPosition(nextPosition);
|
|
576
|
-
|
|
827
|
+
if (isDraggingRef.current || shouldAutoScroll || shouldApplyMomentum) {
|
|
828
|
+
animationFrameRef.current = requestAnimationFrame(animate);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
animationFrameRef.current = null;
|
|
832
|
+
lastFrameTimeRef.current = 0;
|
|
577
833
|
};
|
|
578
|
-
|
|
834
|
+
animationLoopRef.current = animate;
|
|
835
|
+
startAnimationLoop();
|
|
579
836
|
return () => {
|
|
580
|
-
if (animationFrameRef.current)
|
|
837
|
+
if (animationFrameRef.current !== null) {
|
|
838
|
+
cancelAnimationFrame(animationFrameRef.current);
|
|
839
|
+
animationFrameRef.current = null;
|
|
840
|
+
}
|
|
841
|
+
if (animationLoopRef.current === animate) animationLoopRef.current = null;
|
|
581
842
|
};
|
|
582
843
|
}, [
|
|
583
844
|
commitPosition,
|
|
584
845
|
direction,
|
|
585
846
|
edgeHoverScroll,
|
|
586
|
-
isHorizontal,
|
|
587
847
|
isPaused,
|
|
588
848
|
isTabActive,
|
|
589
849
|
normalizedFriction,
|
|
850
|
+
normalizedEdgeHoverSpeedMultiplier,
|
|
590
851
|
normalizedSpeed,
|
|
591
852
|
prefersReducedMotion,
|
|
853
|
+
startAnimationLoop,
|
|
592
854
|
wrapPosition
|
|
593
855
|
]);
|
|
594
856
|
useEffect(() => {
|
|
@@ -600,7 +862,7 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
|
|
|
600
862
|
}, {
|
|
601
863
|
root: null,
|
|
602
864
|
rootMargin: lazyRootMargin,
|
|
603
|
-
threshold:
|
|
865
|
+
threshold: normalizedLazyThreshold
|
|
604
866
|
});
|
|
605
867
|
const items = containerRef.current.querySelectorAll(".content-item");
|
|
606
868
|
items.forEach((item) => observer.observe(item));
|
|
@@ -612,13 +874,14 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
|
|
|
612
874
|
children,
|
|
613
875
|
lazy,
|
|
614
876
|
lazyRootMargin,
|
|
615
|
-
|
|
877
|
+
normalizedLazyThreshold
|
|
616
878
|
]);
|
|
617
|
-
return /* @__PURE__ */ jsxs("
|
|
618
|
-
|
|
879
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
880
|
+
"aria-label": ariaLabel,
|
|
881
|
+
className: `react-sway-container scroller-content${className ? ` ${className}` : ""}`,
|
|
619
882
|
ref: containerRef,
|
|
620
883
|
style: {
|
|
621
|
-
cursor: draggable ?
|
|
884
|
+
cursor: draggable ? isDraggingRef.current ? "grabbing" : "grab" : "default",
|
|
622
885
|
display: isHorizontal ? "flex" : void 0,
|
|
623
886
|
flexDirection: isHorizontal ? "row" : void 0,
|
|
624
887
|
height: isHorizontal ? "100%" : "max-content",
|
|
@@ -651,14 +914,16 @@ function ReactSway({ axis, autoScroll = true, children, direction = "up", dragga
|
|
|
651
914
|
"aria-hidden": "true",
|
|
652
915
|
className: "content-group duplicate",
|
|
653
916
|
"data-duplicate": "true",
|
|
917
|
+
inert: true,
|
|
654
918
|
role: "presentation",
|
|
655
919
|
style: isHorizontal ? {
|
|
656
920
|
display: "flex",
|
|
657
|
-
flex: "0 0 auto"
|
|
658
|
-
|
|
921
|
+
flex: "0 0 auto",
|
|
922
|
+
pointerEvents: "none"
|
|
923
|
+
} : { pointerEvents: "none" },
|
|
659
924
|
children
|
|
660
925
|
}, `duplicate-${duplicateIndex}`))]
|
|
661
926
|
});
|
|
662
|
-
}
|
|
927
|
+
});
|
|
663
928
|
//#endregion
|
|
664
|
-
export { ReactSway };
|
|
929
|
+
export { DEFAULT_EDGE_HOVER_SIZE, DEFAULT_EDGE_HOVER_SPEED_MULTIPLIER, ReactSway };
|