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