react-sway 0.4.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +44 -7
- package/dist/ReactSway.d.ts +14 -6
- package/dist/depth-position.d.ts +9 -0
- package/dist/index.cjs +204 -72
- package/dist/index.d.ts +1 -0
- package/dist/index.js +182 -74
- package/package.json +9 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0 - 2026-11-09
|
|
4
|
+
|
|
5
|
+
- Add perspective depth scrolling with toward/away directions and configurable camera distance and plane spacing.
|
|
6
|
+
- Mount each depth child once and recycle only at zero opacity without crossing the camera.
|
|
7
|
+
- Share wheel, drag, touch, keyboard, pause, and lifecycle behavior across all three axes.
|
|
8
|
+
- Add `active` and `reducedMotion` for host lifecycle and accessibility integration; preserve React 17/18/19 and existing planar behavior.
|
|
9
|
+
- Add responsive depth documentation and browser coverage, plus geometry and lifecycle regressions.
|
|
10
|
+
- Make the ESM build directly importable in Node with React 17, and verify packed ESM/CJS/SSR consumers on React 17/18/19.
|
|
11
|
+
- Update development dependencies and npm, test Node 22/24/26, and resolve all known dependency audit findings. Keep TypeScript 6.0.3 for ESLint parser compatibility.
|
|
12
|
+
|
|
3
13
|
## 0.4.3 - 2026-10-21
|
|
4
14
|
|
|
5
15
|
- Preserve browser zoom gestures, nested controls, and secondary mouse clicks.
|
package/README.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# react-sway
|
|
2
2
|
|
|
3
|
-
A React component for smooth, infinite, and interactive content scrolling on
|
|
3
|
+
A React component for smooth, infinite, and interactive content scrolling on vertical, horizontal, or perspective depth axes, controllable via touch, mouse drag, wheel, keyboard, or edge hover. React 17, 18, and 19 are supported without runtime dependencies beyond React and React DOM.
|
|
4
4
|
|
|
5
5
|
## What is `react-sway`?
|
|
6
6
|
|
|
7
7
|
React Sway takes your list of items and makes them scroll endlessly. It's designed to be easy to use and performant, with auto-scrolling that pauses when users interact. Edge-hover layouts can also be configured to scroll only while the pointer hovers the active axis boundary: top/bottom for vertical loops or left/right for horizontal loops. Edge-hover speed ramps from idle to a bounded multiple of the configured speed as the pointer moves deeper into the boundary zone.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Vertical and horizontal modes duplicate content into inert `<aside>` elements with `aria-hidden="true"` and `role="presentation"`. Depth mode mounts each direct child once and recycles its 3D plane at zero opacity, keeping every plane behind the perspective camera.
|
|
10
10
|
|
|
11
11
|
### Core Features
|
|
12
12
|
|
|
13
|
-
* **Smooth Infinite Scroll:** Content loops continuously on
|
|
13
|
+
* **Smooth Infinite Scroll:** Content loops continuously on vertical, horizontal, or depth axes.
|
|
14
14
|
* **Auto-Scroll:** Scrolls automatically, with configurable speed and direction.
|
|
15
15
|
* **Edge Hover:** Use `edgeHoverScroll` to keep auto-scroll idle until the pointer enters the active axis boundary zone, with speed scaled smoothly by boundary depth and capped by `edgeHoverSpeedMultiplier`.
|
|
16
16
|
* **User Friendly Interactions:**
|
|
@@ -277,16 +277,51 @@ function MinimalEdgeHoverExamples() {
|
|
|
277
277
|
}
|
|
278
278
|
```
|
|
279
279
|
|
|
280
|
+
### Perspective depth scrolling
|
|
281
|
+
|
|
282
|
+
```tsx
|
|
283
|
+
import { ReactSway } from 'react-sway';
|
|
284
|
+
|
|
285
|
+
function DepthGallery({ active = true }: { active?: boolean }) {
|
|
286
|
+
return (
|
|
287
|
+
<div className="depth-viewport">
|
|
288
|
+
<ReactSway active={active} ariaLabel="Photo gallery" axis="depth" direction="toward">
|
|
289
|
+
<img alt="Mountain landscape" className="depth-image" src="/mountain.jpg" />
|
|
290
|
+
<img alt="Modern architecture" className="depth-image depth-image-offset" src="/building.jpg" />
|
|
291
|
+
<img alt="Coastal landscape" className="depth-image" src="/coast.jpg" />
|
|
292
|
+
</ReactSway>
|
|
293
|
+
</div>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
```css
|
|
299
|
+
.depth-viewport { height: 600px; overflow: hidden; position: relative; }
|
|
300
|
+
.depth-image { display: block; height: 200px; max-width: none; object-fit: cover; transform: translate(-50%, -50%); width: 160px; }
|
|
301
|
+
.depth-image-offset { margin-left: 50px; margin-top: -80px; width: 200px; }
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Each **direct child** is one plane anchored at the viewport center. A fragment or a list wrapper is one plane; pass separate children for independent motion. Give children explicit sizes and use their CSS for composition, offsets, and tilt. The positioned parent supplies viewport dimensions and clipping. Keep `overflow: hidden` on that parent so the internal 3D scene retains depth.
|
|
305
|
+
|
|
306
|
+
`direction="toward"` approaches the camera; `"away"` recedes. Either direction infers `axis="depth"` when the axis is omitted. `depthPerspective` defaults to 800 CSS pixels and `depthSpacing` to 420; both must be finite and positive. Invalid values use their defaults, positive values below 1 are clamped to 1, and larger values are bounded by `Number.MAX_SAFE_INTEGER`. The exported `DEFAULT_DEPTH_PERSPECTIVE` and `DEFAULT_DEPTH_SPACING` constants share those defaults.
|
|
307
|
+
|
|
308
|
+
The loop period is the direct-child count times `depthSpacing`. Automatic movement and manual wheel/drag/keyboard input use the same periodic phase; vertical gestures and up/down arrows control depth. Set `autoScroll={false}` for manual exploration, or `active={false}` to suspend Sway's animation and input while preserving the current position. Empty depth scenes stay idle. Host applications can request reduced motion with `reducedMotion`; the system preference always takes precedence. Depth auto-scroll and momentum stop under reduced motion, while deliberate manual steps remain available. Existing planar loops retain their reduced speed behavior.
|
|
309
|
+
|
|
310
|
+
For decorative backgrounds, disable `keyboard`, `draggable`, and `wheelEnabled`, use empty image alt text, and put stationary application controls outside the Sway region. Automatic motion should have an accessible pause control. Inspect the [live depth example](https://lafittemehdy.github.io/sway/) for a responsive composition.
|
|
311
|
+
|
|
280
312
|
## Props
|
|
281
313
|
|
|
282
314
|
| Prop | Type | Default | Description |
|
|
283
315
|
|------|------|---------|-------------|
|
|
316
|
+
| `active` | `boolean` | `true` | Suspend Sway animation and input when false, retaining content and position. Nested native controls remain usable. |
|
|
284
317
|
| `ariaLabel` | `string` | `'Scrollable content'` | Accessible name for the scrolling region. |
|
|
285
|
-
| `axis` | `'horizontal' \| 'vertical'` | Inferred from `direction`, otherwise `'vertical'` | Scroll axis
|
|
318
|
+
| `axis` | `'depth' \| 'horizontal' \| 'vertical'` | Inferred from `direction`, otherwise `'vertical'` | Scroll axis; depth assigns each direct child a 3D plane. |
|
|
286
319
|
| `autoScroll` | `boolean` | `true` | Enable/disable auto-scrolling. |
|
|
287
320
|
| `children` | `ReactNode` | - | Content elements to render in the scroll container. |
|
|
288
321
|
| `className` | `string` | - | Additional class name applied to the semantic scrolling region. |
|
|
289
|
-
| `
|
|
322
|
+
| `depthPerspective` | `number` | `800` | Perspective camera distance in CSS pixels, depth mode only. |
|
|
323
|
+
| `depthSpacing` | `number` | `420` | Distance between consecutive depth planes in CSS pixels. |
|
|
324
|
+
| `direction` | `'away' \| 'down' \| 'left' \| 'right' \| 'toward' \| 'up'` | `'up'` | `left`/`right` infer horizontal; `toward`/`away` infer depth. Explicit `axis` takes precedence. |
|
|
290
325
|
| `draggable` | `boolean` | `true` | Enable mouse/touch drag interaction. |
|
|
291
326
|
| `edgeHoverInputMode` | `'external' \| 'hybrid'` | `'hybrid'` | Use intrinsic pointer sensing plus the imperative bridge, or external imperative ownership only for composed drag surfaces. |
|
|
292
327
|
| `edgeHoverScroll` | `boolean` | `false` | Only auto-scroll while hovering the active axis boundary: top/bottom for vertical, left/right for horizontal. A short ingress feather removes sensor chatter; velocity then accelerates through the physical edge and reaches its bounded cap one activation-zone width beyond it. |
|
|
@@ -301,6 +336,7 @@ function MinimalEdgeHoverExamples() {
|
|
|
301
336
|
| `onResume` | `() => void` | - | Fired when interaction-paused autonomous scrolling resumes. |
|
|
302
337
|
| `onScroll` | `(position: number) => void` | - | Fired on every position change with the current scroll position. |
|
|
303
338
|
| `pauseOnInteraction` | `boolean` | `true` | Pause auto-scroll during user interaction. |
|
|
339
|
+
| `reducedMotion` | `boolean` | `false` | Request reduced motion in addition to the OS preference. Stops depth auto-scroll and momentum. |
|
|
304
340
|
| `resumeDelay` | `number` | `2000` | Milliseconds before auto-scroll resumes after interaction. |
|
|
305
341
|
| `speed` | `number` | `0.5` | Baseline auto-scroll speed in pixels per frame at 60fps. Edge-hover mode preserves this baseline inside the active zone and adds bounded depth acceleration. |
|
|
306
342
|
| `wheelEnabled` | `boolean` | `true` | Enable mouse wheel scrolling. |
|
|
@@ -374,11 +410,12 @@ External drag owners should retain pointer capture or route global pointer coord
|
|
|
374
410
|
|
|
375
411
|
## Runtime and accessibility contract
|
|
376
412
|
|
|
377
|
-
-
|
|
413
|
+
- Planar duplicate loop groups are inert, `aria-hidden`, presentation-only, non-interactive, and pointer-disabled. Depth children are never duplicated.
|
|
414
|
+
- Hidden pages pause animation and resume without consuming elapsed hidden time. Inactive instances also relinquish gesture ownership and discard momentum.
|
|
378
415
|
- Mouse and touch global listeners are installed only during active drag gestures and are removed on release, cancel, blur, unmount, or prop changes.
|
|
379
416
|
- Edge-hover mode does not continuously request animation frames while idle; it wakes when the pointer enters an edge, wheel input arrives, momentum exists, or auto-scroll resumes.
|
|
380
417
|
- `wheelMode="axis"` lets cross-axis page scroll pass through. `wheelMode="capture"` intentionally lets Sway own every wheel event it receives.
|
|
381
418
|
|
|
382
419
|
## License
|
|
383
420
|
|
|
384
|
-
This package is licensed under the [MIT License](
|
|
421
|
+
This package is licensed under the [MIT License](https://github.com/lafittemehdy/sway/blob/main/LICENSE).
|
package/dist/ReactSway.d.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
import { type ReactNode } from 'react';
|
|
5
5
|
import { type SwayWheelMode } from './wheel-input';
|
|
6
6
|
export type { SwayWheelMode } from './wheel-input';
|
|
7
|
-
export type SwayAxis = 'horizontal' | 'vertical';
|
|
8
|
-
export type SwayDirection = 'down' | 'left' | 'right' | 'up';
|
|
7
|
+
export type SwayAxis = 'depth' | 'horizontal' | 'vertical';
|
|
8
|
+
export type SwayDirection = 'away' | 'down' | 'left' | 'right' | 'toward' | 'up';
|
|
9
9
|
export type SwayEdgeHoverInputMode = 'external' | 'hybrid';
|
|
10
10
|
export interface ReactSwayHandle {
|
|
11
11
|
/**
|
|
@@ -36,9 +36,11 @@ export interface ReactSwayHandle {
|
|
|
36
36
|
* Props for the ReactSway infinite scrolling component.
|
|
37
37
|
*/
|
|
38
38
|
export interface ReactSwayProps {
|
|
39
|
+
/** Suspend Sway animation and input while keeping content and scroll position mounted. @default true */
|
|
40
|
+
active?: boolean;
|
|
39
41
|
/** Accessible name for the scrolling region. */
|
|
40
42
|
ariaLabel?: string;
|
|
41
|
-
/** Scroll axis.
|
|
43
|
+
/** Scroll axis. Left/right imply horizontal; toward/away imply depth. @default 'vertical' */
|
|
42
44
|
axis?: SwayAxis;
|
|
43
45
|
/** Enable/disable auto-scrolling. @default true */
|
|
44
46
|
autoScroll?: boolean;
|
|
@@ -46,6 +48,10 @@ export interface ReactSwayProps {
|
|
|
46
48
|
children: ReactNode;
|
|
47
49
|
/** Additional class name applied to the semantic scrolling region. */
|
|
48
50
|
className?: string;
|
|
51
|
+
/** Camera distance in CSS pixels for depth mode. Invalid lengths use the default. @default 800 */
|
|
52
|
+
depthPerspective?: number;
|
|
53
|
+
/** Distance in CSS pixels between depth planes. Invalid lengths use the default. @default 420 */
|
|
54
|
+
depthSpacing?: number;
|
|
49
55
|
/** Auto-scroll direction. @default 'up' */
|
|
50
56
|
direction?: SwayDirection;
|
|
51
57
|
/** Enable mouse/touch drag interaction. @default true */
|
|
@@ -83,6 +89,8 @@ export interface ReactSwayProps {
|
|
|
83
89
|
onScroll?: (position: number) => void;
|
|
84
90
|
/** Pause auto-scroll during user interaction. @default true */
|
|
85
91
|
pauseOnInteraction?: boolean;
|
|
92
|
+
/** Request reduced motion in addition to the system preference; false never overrides the system. @default false */
|
|
93
|
+
reducedMotion?: boolean;
|
|
86
94
|
/** Milliseconds before auto-scroll resumes after interaction. @default 2000 */
|
|
87
95
|
resumeDelay?: number;
|
|
88
96
|
/** Baseline auto-scroll speed in pixels per frame at 60fps. @default 0.5 */
|
|
@@ -106,11 +114,11 @@ export interface ReactSwayProps {
|
|
|
106
114
|
* Renders children in a continuously looping scroll area with support for
|
|
107
115
|
* auto-scrolling, mouse drag, touch swipe, wheel, keyboard interactions,
|
|
108
116
|
* and optional edge-hover scrolling.
|
|
109
|
-
*
|
|
117
|
+
* Planar content is duplicated to create a seamless loop effect. Duplicate content
|
|
110
118
|
* is wrapped in `<aside>` elements with `aria-hidden="true"` for accessibility.
|
|
111
119
|
*
|
|
112
|
-
*
|
|
113
|
-
* and
|
|
120
|
+
* Reduced motion stops depth auto-scroll, lowers planar auto-scroll speed,
|
|
121
|
+
* and disables momentum effects.
|
|
114
122
|
*
|
|
115
123
|
* @example
|
|
116
124
|
* ```tsx
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Maps one scroll phase onto periodic depth planes without crossing the perspective camera. */
|
|
2
|
+
export declare const DEFAULT_DEPTH_PERSPECTIVE = 800;
|
|
3
|
+
export declare const DEFAULT_DEPTH_SPACING = 420;
|
|
4
|
+
/** Bounds pixel lengths so periods and fade denominators remain finite and nonzero. */
|
|
5
|
+
export declare function normalizeDepthLength(value: number, fallback: number): number;
|
|
6
|
+
export declare function getDepthItemState(position: number, index: number, count: number, perspective?: number, spacing?: number): {
|
|
7
|
+
opacity: number;
|
|
8
|
+
z: number;
|
|
9
|
+
};
|
package/dist/index.cjs
CHANGED
|
@@ -1,10 +1,48 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
// \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
2
23
|
let react = require("react");
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
*/
|
|
24
|
+
react = __toESM(react, 1);
|
|
25
|
+
// src/depth-position.ts
|
|
26
|
+
var DEFAULT_DEPTH_PERSPECTIVE = 800;
|
|
27
|
+
var DEFAULT_DEPTH_SPACING = 420;
|
|
28
|
+
/** Bounds pixel lengths so periods and fade denominators remain finite and nonzero. */
|
|
29
|
+
function normalizeDepthLength(value, fallback) {
|
|
30
|
+
return Number.isFinite(value) && value > 0 ? Math.min(Number.MAX_SAFE_INTEGER, Math.max(1, value)) : fallback;
|
|
31
|
+
}
|
|
32
|
+
function getDepthItemState(position, index, count, perspective = 800, spacing = 420) {
|
|
33
|
+
if (count <= 0) return {
|
|
34
|
+
opacity: 0,
|
|
35
|
+
z: 0
|
|
36
|
+
};
|
|
37
|
+
const period = count * spacing;
|
|
38
|
+
const phase = ((index * spacing + position) % period + period) % period;
|
|
39
|
+
const fade = Math.min(1, Math.min(phase, period - phase) / (spacing * .45));
|
|
40
|
+
return {
|
|
41
|
+
opacity: fade * fade * (3 - 2 * fade),
|
|
42
|
+
z: perspective * .7 - phase
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// src/edge-hover-velocity.ts
|
|
8
46
|
/** Default edge-hover activation thickness in pixels. */
|
|
9
47
|
var DEFAULT_EDGE_HOVER_SIZE = 96;
|
|
10
48
|
/** Default maximum edge-hover speed relative to the configured base speed. */
|
|
@@ -79,9 +117,7 @@ function getEdgeHoverIntensity(pointerPosition, visibleInterval, edgeHoverSize)
|
|
|
79
117
|
}
|
|
80
118
|
return 0;
|
|
81
119
|
}
|
|
82
|
-
|
|
83
|
-
//#region src/wheel-input.ts
|
|
84
|
-
/** Fallback pixel height for wheel events reported in line units. */
|
|
120
|
+
// src/wheel-input.ts
|
|
85
121
|
var WHEEL_LINE_HEIGHT_FALLBACK_PX = 16;
|
|
86
122
|
/** WheelEvent deltaMode value for line-based deltas. */
|
|
87
123
|
var WHEEL_MODE_LINE = 1;
|
|
@@ -128,11 +164,7 @@ function getOwnedWheelDelta(event, container, isHorizontal, wheelMode) {
|
|
|
128
164
|
if (wheelMode === "axis" && absDeltaX > absDeltaY) return null;
|
|
129
165
|
return deltaY;
|
|
130
166
|
}
|
|
131
|
-
|
|
132
|
-
//#region src/ReactSway.tsx
|
|
133
|
-
/**
|
|
134
|
-
* Core ReactSway component implementing axis-aware infinite scrolling interactions.
|
|
135
|
-
*/
|
|
167
|
+
// src/ReactSway.tsx
|
|
136
168
|
/** Velocity applied per arrow key press (pixels). */
|
|
137
169
|
var ARROW_KEY_VELOCITY = 15;
|
|
138
170
|
/** Default friction coefficient applied to velocity each frame. */
|
|
@@ -186,11 +218,11 @@ function getVisibleEdgeInterval(rect, isHorizontal) {
|
|
|
186
218
|
* Renders children in a continuously looping scroll area with support for
|
|
187
219
|
* auto-scrolling, mouse drag, touch swipe, wheel, keyboard interactions,
|
|
188
220
|
* and optional edge-hover scrolling.
|
|
189
|
-
*
|
|
221
|
+
* Planar content is duplicated to create a seamless loop effect. Duplicate content
|
|
190
222
|
* is wrapped in `<aside>` elements with `aria-hidden="true"` for accessibility.
|
|
191
223
|
*
|
|
192
|
-
*
|
|
193
|
-
* and
|
|
224
|
+
* Reduced motion stops depth auto-scroll, lowers planar auto-scroll speed,
|
|
225
|
+
* and disables momentum effects.
|
|
194
226
|
*
|
|
195
227
|
* @example
|
|
196
228
|
* ```tsx
|
|
@@ -200,8 +232,14 @@ function getVisibleEdgeInterval(rect, isHorizontal) {
|
|
|
200
232
|
* </ReactSway>
|
|
201
233
|
* ```
|
|
202
234
|
*/
|
|
203
|
-
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) {
|
|
204
|
-
const
|
|
235
|
+
var ReactSway = (0, react.forwardRef)(function ReactSway({ active = true, ariaLabel = "Scrollable content", axis, autoScroll = true, children, className, depthPerspective = 800, depthSpacing = 420, direction = "up", draggable = true, edgeHoverInputMode = "hybrid", edgeHoverScroll = false, edgeHoverSize = 96, edgeHoverSpeedMultiplier = 6, friction = DEFAULT_FRICTION, keyboard = true, lazy = true, lazyRootMargin = DEFAULT_LAZY_ROOT_MARGIN, lazyThreshold = DEFAULT_LAZY_THRESHOLD, onPause, onResume, onScroll, pauseOnInteraction = true, reducedMotion = false, resumeDelay = DEFAULT_RESUME_DELAY, speed = DEFAULT_SPEED, wheelEnabled = true, wheelMode = "axis" }, ref) {
|
|
236
|
+
const scrollAxis = (0, react.useMemo)(() => axis ?? (direction === "toward" || direction === "away" ? "depth" : direction === "left" || direction === "right" ? "horizontal" : "vertical"), [axis, direction]);
|
|
237
|
+
const isHorizontal = scrollAxis === "horizontal";
|
|
238
|
+
const isDepth = scrollAxis === "depth";
|
|
239
|
+
const depthChildren = (0, react.useMemo)(() => react.Children.toArray(children), [children]);
|
|
240
|
+
const enabled = active && (!isDepth || depthChildren.length > 0);
|
|
241
|
+
const normalizedDepthPerspective = normalizeDepthLength(depthPerspective, 800);
|
|
242
|
+
const normalizedDepthSpacing = normalizeDepthLength(depthSpacing, 420);
|
|
205
243
|
const normalizedEdgeHoverSize = (0, react.useMemo)(() => Number.isFinite(edgeHoverSize) ? Math.max(1, edgeHoverSize) : 96, [edgeHoverSize]);
|
|
206
244
|
const normalizedEdgeHoverSpeedMultiplier = (0, react.useMemo)(() => normalizeEdgeHoverSpeedMultiplier(edgeHoverSpeedMultiplier), [edgeHoverSpeedMultiplier]);
|
|
207
245
|
const normalizedFriction = (0, react.useMemo)(() => Number.isFinite(friction) ? Math.min(Math.max(friction, 0), 1) : DEFAULT_FRICTION, [friction]);
|
|
@@ -209,17 +247,22 @@ var ReactSway = (0, react.forwardRef)(function ReactSway({ ariaLabel = "Scrollab
|
|
|
209
247
|
const normalizedResumeDelay = (0, react.useMemo)(() => Number.isFinite(resumeDelay) ? Math.max(0, resumeDelay) : DEFAULT_RESUME_DELAY, [resumeDelay]);
|
|
210
248
|
const normalizedSpeed = (0, react.useMemo)(() => Number.isFinite(speed) ? Math.max(0, speed) : DEFAULT_SPEED, [speed]);
|
|
211
249
|
const touchAction = (0, react.useMemo)(() => {
|
|
212
|
-
if (!draggable) return "auto";
|
|
250
|
+
if (!enabled || !draggable) return "auto";
|
|
213
251
|
return isHorizontal ? "pan-y" : "pan-x";
|
|
214
|
-
}, [
|
|
215
|
-
|
|
252
|
+
}, [
|
|
253
|
+
draggable,
|
|
254
|
+
enabled,
|
|
255
|
+
isHorizontal
|
|
256
|
+
]);
|
|
257
|
+
const overscrollBehavior = enabled && wheelEnabled && wheelMode === "capture" ? "contain" : "auto";
|
|
216
258
|
const [isPaused, setIsPaused] = (0, react.useState)(false);
|
|
217
259
|
const [isTabActive, setIsTabActive] = (0, react.useState)(() => typeof document === "undefined" || !document.hidden);
|
|
218
260
|
const [loopSegmentCount, setLoopSegmentCount] = (0, react.useState)(MIN_LOOP_SEGMENTS);
|
|
219
|
-
const [
|
|
261
|
+
const [systemReducedMotion, setSystemReducedMotion] = (0, react.useState)(() => {
|
|
220
262
|
if (typeof window === "undefined") return false;
|
|
221
263
|
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
222
264
|
});
|
|
265
|
+
const prefersReducedMotion = systemReducedMotion || reducedMotion;
|
|
223
266
|
const animationFrameRef = (0, react.useRef)(null);
|
|
224
267
|
const animationLoopRef = (0, react.useRef)(null);
|
|
225
268
|
const autoScrollRef = (0, react.useRef)({
|
|
@@ -266,7 +309,7 @@ var ReactSway = (0, react.forwardRef)(function ReactSway({ ariaLabel = "Scrollab
|
|
|
266
309
|
if (typeof window === "undefined") return;
|
|
267
310
|
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
268
311
|
const handleChange = (e) => {
|
|
269
|
-
|
|
312
|
+
setSystemReducedMotion(e.matches);
|
|
270
313
|
};
|
|
271
314
|
mediaQuery.addEventListener("change", handleChange);
|
|
272
315
|
return () => {
|
|
@@ -306,11 +349,28 @@ var ReactSway = (0, react.forwardRef)(function ReactSway({ ariaLabel = "Scrollab
|
|
|
306
349
|
}, [isHorizontal]);
|
|
307
350
|
const renderPosition = (0, react.useCallback)((rawPosition) => {
|
|
308
351
|
if (!containerRef.current) return;
|
|
352
|
+
if (isDepth) {
|
|
353
|
+
containerRef.current.style.transform = "translate3d(0, 0, 0)";
|
|
354
|
+
const items = originalGroupRef.current?.children;
|
|
355
|
+
if (!items) return;
|
|
356
|
+
Array.from(items).forEach((item, index) => {
|
|
357
|
+
const { opacity, z } = getDepthItemState(rawPosition, index, items.length, normalizedDepthPerspective, normalizedDepthSpacing);
|
|
358
|
+
const element = item;
|
|
359
|
+
element.style.transform = `translate3d(0, 0, ${z}px)`;
|
|
360
|
+
element.style.opacity = String(opacity);
|
|
361
|
+
});
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
309
364
|
const currentLoopPoint = loopPointRef.current;
|
|
310
365
|
let visualPosition = rawPosition % (currentLoopPoint || 1);
|
|
311
366
|
if (visualPosition > 0 && currentLoopPoint > 0) visualPosition -= currentLoopPoint;
|
|
312
367
|
containerRef.current.style.transform = isHorizontal ? `translate3d(${visualPosition}px, 0, 0)` : `translate3d(0, ${visualPosition}px, 0)`;
|
|
313
|
-
}, [
|
|
368
|
+
}, [
|
|
369
|
+
isDepth,
|
|
370
|
+
isHorizontal,
|
|
371
|
+
normalizedDepthPerspective,
|
|
372
|
+
normalizedDepthSpacing
|
|
373
|
+
]);
|
|
314
374
|
const commitPosition = (0, react.useCallback)((nextPosition) => {
|
|
315
375
|
if (positionRef.current === nextPosition) return;
|
|
316
376
|
positionRef.current = nextPosition;
|
|
@@ -331,11 +391,14 @@ var ReactSway = (0, react.forwardRef)(function ReactSway({ ariaLabel = "Scrollab
|
|
|
331
391
|
}, [draggable]);
|
|
332
392
|
const recalculateLoopPoint = (0, react.useCallback)(() => {
|
|
333
393
|
if (!containerRef.current || !originalGroupRef.current) return;
|
|
334
|
-
const originalContentSize = isHorizontal ? originalGroupRef.current.scrollWidth : originalGroupRef.current.scrollHeight;
|
|
335
|
-
if (originalContentSize <= 0)
|
|
394
|
+
const originalContentSize = isDepth ? depthChildren.length * normalizedDepthSpacing : isHorizontal ? originalGroupRef.current.scrollWidth : originalGroupRef.current.scrollHeight;
|
|
395
|
+
if (originalContentSize <= 0) {
|
|
396
|
+
if (isDepth) loopPointRef.current = 0;
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
336
399
|
const viewport = containerRef.current.parentElement ?? containerRef.current;
|
|
337
400
|
const viewportSize = isHorizontal ? Math.max(viewport.clientWidth, 1) : Math.max(viewport.clientHeight, 1);
|
|
338
|
-
const nextLoopSegmentCount = Math.max(MIN_LOOP_SEGMENTS, Math.ceil(viewportSize / originalContentSize) + 1);
|
|
401
|
+
const nextLoopSegmentCount = isDepth ? 1 : Math.max(MIN_LOOP_SEGMENTS, Math.ceil(viewportSize / originalContentSize) + 1);
|
|
339
402
|
const nextLoopPoint = originalContentSize;
|
|
340
403
|
const previousMeasurement = lastLoopMeasurementRef.current;
|
|
341
404
|
const nextMeasurement = {
|
|
@@ -348,7 +411,13 @@ var ReactSway = (0, react.forwardRef)(function ReactSway({ ariaLabel = "Scrollab
|
|
|
348
411
|
setLoopSegmentCount((previousLoopSegmentCount) => previousLoopSegmentCount === nextLoopSegmentCount ? previousLoopSegmentCount : nextLoopSegmentCount);
|
|
349
412
|
loopPointRef.current = nextLoopPoint;
|
|
350
413
|
renderPosition(positionRef.current);
|
|
351
|
-
}, [
|
|
414
|
+
}, [
|
|
415
|
+
depthChildren.length,
|
|
416
|
+
isDepth,
|
|
417
|
+
isHorizontal,
|
|
418
|
+
normalizedDepthSpacing,
|
|
419
|
+
renderPosition
|
|
420
|
+
]);
|
|
352
421
|
(0, react.useEffect)(() => {
|
|
353
422
|
autoScrollRef.current.desired = autoScroll;
|
|
354
423
|
if (!autoScroll) {
|
|
@@ -373,6 +442,7 @@ var ReactSway = (0, react.forwardRef)(function ReactSway({ ariaLabel = "Scrollab
|
|
|
373
442
|
scheduleLoopPointRecalculation();
|
|
374
443
|
}, [
|
|
375
444
|
children,
|
|
445
|
+
isDepth,
|
|
376
446
|
isHorizontal,
|
|
377
447
|
scheduleLoopPointRecalculation
|
|
378
448
|
]);
|
|
@@ -427,7 +497,14 @@ var ReactSway = (0, react.forwardRef)(function ReactSway({ ariaLabel = "Scrollab
|
|
|
427
497
|
touchDragListenerCleanupRef.current = null;
|
|
428
498
|
}, []);
|
|
429
499
|
const handleKeyDown = (0, react.useCallback)((e) => {
|
|
430
|
-
if (!keyboard || isInteractiveTarget(e.target)) return;
|
|
500
|
+
if (!enabled || !keyboard || isInteractiveTarget(e.target)) return;
|
|
501
|
+
const arrowDelta = isHorizontal ? e.key === "ArrowLeft" ? ARROW_KEY_VELOCITY : e.key === "ArrowRight" ? -15 : 0 : e.key === "ArrowUp" ? ARROW_KEY_VELOCITY : e.key === "ArrowDown" ? -15 : 0;
|
|
502
|
+
if (prefersReducedMotion && arrowDelta !== 0) {
|
|
503
|
+
e.preventDefault();
|
|
504
|
+
commitPosition(wrapPosition(positionRef.current + arrowDelta));
|
|
505
|
+
velocityRef.current = 0;
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
431
508
|
switch (e.key) {
|
|
432
509
|
case " ":
|
|
433
510
|
e.preventDefault();
|
|
@@ -478,12 +555,15 @@ var ReactSway = (0, react.forwardRef)(function ReactSway({ ariaLabel = "Scrollab
|
|
|
478
555
|
startAnimationLoop();
|
|
479
556
|
}, [
|
|
480
557
|
commitPosition,
|
|
558
|
+
enabled,
|
|
481
559
|
isHorizontal,
|
|
482
560
|
keyboard,
|
|
483
561
|
pauseAutoScroll,
|
|
562
|
+
prefersReducedMotion,
|
|
484
563
|
scheduleAutoScrollResume,
|
|
485
564
|
startAnimationLoop,
|
|
486
|
-
togglePause
|
|
565
|
+
togglePause,
|
|
566
|
+
wrapPosition
|
|
487
567
|
]);
|
|
488
568
|
const handleMouseMove = (0, react.useCallback)((e) => {
|
|
489
569
|
if (!isDraggingRef.current) return;
|
|
@@ -541,7 +621,7 @@ var ReactSway = (0, react.forwardRef)(function ReactSway({ ariaLabel = "Scrollab
|
|
|
541
621
|
handleMouseUp
|
|
542
622
|
]);
|
|
543
623
|
const handleMouseDown = (0, react.useCallback)((e) => {
|
|
544
|
-
if (!draggable || e.button !== 0 || isInteractiveTarget(e.target)) return;
|
|
624
|
+
if (!enabled || !draggable || e.button !== 0 || isInteractiveTarget(e.target)) return;
|
|
545
625
|
e.preventDefault();
|
|
546
626
|
containerRef.current?.focus();
|
|
547
627
|
setDraggingState(true);
|
|
@@ -554,6 +634,7 @@ var ReactSway = (0, react.forwardRef)(function ReactSway({ ariaLabel = "Scrollab
|
|
|
554
634
|
}, [
|
|
555
635
|
attachMouseDragListeners,
|
|
556
636
|
draggable,
|
|
637
|
+
enabled,
|
|
557
638
|
pauseAutoScroll,
|
|
558
639
|
setDraggingState,
|
|
559
640
|
startAnimationLoop
|
|
@@ -577,6 +658,7 @@ var ReactSway = (0, react.forwardRef)(function ReactSway({ ariaLabel = "Scrollab
|
|
|
577
658
|
if (touchInteractionStateRef.current === "idle" || touchInteractionStateRef.current === "ignored") return;
|
|
578
659
|
if (e.touches.length !== 1) return;
|
|
579
660
|
const touch = e.touches[0];
|
|
661
|
+
if (!touch) return;
|
|
580
662
|
if (touchInteractionStateRef.current === "pending") {
|
|
581
663
|
const totalDeltaX = touch.clientX - touchStartXRef.current;
|
|
582
664
|
const totalDeltaY = touch.clientY - touchStartYRef.current;
|
|
@@ -621,18 +703,24 @@ var ReactSway = (0, react.forwardRef)(function ReactSway({ ariaLabel = "Scrollab
|
|
|
621
703
|
handleTouchMove
|
|
622
704
|
]);
|
|
623
705
|
const handleTouchStart = (0, react.useCallback)((e) => {
|
|
624
|
-
if (!draggable || e.touches.length !== 1 || isInteractiveTarget(e.target)) return;
|
|
706
|
+
if (!enabled || !draggable || e.touches.length !== 1 || isInteractiveTarget(e.target)) return;
|
|
707
|
+
const touch = e.touches[0];
|
|
708
|
+
if (!touch) return;
|
|
625
709
|
containerRef.current?.focus();
|
|
626
710
|
touchInteractionStateRef.current = "pending";
|
|
627
|
-
lastTouchXRef.current =
|
|
628
|
-
lastTouchYRef.current =
|
|
629
|
-
touchStartXRef.current =
|
|
630
|
-
touchStartYRef.current =
|
|
711
|
+
lastTouchXRef.current = touch.clientX;
|
|
712
|
+
lastTouchYRef.current = touch.clientY;
|
|
713
|
+
touchStartXRef.current = touch.clientX;
|
|
714
|
+
touchStartYRef.current = touch.clientY;
|
|
631
715
|
velocityRef.current = 0;
|
|
632
716
|
attachTouchDragListeners();
|
|
633
|
-
}, [
|
|
717
|
+
}, [
|
|
718
|
+
attachTouchDragListeners,
|
|
719
|
+
draggable,
|
|
720
|
+
enabled
|
|
721
|
+
]);
|
|
634
722
|
const handleWheel = (0, react.useCallback)((e) => {
|
|
635
|
-
if (!wheelEnabled || e.defaultPrevented) return false;
|
|
723
|
+
if (!enabled || !wheelEnabled || e.defaultPrevented) return false;
|
|
636
724
|
const currentContainer = e.currentTarget instanceof HTMLElement ? e.currentTarget : containerRef.current;
|
|
637
725
|
if (!currentContainer) return false;
|
|
638
726
|
const ownedDelta = getOwnedWheelDelta(e, currentContainer, isHorizontal, wheelMode);
|
|
@@ -649,6 +737,7 @@ var ReactSway = (0, react.forwardRef)(function ReactSway({ ariaLabel = "Scrollab
|
|
|
649
737
|
return true;
|
|
650
738
|
}, [
|
|
651
739
|
commitPosition,
|
|
740
|
+
enabled,
|
|
652
741
|
isHorizontal,
|
|
653
742
|
pauseAutoScroll,
|
|
654
743
|
scheduleAutoScrollResume,
|
|
@@ -658,7 +747,7 @@ var ReactSway = (0, react.forwardRef)(function ReactSway({ ariaLabel = "Scrollab
|
|
|
658
747
|
wrapPosition
|
|
659
748
|
]);
|
|
660
749
|
const applyEdgeHoverPoint = (0, react.useCallback)((point) => {
|
|
661
|
-
if (!edgeHoverScroll) return false;
|
|
750
|
+
if (!enabled || !edgeHoverScroll) return false;
|
|
662
751
|
const viewport = containerRef.current?.parentElement ?? containerRef.current;
|
|
663
752
|
if (!viewport) return false;
|
|
664
753
|
const visibleInterval = readVisibleEdgeInterval(viewport);
|
|
@@ -671,6 +760,7 @@ var ReactSway = (0, react.forwardRef)(function ReactSway({ ariaLabel = "Scrollab
|
|
|
671
760
|
return true;
|
|
672
761
|
}, [
|
|
673
762
|
edgeHoverScroll,
|
|
763
|
+
enabled,
|
|
674
764
|
isHorizontal,
|
|
675
765
|
normalizedEdgeHoverSize,
|
|
676
766
|
readVisibleEdgeInterval,
|
|
@@ -707,7 +797,7 @@ var ReactSway = (0, react.forwardRef)(function ReactSway({ ariaLabel = "Scrollab
|
|
|
707
797
|
}, [setEdgeHoverIntensity]);
|
|
708
798
|
(0, react.useEffect)(() => {
|
|
709
799
|
const currentContainer = containerRef.current;
|
|
710
|
-
if (!currentContainer) return;
|
|
800
|
+
if (!currentContainer || !enabled) return;
|
|
711
801
|
if (draggable) {
|
|
712
802
|
currentContainer.addEventListener("mousedown", handleMouseDown);
|
|
713
803
|
currentContainer.addEventListener("touchstart", handleTouchStart, { passive: true });
|
|
@@ -745,6 +835,7 @@ var ReactSway = (0, react.forwardRef)(function ReactSway({ ariaLabel = "Scrollab
|
|
|
745
835
|
detachMouseDragListeners,
|
|
746
836
|
detachTouchDragListeners,
|
|
747
837
|
draggable,
|
|
838
|
+
enabled,
|
|
748
839
|
edgeHoverScroll,
|
|
749
840
|
edgeHoverInputMode,
|
|
750
841
|
clearEdgeHover,
|
|
@@ -755,6 +846,24 @@ var ReactSway = (0, react.forwardRef)(function ReactSway({ ariaLabel = "Scrollab
|
|
|
755
846
|
handleWheel,
|
|
756
847
|
wheelEnabled
|
|
757
848
|
]);
|
|
849
|
+
(0, react.useEffect)(() => {
|
|
850
|
+
if (enabled) return;
|
|
851
|
+
clearInactivityTimer();
|
|
852
|
+
clearEdgeHover();
|
|
853
|
+
detachMouseDragListeners();
|
|
854
|
+
detachTouchDragListeners();
|
|
855
|
+
touchInteractionStateRef.current = "idle";
|
|
856
|
+
setDraggingState(false);
|
|
857
|
+
velocityRef.current = 0;
|
|
858
|
+
autoScrollRef.current.active = autoScrollRef.current.desired;
|
|
859
|
+
}, [
|
|
860
|
+
clearEdgeHover,
|
|
861
|
+
clearInactivityTimer,
|
|
862
|
+
detachMouseDragListeners,
|
|
863
|
+
detachTouchDragListeners,
|
|
864
|
+
enabled,
|
|
865
|
+
setDraggingState
|
|
866
|
+
]);
|
|
758
867
|
(0, react.useEffect)(() => {
|
|
759
868
|
window.addEventListener("resize", scheduleLoopPointRecalculation);
|
|
760
869
|
return () => {
|
|
@@ -803,7 +912,7 @@ var ReactSway = (0, react.forwardRef)(function ReactSway({ ariaLabel = "Scrollab
|
|
|
803
912
|
};
|
|
804
913
|
}, [clearInactivityTimer]);
|
|
805
914
|
(0, react.useEffect)(() => {
|
|
806
|
-
if (!isTabActive || isPaused) {
|
|
915
|
+
if (!enabled || !isTabActive || isPaused) {
|
|
807
916
|
if (animationFrameRef.current !== null) {
|
|
808
917
|
cancelAnimationFrame(animationFrameRef.current);
|
|
809
918
|
animationFrameRef.current = null;
|
|
@@ -812,7 +921,7 @@ var ReactSway = (0, react.forwardRef)(function ReactSway({ ariaLabel = "Scrollab
|
|
|
812
921
|
return;
|
|
813
922
|
}
|
|
814
923
|
lastFrameTimeRef.current = 0;
|
|
815
|
-
const directionMultiplier = direction === "down" || direction === "right" ? 1 : -1;
|
|
924
|
+
const directionMultiplier = direction === "away" || direction === "down" || direction === "right" ? 1 : -1;
|
|
816
925
|
const animate = (currentTime) => {
|
|
817
926
|
let deltaTime = lastFrameTimeRef.current ? (currentTime - lastFrameTimeRef.current) / MS_PER_FRAME_60FPS : 1;
|
|
818
927
|
deltaTime = Math.min(deltaTime, MAX_DELTA_TIME);
|
|
@@ -823,7 +932,7 @@ var ReactSway = (0, react.forwardRef)(function ReactSway({ ariaLabel = "Scrollab
|
|
|
823
932
|
let nextPosition = positionRef.current;
|
|
824
933
|
const effectiveSpeed = prefersReducedMotion ? normalizedSpeed * REDUCED_MOTION_SPEED_FACTOR : normalizedSpeed;
|
|
825
934
|
const edgeHoverMultiplier = edgeHoverScroll ? getEdgeHoverVelocityScale(edgeHoverIntensityRef.current, normalizedEdgeHoverSpeedMultiplier) : directionMultiplier;
|
|
826
|
-
const shouldAutoScroll = (autoScrollRef.current.desired && autoScrollRef.current.active || externalEdgeHoverLeaseRef.current) && !isDraggingRef.current && (!edgeHoverScroll || edgeHoverMultiplier !== 0);
|
|
935
|
+
const shouldAutoScroll = (autoScrollRef.current.desired && autoScrollRef.current.active || externalEdgeHoverLeaseRef.current) && !isDraggingRef.current && !(isDepth && prefersReducedMotion) && (!edgeHoverScroll || edgeHoverMultiplier !== 0);
|
|
827
936
|
if (shouldAutoScroll) {
|
|
828
937
|
const autoScrollDeltaTime = edgeHoverScroll ? Math.min(deltaTime, MAX_EDGE_HOVER_DELTA_TIME) : deltaTime;
|
|
829
938
|
nextPosition += edgeHoverMultiplier * effectiveSpeed * autoScrollDeltaTime;
|
|
@@ -852,6 +961,8 @@ var ReactSway = (0, react.forwardRef)(function ReactSway({ ariaLabel = "Scrollab
|
|
|
852
961
|
commitPosition,
|
|
853
962
|
direction,
|
|
854
963
|
edgeHoverScroll,
|
|
964
|
+
enabled,
|
|
965
|
+
isDepth,
|
|
855
966
|
isPaused,
|
|
856
967
|
isTabActive,
|
|
857
968
|
normalizedFriction,
|
|
@@ -884,56 +995,77 @@ var ReactSway = (0, react.forwardRef)(function ReactSway({ ariaLabel = "Scrollab
|
|
|
884
995
|
lazyRootMargin,
|
|
885
996
|
normalizedLazyThreshold
|
|
886
997
|
]);
|
|
887
|
-
return /* @__PURE__ */
|
|
998
|
+
return /* @__PURE__ */ react.default.createElement("section", {
|
|
888
999
|
"aria-label": ariaLabel,
|
|
889
1000
|
className: `react-sway-container scroller-content${className ? ` ${className}` : ""}`,
|
|
890
1001
|
ref: containerRef,
|
|
891
1002
|
style: {
|
|
892
|
-
cursor: draggable ? isDraggingRef.current ? "grabbing" : "grab" : "default",
|
|
1003
|
+
cursor: enabled && draggable ? isDraggingRef.current ? "grabbing" : "grab" : "default",
|
|
893
1004
|
display: isHorizontal ? "flex" : void 0,
|
|
894
1005
|
flexDirection: isHorizontal ? "row" : void 0,
|
|
895
|
-
height: isHorizontal ? "100%" : "max-content",
|
|
1006
|
+
height: isHorizontal || isDepth ? "100%" : "max-content",
|
|
896
1007
|
MozUserSelect: "none",
|
|
897
1008
|
msUserSelect: "none",
|
|
898
1009
|
minHeight: isHorizontal ? void 0 : "100%",
|
|
899
|
-
overflow: "hidden",
|
|
1010
|
+
overflow: isDepth ? "visible" : "hidden",
|
|
1011
|
+
perspective: isDepth ? normalizedDepthPerspective : void 0,
|
|
900
1012
|
overscrollBehavior,
|
|
901
1013
|
pointerEvents: "auto",
|
|
902
1014
|
position: "absolute",
|
|
903
1015
|
touchAction,
|
|
904
1016
|
transform: "translate3d(0, 0, 0)",
|
|
1017
|
+
transformStyle: isDepth ? "preserve-3d" : void 0,
|
|
905
1018
|
userSelect: "none",
|
|
906
1019
|
WebkitUserSelect: "none",
|
|
907
1020
|
width: isHorizontal ? "max-content" : "100%",
|
|
908
|
-
willChange: "transform",
|
|
1021
|
+
willChange: isDepth ? void 0 : "transform",
|
|
909
1022
|
zIndex: 1
|
|
910
1023
|
},
|
|
911
1024
|
onKeyDown: keyboard ? handleKeyDown : void 0,
|
|
912
|
-
tabIndex: keyboard ? 0 : void 0
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1025
|
+
tabIndex: enabled && keyboard ? 0 : void 0
|
|
1026
|
+
}, /* @__PURE__ */ react.default.createElement("div", {
|
|
1027
|
+
className: "content-group original",
|
|
1028
|
+
ref: originalGroupRef,
|
|
1029
|
+
style: isDepth ? {
|
|
1030
|
+
height: "100%",
|
|
1031
|
+
position: "relative",
|
|
1032
|
+
transformStyle: "preserve-3d"
|
|
1033
|
+
} : isHorizontal ? {
|
|
1034
|
+
display: "flex",
|
|
1035
|
+
flex: "0 0 auto"
|
|
1036
|
+
} : void 0
|
|
1037
|
+
}, isDepth ? depthChildren.map((child, index) => {
|
|
1038
|
+
const { opacity, z } = getDepthItemState(0, index, depthChildren.length, normalizedDepthPerspective, normalizedDepthSpacing);
|
|
1039
|
+
return /* @__PURE__ */ react.default.createElement("div", {
|
|
1040
|
+
className: "react-sway-depth-item",
|
|
1041
|
+
key: typeof child === "object" && "key" in child ? child.key : index,
|
|
1042
|
+
style: {
|
|
1043
|
+
height: 0,
|
|
1044
|
+
left: "50%",
|
|
1045
|
+
opacity,
|
|
1046
|
+
position: "absolute",
|
|
1047
|
+
top: "50%",
|
|
1048
|
+
transform: `translate3d(0, 0, ${z}px)`,
|
|
1049
|
+
width: 0,
|
|
1050
|
+
willChange: "transform, opacity"
|
|
1051
|
+
}
|
|
1052
|
+
}, child);
|
|
1053
|
+
}) : children), Array.from({ length: isDepth ? 0 : loopSegmentCount - 1 }, (_, duplicateIndex) => /* @__PURE__ */ react.default.createElement("aside", {
|
|
1054
|
+
"aria-hidden": "true",
|
|
1055
|
+
className: "content-group duplicate",
|
|
1056
|
+
"data-duplicate": "true",
|
|
1057
|
+
...INERT_ATTRIBUTE,
|
|
1058
|
+
key: `duplicate-${duplicateIndex}`,
|
|
1059
|
+
role: "presentation",
|
|
1060
|
+
style: isHorizontal ? {
|
|
1061
|
+
display: "flex",
|
|
1062
|
+
flex: "0 0 auto",
|
|
1063
|
+
pointerEvents: "none"
|
|
1064
|
+
} : { pointerEvents: "none" }
|
|
1065
|
+
}, children)));
|
|
935
1066
|
});
|
|
936
|
-
|
|
1067
|
+
exports.DEFAULT_DEPTH_PERSPECTIVE = DEFAULT_DEPTH_PERSPECTIVE;
|
|
1068
|
+
exports.DEFAULT_DEPTH_SPACING = DEFAULT_DEPTH_SPACING;
|
|
937
1069
|
exports.DEFAULT_EDGE_HOVER_SIZE = DEFAULT_EDGE_HOVER_SIZE;
|
|
938
1070
|
exports.DEFAULT_EDGE_HOVER_SPEED_MULTIPLIER = DEFAULT_EDGE_HOVER_SPEED_MULTIPLIER;
|
|
939
1071
|
exports.ReactSway = ReactSway;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
* Public package entrypoint for ReactSway exports.
|
|
3
3
|
*/
|
|
4
4
|
export { default as ReactSway } from './ReactSway';
|
|
5
|
+
export { DEFAULT_DEPTH_PERSPECTIVE, DEFAULT_DEPTH_SPACING, } from './depth-position';
|
|
5
6
|
export { DEFAULT_EDGE_HOVER_SIZE, DEFAULT_EDGE_HOVER_SPEED_MULTIPLIER, } from './edge-hover-velocity';
|
|
6
7
|
export type { ReactSwayHandle, ReactSwayProps, SwayAxis, SwayDirection, SwayEdgeHoverInputMode, SwayWheelMode, } from './ReactSway';
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import React, { Children, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, version } from "react";
|
|
2
|
+
// src/depth-position.ts
|
|
3
|
+
var DEFAULT_DEPTH_PERSPECTIVE = 800;
|
|
4
|
+
var DEFAULT_DEPTH_SPACING = 420;
|
|
5
|
+
/** Bounds pixel lengths so periods and fade denominators remain finite and nonzero. */
|
|
6
|
+
function normalizeDepthLength(value, fallback) {
|
|
7
|
+
return Number.isFinite(value) && value > 0 ? Math.min(Number.MAX_SAFE_INTEGER, Math.max(1, value)) : fallback;
|
|
8
|
+
}
|
|
9
|
+
function getDepthItemState(position, index, count, perspective = 800, spacing = 420) {
|
|
10
|
+
if (count <= 0) return {
|
|
11
|
+
opacity: 0,
|
|
12
|
+
z: 0
|
|
13
|
+
};
|
|
14
|
+
const period = count * spacing;
|
|
15
|
+
const phase = ((index * spacing + position) % period + period) % period;
|
|
16
|
+
const fade = Math.min(1, Math.min(phase, period - phase) / (spacing * .45));
|
|
17
|
+
return {
|
|
18
|
+
opacity: fade * fade * (3 - 2 * fade),
|
|
19
|
+
z: perspective * .7 - phase
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
// src/edge-hover-velocity.ts
|
|
7
23
|
/** Default edge-hover activation thickness in pixels. */
|
|
8
24
|
var DEFAULT_EDGE_HOVER_SIZE = 96;
|
|
9
25
|
/** Default maximum edge-hover speed relative to the configured base speed. */
|
|
@@ -78,9 +94,7 @@ function getEdgeHoverIntensity(pointerPosition, visibleInterval, edgeHoverSize)
|
|
|
78
94
|
}
|
|
79
95
|
return 0;
|
|
80
96
|
}
|
|
81
|
-
|
|
82
|
-
//#region src/wheel-input.ts
|
|
83
|
-
/** Fallback pixel height for wheel events reported in line units. */
|
|
97
|
+
// src/wheel-input.ts
|
|
84
98
|
var WHEEL_LINE_HEIGHT_FALLBACK_PX = 16;
|
|
85
99
|
/** WheelEvent deltaMode value for line-based deltas. */
|
|
86
100
|
var WHEEL_MODE_LINE = 1;
|
|
@@ -127,11 +141,7 @@ function getOwnedWheelDelta(event, container, isHorizontal, wheelMode) {
|
|
|
127
141
|
if (wheelMode === "axis" && absDeltaX > absDeltaY) return null;
|
|
128
142
|
return deltaY;
|
|
129
143
|
}
|
|
130
|
-
|
|
131
|
-
//#region src/ReactSway.tsx
|
|
132
|
-
/**
|
|
133
|
-
* Core ReactSway component implementing axis-aware infinite scrolling interactions.
|
|
134
|
-
*/
|
|
144
|
+
// src/ReactSway.tsx
|
|
135
145
|
/** Velocity applied per arrow key press (pixels). */
|
|
136
146
|
var ARROW_KEY_VELOCITY = 15;
|
|
137
147
|
/** Default friction coefficient applied to velocity each frame. */
|
|
@@ -185,11 +195,11 @@ function getVisibleEdgeInterval(rect, isHorizontal) {
|
|
|
185
195
|
* Renders children in a continuously looping scroll area with support for
|
|
186
196
|
* auto-scrolling, mouse drag, touch swipe, wheel, keyboard interactions,
|
|
187
197
|
* and optional edge-hover scrolling.
|
|
188
|
-
*
|
|
198
|
+
* Planar content is duplicated to create a seamless loop effect. Duplicate content
|
|
189
199
|
* is wrapped in `<aside>` elements with `aria-hidden="true"` for accessibility.
|
|
190
200
|
*
|
|
191
|
-
*
|
|
192
|
-
* and
|
|
201
|
+
* Reduced motion stops depth auto-scroll, lowers planar auto-scroll speed,
|
|
202
|
+
* and disables momentum effects.
|
|
193
203
|
*
|
|
194
204
|
* @example
|
|
195
205
|
* ```tsx
|
|
@@ -199,8 +209,14 @@ function getVisibleEdgeInterval(rect, isHorizontal) {
|
|
|
199
209
|
* </ReactSway>
|
|
200
210
|
* ```
|
|
201
211
|
*/
|
|
202
|
-
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) {
|
|
203
|
-
const
|
|
212
|
+
var ReactSway = forwardRef(function ReactSway({ active = true, ariaLabel = "Scrollable content", axis, autoScroll = true, children, className, depthPerspective = 800, depthSpacing = 420, direction = "up", draggable = true, edgeHoverInputMode = "hybrid", edgeHoverScroll = false, edgeHoverSize = 96, edgeHoverSpeedMultiplier = 6, friction = DEFAULT_FRICTION, keyboard = true, lazy = true, lazyRootMargin = DEFAULT_LAZY_ROOT_MARGIN, lazyThreshold = DEFAULT_LAZY_THRESHOLD, onPause, onResume, onScroll, pauseOnInteraction = true, reducedMotion = false, resumeDelay = DEFAULT_RESUME_DELAY, speed = DEFAULT_SPEED, wheelEnabled = true, wheelMode = "axis" }, ref) {
|
|
213
|
+
const scrollAxis = useMemo(() => axis ?? (direction === "toward" || direction === "away" ? "depth" : direction === "left" || direction === "right" ? "horizontal" : "vertical"), [axis, direction]);
|
|
214
|
+
const isHorizontal = scrollAxis === "horizontal";
|
|
215
|
+
const isDepth = scrollAxis === "depth";
|
|
216
|
+
const depthChildren = useMemo(() => Children.toArray(children), [children]);
|
|
217
|
+
const enabled = active && (!isDepth || depthChildren.length > 0);
|
|
218
|
+
const normalizedDepthPerspective = normalizeDepthLength(depthPerspective, 800);
|
|
219
|
+
const normalizedDepthSpacing = normalizeDepthLength(depthSpacing, 420);
|
|
204
220
|
const normalizedEdgeHoverSize = useMemo(() => Number.isFinite(edgeHoverSize) ? Math.max(1, edgeHoverSize) : 96, [edgeHoverSize]);
|
|
205
221
|
const normalizedEdgeHoverSpeedMultiplier = useMemo(() => normalizeEdgeHoverSpeedMultiplier(edgeHoverSpeedMultiplier), [edgeHoverSpeedMultiplier]);
|
|
206
222
|
const normalizedFriction = useMemo(() => Number.isFinite(friction) ? Math.min(Math.max(friction, 0), 1) : DEFAULT_FRICTION, [friction]);
|
|
@@ -208,17 +224,22 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
208
224
|
const normalizedResumeDelay = useMemo(() => Number.isFinite(resumeDelay) ? Math.max(0, resumeDelay) : DEFAULT_RESUME_DELAY, [resumeDelay]);
|
|
209
225
|
const normalizedSpeed = useMemo(() => Number.isFinite(speed) ? Math.max(0, speed) : DEFAULT_SPEED, [speed]);
|
|
210
226
|
const touchAction = useMemo(() => {
|
|
211
|
-
if (!draggable) return "auto";
|
|
227
|
+
if (!enabled || !draggable) return "auto";
|
|
212
228
|
return isHorizontal ? "pan-y" : "pan-x";
|
|
213
|
-
}, [
|
|
214
|
-
|
|
229
|
+
}, [
|
|
230
|
+
draggable,
|
|
231
|
+
enabled,
|
|
232
|
+
isHorizontal
|
|
233
|
+
]);
|
|
234
|
+
const overscrollBehavior = enabled && wheelEnabled && wheelMode === "capture" ? "contain" : "auto";
|
|
215
235
|
const [isPaused, setIsPaused] = useState(false);
|
|
216
236
|
const [isTabActive, setIsTabActive] = useState(() => typeof document === "undefined" || !document.hidden);
|
|
217
237
|
const [loopSegmentCount, setLoopSegmentCount] = useState(MIN_LOOP_SEGMENTS);
|
|
218
|
-
const [
|
|
238
|
+
const [systemReducedMotion, setSystemReducedMotion] = useState(() => {
|
|
219
239
|
if (typeof window === "undefined") return false;
|
|
220
240
|
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
221
241
|
});
|
|
242
|
+
const prefersReducedMotion = systemReducedMotion || reducedMotion;
|
|
222
243
|
const animationFrameRef = useRef(null);
|
|
223
244
|
const animationLoopRef = useRef(null);
|
|
224
245
|
const autoScrollRef = useRef({
|
|
@@ -265,7 +286,7 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
265
286
|
if (typeof window === "undefined") return;
|
|
266
287
|
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
267
288
|
const handleChange = (e) => {
|
|
268
|
-
|
|
289
|
+
setSystemReducedMotion(e.matches);
|
|
269
290
|
};
|
|
270
291
|
mediaQuery.addEventListener("change", handleChange);
|
|
271
292
|
return () => {
|
|
@@ -305,11 +326,28 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
305
326
|
}, [isHorizontal]);
|
|
306
327
|
const renderPosition = useCallback((rawPosition) => {
|
|
307
328
|
if (!containerRef.current) return;
|
|
329
|
+
if (isDepth) {
|
|
330
|
+
containerRef.current.style.transform = "translate3d(0, 0, 0)";
|
|
331
|
+
const items = originalGroupRef.current?.children;
|
|
332
|
+
if (!items) return;
|
|
333
|
+
Array.from(items).forEach((item, index) => {
|
|
334
|
+
const { opacity, z } = getDepthItemState(rawPosition, index, items.length, normalizedDepthPerspective, normalizedDepthSpacing);
|
|
335
|
+
const element = item;
|
|
336
|
+
element.style.transform = `translate3d(0, 0, ${z}px)`;
|
|
337
|
+
element.style.opacity = String(opacity);
|
|
338
|
+
});
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
308
341
|
const currentLoopPoint = loopPointRef.current;
|
|
309
342
|
let visualPosition = rawPosition % (currentLoopPoint || 1);
|
|
310
343
|
if (visualPosition > 0 && currentLoopPoint > 0) visualPosition -= currentLoopPoint;
|
|
311
344
|
containerRef.current.style.transform = isHorizontal ? `translate3d(${visualPosition}px, 0, 0)` : `translate3d(0, ${visualPosition}px, 0)`;
|
|
312
|
-
}, [
|
|
345
|
+
}, [
|
|
346
|
+
isDepth,
|
|
347
|
+
isHorizontal,
|
|
348
|
+
normalizedDepthPerspective,
|
|
349
|
+
normalizedDepthSpacing
|
|
350
|
+
]);
|
|
313
351
|
const commitPosition = useCallback((nextPosition) => {
|
|
314
352
|
if (positionRef.current === nextPosition) return;
|
|
315
353
|
positionRef.current = nextPosition;
|
|
@@ -330,11 +368,14 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
330
368
|
}, [draggable]);
|
|
331
369
|
const recalculateLoopPoint = useCallback(() => {
|
|
332
370
|
if (!containerRef.current || !originalGroupRef.current) return;
|
|
333
|
-
const originalContentSize = isHorizontal ? originalGroupRef.current.scrollWidth : originalGroupRef.current.scrollHeight;
|
|
334
|
-
if (originalContentSize <= 0)
|
|
371
|
+
const originalContentSize = isDepth ? depthChildren.length * normalizedDepthSpacing : isHorizontal ? originalGroupRef.current.scrollWidth : originalGroupRef.current.scrollHeight;
|
|
372
|
+
if (originalContentSize <= 0) {
|
|
373
|
+
if (isDepth) loopPointRef.current = 0;
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
335
376
|
const viewport = containerRef.current.parentElement ?? containerRef.current;
|
|
336
377
|
const viewportSize = isHorizontal ? Math.max(viewport.clientWidth, 1) : Math.max(viewport.clientHeight, 1);
|
|
337
|
-
const nextLoopSegmentCount = Math.max(MIN_LOOP_SEGMENTS, Math.ceil(viewportSize / originalContentSize) + 1);
|
|
378
|
+
const nextLoopSegmentCount = isDepth ? 1 : Math.max(MIN_LOOP_SEGMENTS, Math.ceil(viewportSize / originalContentSize) + 1);
|
|
338
379
|
const nextLoopPoint = originalContentSize;
|
|
339
380
|
const previousMeasurement = lastLoopMeasurementRef.current;
|
|
340
381
|
const nextMeasurement = {
|
|
@@ -347,7 +388,13 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
347
388
|
setLoopSegmentCount((previousLoopSegmentCount) => previousLoopSegmentCount === nextLoopSegmentCount ? previousLoopSegmentCount : nextLoopSegmentCount);
|
|
348
389
|
loopPointRef.current = nextLoopPoint;
|
|
349
390
|
renderPosition(positionRef.current);
|
|
350
|
-
}, [
|
|
391
|
+
}, [
|
|
392
|
+
depthChildren.length,
|
|
393
|
+
isDepth,
|
|
394
|
+
isHorizontal,
|
|
395
|
+
normalizedDepthSpacing,
|
|
396
|
+
renderPosition
|
|
397
|
+
]);
|
|
351
398
|
useEffect(() => {
|
|
352
399
|
autoScrollRef.current.desired = autoScroll;
|
|
353
400
|
if (!autoScroll) {
|
|
@@ -372,6 +419,7 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
372
419
|
scheduleLoopPointRecalculation();
|
|
373
420
|
}, [
|
|
374
421
|
children,
|
|
422
|
+
isDepth,
|
|
375
423
|
isHorizontal,
|
|
376
424
|
scheduleLoopPointRecalculation
|
|
377
425
|
]);
|
|
@@ -426,7 +474,14 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
426
474
|
touchDragListenerCleanupRef.current = null;
|
|
427
475
|
}, []);
|
|
428
476
|
const handleKeyDown = useCallback((e) => {
|
|
429
|
-
if (!keyboard || isInteractiveTarget(e.target)) return;
|
|
477
|
+
if (!enabled || !keyboard || isInteractiveTarget(e.target)) return;
|
|
478
|
+
const arrowDelta = isHorizontal ? e.key === "ArrowLeft" ? ARROW_KEY_VELOCITY : e.key === "ArrowRight" ? -15 : 0 : e.key === "ArrowUp" ? ARROW_KEY_VELOCITY : e.key === "ArrowDown" ? -15 : 0;
|
|
479
|
+
if (prefersReducedMotion && arrowDelta !== 0) {
|
|
480
|
+
e.preventDefault();
|
|
481
|
+
commitPosition(wrapPosition(positionRef.current + arrowDelta));
|
|
482
|
+
velocityRef.current = 0;
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
430
485
|
switch (e.key) {
|
|
431
486
|
case " ":
|
|
432
487
|
e.preventDefault();
|
|
@@ -477,12 +532,15 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
477
532
|
startAnimationLoop();
|
|
478
533
|
}, [
|
|
479
534
|
commitPosition,
|
|
535
|
+
enabled,
|
|
480
536
|
isHorizontal,
|
|
481
537
|
keyboard,
|
|
482
538
|
pauseAutoScroll,
|
|
539
|
+
prefersReducedMotion,
|
|
483
540
|
scheduleAutoScrollResume,
|
|
484
541
|
startAnimationLoop,
|
|
485
|
-
togglePause
|
|
542
|
+
togglePause,
|
|
543
|
+
wrapPosition
|
|
486
544
|
]);
|
|
487
545
|
const handleMouseMove = useCallback((e) => {
|
|
488
546
|
if (!isDraggingRef.current) return;
|
|
@@ -540,7 +598,7 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
540
598
|
handleMouseUp
|
|
541
599
|
]);
|
|
542
600
|
const handleMouseDown = useCallback((e) => {
|
|
543
|
-
if (!draggable || e.button !== 0 || isInteractiveTarget(e.target)) return;
|
|
601
|
+
if (!enabled || !draggable || e.button !== 0 || isInteractiveTarget(e.target)) return;
|
|
544
602
|
e.preventDefault();
|
|
545
603
|
containerRef.current?.focus();
|
|
546
604
|
setDraggingState(true);
|
|
@@ -553,6 +611,7 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
553
611
|
}, [
|
|
554
612
|
attachMouseDragListeners,
|
|
555
613
|
draggable,
|
|
614
|
+
enabled,
|
|
556
615
|
pauseAutoScroll,
|
|
557
616
|
setDraggingState,
|
|
558
617
|
startAnimationLoop
|
|
@@ -576,6 +635,7 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
576
635
|
if (touchInteractionStateRef.current === "idle" || touchInteractionStateRef.current === "ignored") return;
|
|
577
636
|
if (e.touches.length !== 1) return;
|
|
578
637
|
const touch = e.touches[0];
|
|
638
|
+
if (!touch) return;
|
|
579
639
|
if (touchInteractionStateRef.current === "pending") {
|
|
580
640
|
const totalDeltaX = touch.clientX - touchStartXRef.current;
|
|
581
641
|
const totalDeltaY = touch.clientY - touchStartYRef.current;
|
|
@@ -620,18 +680,24 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
620
680
|
handleTouchMove
|
|
621
681
|
]);
|
|
622
682
|
const handleTouchStart = useCallback((e) => {
|
|
623
|
-
if (!draggable || e.touches.length !== 1 || isInteractiveTarget(e.target)) return;
|
|
683
|
+
if (!enabled || !draggable || e.touches.length !== 1 || isInteractiveTarget(e.target)) return;
|
|
684
|
+
const touch = e.touches[0];
|
|
685
|
+
if (!touch) return;
|
|
624
686
|
containerRef.current?.focus();
|
|
625
687
|
touchInteractionStateRef.current = "pending";
|
|
626
|
-
lastTouchXRef.current =
|
|
627
|
-
lastTouchYRef.current =
|
|
628
|
-
touchStartXRef.current =
|
|
629
|
-
touchStartYRef.current =
|
|
688
|
+
lastTouchXRef.current = touch.clientX;
|
|
689
|
+
lastTouchYRef.current = touch.clientY;
|
|
690
|
+
touchStartXRef.current = touch.clientX;
|
|
691
|
+
touchStartYRef.current = touch.clientY;
|
|
630
692
|
velocityRef.current = 0;
|
|
631
693
|
attachTouchDragListeners();
|
|
632
|
-
}, [
|
|
694
|
+
}, [
|
|
695
|
+
attachTouchDragListeners,
|
|
696
|
+
draggable,
|
|
697
|
+
enabled
|
|
698
|
+
]);
|
|
633
699
|
const handleWheel = useCallback((e) => {
|
|
634
|
-
if (!wheelEnabled || e.defaultPrevented) return false;
|
|
700
|
+
if (!enabled || !wheelEnabled || e.defaultPrevented) return false;
|
|
635
701
|
const currentContainer = e.currentTarget instanceof HTMLElement ? e.currentTarget : containerRef.current;
|
|
636
702
|
if (!currentContainer) return false;
|
|
637
703
|
const ownedDelta = getOwnedWheelDelta(e, currentContainer, isHorizontal, wheelMode);
|
|
@@ -648,6 +714,7 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
648
714
|
return true;
|
|
649
715
|
}, [
|
|
650
716
|
commitPosition,
|
|
717
|
+
enabled,
|
|
651
718
|
isHorizontal,
|
|
652
719
|
pauseAutoScroll,
|
|
653
720
|
scheduleAutoScrollResume,
|
|
@@ -657,7 +724,7 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
657
724
|
wrapPosition
|
|
658
725
|
]);
|
|
659
726
|
const applyEdgeHoverPoint = useCallback((point) => {
|
|
660
|
-
if (!edgeHoverScroll) return false;
|
|
727
|
+
if (!enabled || !edgeHoverScroll) return false;
|
|
661
728
|
const viewport = containerRef.current?.parentElement ?? containerRef.current;
|
|
662
729
|
if (!viewport) return false;
|
|
663
730
|
const visibleInterval = readVisibleEdgeInterval(viewport);
|
|
@@ -670,6 +737,7 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
670
737
|
return true;
|
|
671
738
|
}, [
|
|
672
739
|
edgeHoverScroll,
|
|
740
|
+
enabled,
|
|
673
741
|
isHorizontal,
|
|
674
742
|
normalizedEdgeHoverSize,
|
|
675
743
|
readVisibleEdgeInterval,
|
|
@@ -706,7 +774,7 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
706
774
|
}, [setEdgeHoverIntensity]);
|
|
707
775
|
useEffect(() => {
|
|
708
776
|
const currentContainer = containerRef.current;
|
|
709
|
-
if (!currentContainer) return;
|
|
777
|
+
if (!currentContainer || !enabled) return;
|
|
710
778
|
if (draggable) {
|
|
711
779
|
currentContainer.addEventListener("mousedown", handleMouseDown);
|
|
712
780
|
currentContainer.addEventListener("touchstart", handleTouchStart, { passive: true });
|
|
@@ -744,6 +812,7 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
744
812
|
detachMouseDragListeners,
|
|
745
813
|
detachTouchDragListeners,
|
|
746
814
|
draggable,
|
|
815
|
+
enabled,
|
|
747
816
|
edgeHoverScroll,
|
|
748
817
|
edgeHoverInputMode,
|
|
749
818
|
clearEdgeHover,
|
|
@@ -754,6 +823,24 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
754
823
|
handleWheel,
|
|
755
824
|
wheelEnabled
|
|
756
825
|
]);
|
|
826
|
+
useEffect(() => {
|
|
827
|
+
if (enabled) return;
|
|
828
|
+
clearInactivityTimer();
|
|
829
|
+
clearEdgeHover();
|
|
830
|
+
detachMouseDragListeners();
|
|
831
|
+
detachTouchDragListeners();
|
|
832
|
+
touchInteractionStateRef.current = "idle";
|
|
833
|
+
setDraggingState(false);
|
|
834
|
+
velocityRef.current = 0;
|
|
835
|
+
autoScrollRef.current.active = autoScrollRef.current.desired;
|
|
836
|
+
}, [
|
|
837
|
+
clearEdgeHover,
|
|
838
|
+
clearInactivityTimer,
|
|
839
|
+
detachMouseDragListeners,
|
|
840
|
+
detachTouchDragListeners,
|
|
841
|
+
enabled,
|
|
842
|
+
setDraggingState
|
|
843
|
+
]);
|
|
757
844
|
useEffect(() => {
|
|
758
845
|
window.addEventListener("resize", scheduleLoopPointRecalculation);
|
|
759
846
|
return () => {
|
|
@@ -802,7 +889,7 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
802
889
|
};
|
|
803
890
|
}, [clearInactivityTimer]);
|
|
804
891
|
useEffect(() => {
|
|
805
|
-
if (!isTabActive || isPaused) {
|
|
892
|
+
if (!enabled || !isTabActive || isPaused) {
|
|
806
893
|
if (animationFrameRef.current !== null) {
|
|
807
894
|
cancelAnimationFrame(animationFrameRef.current);
|
|
808
895
|
animationFrameRef.current = null;
|
|
@@ -811,7 +898,7 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
811
898
|
return;
|
|
812
899
|
}
|
|
813
900
|
lastFrameTimeRef.current = 0;
|
|
814
|
-
const directionMultiplier = direction === "down" || direction === "right" ? 1 : -1;
|
|
901
|
+
const directionMultiplier = direction === "away" || direction === "down" || direction === "right" ? 1 : -1;
|
|
815
902
|
const animate = (currentTime) => {
|
|
816
903
|
let deltaTime = lastFrameTimeRef.current ? (currentTime - lastFrameTimeRef.current) / MS_PER_FRAME_60FPS : 1;
|
|
817
904
|
deltaTime = Math.min(deltaTime, MAX_DELTA_TIME);
|
|
@@ -822,7 +909,7 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
822
909
|
let nextPosition = positionRef.current;
|
|
823
910
|
const effectiveSpeed = prefersReducedMotion ? normalizedSpeed * REDUCED_MOTION_SPEED_FACTOR : normalizedSpeed;
|
|
824
911
|
const edgeHoverMultiplier = edgeHoverScroll ? getEdgeHoverVelocityScale(edgeHoverIntensityRef.current, normalizedEdgeHoverSpeedMultiplier) : directionMultiplier;
|
|
825
|
-
const shouldAutoScroll = (autoScrollRef.current.desired && autoScrollRef.current.active || externalEdgeHoverLeaseRef.current) && !isDraggingRef.current && (!edgeHoverScroll || edgeHoverMultiplier !== 0);
|
|
912
|
+
const shouldAutoScroll = (autoScrollRef.current.desired && autoScrollRef.current.active || externalEdgeHoverLeaseRef.current) && !isDraggingRef.current && !(isDepth && prefersReducedMotion) && (!edgeHoverScroll || edgeHoverMultiplier !== 0);
|
|
826
913
|
if (shouldAutoScroll) {
|
|
827
914
|
const autoScrollDeltaTime = edgeHoverScroll ? Math.min(deltaTime, MAX_EDGE_HOVER_DELTA_TIME) : deltaTime;
|
|
828
915
|
nextPosition += edgeHoverMultiplier * effectiveSpeed * autoScrollDeltaTime;
|
|
@@ -851,6 +938,8 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
851
938
|
commitPosition,
|
|
852
939
|
direction,
|
|
853
940
|
edgeHoverScroll,
|
|
941
|
+
enabled,
|
|
942
|
+
isDepth,
|
|
854
943
|
isPaused,
|
|
855
944
|
isTabActive,
|
|
856
945
|
normalizedFriction,
|
|
@@ -883,54 +972,73 @@ var ReactSway = forwardRef(function ReactSway({ ariaLabel = "Scrollable content"
|
|
|
883
972
|
lazyRootMargin,
|
|
884
973
|
normalizedLazyThreshold
|
|
885
974
|
]);
|
|
886
|
-
return /* @__PURE__ */
|
|
975
|
+
return /* @__PURE__ */ React.createElement("section", {
|
|
887
976
|
"aria-label": ariaLabel,
|
|
888
977
|
className: `react-sway-container scroller-content${className ? ` ${className}` : ""}`,
|
|
889
978
|
ref: containerRef,
|
|
890
979
|
style: {
|
|
891
|
-
cursor: draggable ? isDraggingRef.current ? "grabbing" : "grab" : "default",
|
|
980
|
+
cursor: enabled && draggable ? isDraggingRef.current ? "grabbing" : "grab" : "default",
|
|
892
981
|
display: isHorizontal ? "flex" : void 0,
|
|
893
982
|
flexDirection: isHorizontal ? "row" : void 0,
|
|
894
|
-
height: isHorizontal ? "100%" : "max-content",
|
|
983
|
+
height: isHorizontal || isDepth ? "100%" : "max-content",
|
|
895
984
|
MozUserSelect: "none",
|
|
896
985
|
msUserSelect: "none",
|
|
897
986
|
minHeight: isHorizontal ? void 0 : "100%",
|
|
898
|
-
overflow: "hidden",
|
|
987
|
+
overflow: isDepth ? "visible" : "hidden",
|
|
988
|
+
perspective: isDepth ? normalizedDepthPerspective : void 0,
|
|
899
989
|
overscrollBehavior,
|
|
900
990
|
pointerEvents: "auto",
|
|
901
991
|
position: "absolute",
|
|
902
992
|
touchAction,
|
|
903
993
|
transform: "translate3d(0, 0, 0)",
|
|
994
|
+
transformStyle: isDepth ? "preserve-3d" : void 0,
|
|
904
995
|
userSelect: "none",
|
|
905
996
|
WebkitUserSelect: "none",
|
|
906
997
|
width: isHorizontal ? "max-content" : "100%",
|
|
907
|
-
willChange: "transform",
|
|
998
|
+
willChange: isDepth ? void 0 : "transform",
|
|
908
999
|
zIndex: 1
|
|
909
1000
|
},
|
|
910
1001
|
onKeyDown: keyboard ? handleKeyDown : void 0,
|
|
911
|
-
tabIndex: keyboard ? 0 : void 0
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1002
|
+
tabIndex: enabled && keyboard ? 0 : void 0
|
|
1003
|
+
}, /* @__PURE__ */ React.createElement("div", {
|
|
1004
|
+
className: "content-group original",
|
|
1005
|
+
ref: originalGroupRef,
|
|
1006
|
+
style: isDepth ? {
|
|
1007
|
+
height: "100%",
|
|
1008
|
+
position: "relative",
|
|
1009
|
+
transformStyle: "preserve-3d"
|
|
1010
|
+
} : isHorizontal ? {
|
|
1011
|
+
display: "flex",
|
|
1012
|
+
flex: "0 0 auto"
|
|
1013
|
+
} : void 0
|
|
1014
|
+
}, isDepth ? depthChildren.map((child, index) => {
|
|
1015
|
+
const { opacity, z } = getDepthItemState(0, index, depthChildren.length, normalizedDepthPerspective, normalizedDepthSpacing);
|
|
1016
|
+
return /* @__PURE__ */ React.createElement("div", {
|
|
1017
|
+
className: "react-sway-depth-item",
|
|
1018
|
+
key: typeof child === "object" && "key" in child ? child.key : index,
|
|
1019
|
+
style: {
|
|
1020
|
+
height: 0,
|
|
1021
|
+
left: "50%",
|
|
1022
|
+
opacity,
|
|
1023
|
+
position: "absolute",
|
|
1024
|
+
top: "50%",
|
|
1025
|
+
transform: `translate3d(0, 0, ${z}px)`,
|
|
1026
|
+
width: 0,
|
|
1027
|
+
willChange: "transform, opacity"
|
|
1028
|
+
}
|
|
1029
|
+
}, child);
|
|
1030
|
+
}) : children), Array.from({ length: isDepth ? 0 : loopSegmentCount - 1 }, (_, duplicateIndex) => /* @__PURE__ */ React.createElement("aside", {
|
|
1031
|
+
"aria-hidden": "true",
|
|
1032
|
+
className: "content-group duplicate",
|
|
1033
|
+
"data-duplicate": "true",
|
|
1034
|
+
...INERT_ATTRIBUTE,
|
|
1035
|
+
key: `duplicate-${duplicateIndex}`,
|
|
1036
|
+
role: "presentation",
|
|
1037
|
+
style: isHorizontal ? {
|
|
1038
|
+
display: "flex",
|
|
1039
|
+
flex: "0 0 auto",
|
|
1040
|
+
pointerEvents: "none"
|
|
1041
|
+
} : { pointerEvents: "none" }
|
|
1042
|
+
}, children)));
|
|
934
1043
|
});
|
|
935
|
-
|
|
936
|
-
export { DEFAULT_EDGE_HOVER_SIZE, DEFAULT_EDGE_HOVER_SPEED_MULTIPLIER, ReactSway };
|
|
1044
|
+
export { DEFAULT_DEPTH_PERSPECTIVE, DEFAULT_DEPTH_SPACING, DEFAULT_EDGE_HOVER_SIZE, DEFAULT_EDGE_HOVER_SPEED_MULTIPLIER, ReactSway };
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/lafittemehdy/sway/issues"
|
|
8
8
|
},
|
|
9
|
-
"description": "
|
|
9
|
+
"description": "Infinite horizontal, vertical, and perspective depth scrolling for React, with automatic motion and accessible manual interactions.",
|
|
10
10
|
"exports": {
|
|
11
11
|
".": {
|
|
12
12
|
"types": "./dist/index.d.ts",
|
|
@@ -20,14 +20,14 @@
|
|
|
20
20
|
"@testing-library/react": "^16.3.3",
|
|
21
21
|
"@types/react": "^19.3.0",
|
|
22
22
|
"@types/react-dom": "^19.3.0",
|
|
23
|
-
"eslint": "^10.
|
|
23
|
+
"eslint": "^10.14.0",
|
|
24
24
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
25
|
-
"jsdom": "^
|
|
25
|
+
"jsdom": "^31.0.0",
|
|
26
26
|
"react": "^19.3.1",
|
|
27
27
|
"react-dom": "^19.3.1",
|
|
28
|
-
"typescript": "
|
|
29
|
-
"vite": "^8.3.
|
|
30
|
-
"vitest": "^5.0
|
|
28
|
+
"typescript": "~6.0.3",
|
|
29
|
+
"vite": "^8.3.6",
|
|
30
|
+
"vitest": "^5.1.0"
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
33
|
"CHANGELOG.md",
|
|
@@ -37,9 +37,11 @@
|
|
|
37
37
|
"keywords": [
|
|
38
38
|
"content-stream",
|
|
39
39
|
"continuous-scroll",
|
|
40
|
+
"depth-scroll",
|
|
40
41
|
"frontend",
|
|
41
42
|
"infinite-scroll",
|
|
42
43
|
"javascript",
|
|
44
|
+
"perspective",
|
|
43
45
|
"react",
|
|
44
46
|
"react-component",
|
|
45
47
|
"scroller",
|
|
@@ -74,5 +76,5 @@
|
|
|
74
76
|
},
|
|
75
77
|
"type": "module",
|
|
76
78
|
"types": "./dist/index.d.ts",
|
|
77
|
-
"version": "0.
|
|
79
|
+
"version": "0.5.0"
|
|
78
80
|
}
|