react-sway 0.2.6 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +138 -12
- package/dist/{index.d.cts → ReactSway.d.ts} +30 -10
- package/dist/index.cjs +649 -489
- package/dist/index.d.ts +3 -60
- package/dist/index.js +646 -460
- package/package.json +14 -13
- package/src/ReactSway.tsx +296 -64
- package/src/index.ts +1 -1
- package/src/__tests__/ReactSway.test.tsx +0 -573
- package/src/__tests__/setup.ts +0 -20
package/README.md
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
# react-sway
|
|
2
2
|
|
|
3
|
-
A React component for smooth, infinite, and interactive content scrolling. It duplicates content to create a seamless looping effect, controllable via touch, mouse drag, wheel,
|
|
3
|
+
A React component for smooth, infinite, and interactive content scrolling on the vertical or horizontal axis. It duplicates content to create a seamless looping effect, controllable via touch, mouse drag, wheel, keyboard, or edge hover.
|
|
4
4
|
|
|
5
5
|
## What is `react-sway`?
|
|
6
6
|
|
|
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.
|
|
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.
|
|
8
8
|
|
|
9
9
|
It works by duplicating your content to create a seamless loop and uses CSS transforms for smooth animation. The duplicated content is wrapped in `<aside>` elements with `aria-hidden="true"` and `role="presentation"` to ensure good accessibility and helps search engines understand the content structure.
|
|
10
10
|
|
|
11
11
|
### Core Features
|
|
12
12
|
|
|
13
|
-
* **Smooth Infinite Scroll:** Content loops continuously.
|
|
13
|
+
* **Smooth Infinite Scroll:** Content loops continuously on the vertical or horizontal axis.
|
|
14
14
|
* **Auto-Scroll:** Scrolls automatically, with configurable speed and direction.
|
|
15
|
+
* **Edge Hover:** Use `edgeHoverScroll` to keep auto-scroll idle until the pointer enters the active axis boundary zone.
|
|
15
16
|
* **User Friendly Interactions:**
|
|
16
17
|
* Click and drag to scroll.
|
|
17
18
|
* Swipe on touch devices.
|
|
18
|
-
* Mouse wheel support with delta-mode normalization and velocity capping.
|
|
19
|
-
* Keyboard controls: Spacebar to pause/resume,
|
|
19
|
+
* Mouse wheel support with axis-aware intent detection, delta-mode normalization, and velocity capping.
|
|
20
|
+
* Keyboard controls: Spacebar to pause/resume, Arrow keys to scroll, Home/End to jump.
|
|
20
21
|
* **Responsive:** Adjusts to window resizing with debounced recalculation.
|
|
21
22
|
* **Lazy Visibility Detection:** Add a `content-item` class to your child elements, and `react-sway` automatically uses an IntersectionObserver to add a `.visible` class when they enter the viewport. Useful for triggering CSS animations or deferred rendering. Configurable via `lazy`, `lazyRootMargin`, and `lazyThreshold` props.
|
|
22
23
|
* **Reduced Motion Support:** Respects `prefers-reduced-motion: reduce` by lowering auto-scroll speed and disabling momentum effects.
|
|
@@ -83,26 +84,151 @@ function SwayUsageExample() {
|
|
|
83
84
|
export default SwayUsageExample;
|
|
84
85
|
```
|
|
85
86
|
|
|
87
|
+
### Auto-scroll and edge-hover examples
|
|
88
|
+
|
|
89
|
+
React Sway can either move automatically or stay idle until the pointer reaches an axis boundary. The resulting examples are the product of two axes and two trigger modes.
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { ReactSway } from 'react-sway';
|
|
93
|
+
|
|
94
|
+
function TriggerModeExamples() {
|
|
95
|
+
const edgeOnlyProps = {
|
|
96
|
+
draggable: false,
|
|
97
|
+
keyboard: false,
|
|
98
|
+
wheelEnabled: false,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div className="trigger-mode-grid">
|
|
103
|
+
<div className="trigger-mode-case">
|
|
104
|
+
<ReactSway>
|
|
105
|
+
<article className="content-item">Vertical auto</article>
|
|
106
|
+
<article className="content-item">Automatic trigger</article>
|
|
107
|
+
<article className="content-item">Continuous loop</article>
|
|
108
|
+
</ReactSway>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div className="trigger-mode-case">
|
|
112
|
+
<ReactSway {...edgeOnlyProps} edgeHoverScroll edgeHoverSize={96}>
|
|
113
|
+
<article className="content-item">Vertical edge</article>
|
|
114
|
+
<article className="content-item">Top boundary</article>
|
|
115
|
+
<article className="content-item">Bottom boundary</article>
|
|
116
|
+
</ReactSway>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div className="trigger-mode-case trigger-mode-case-horizontal">
|
|
120
|
+
<ReactSway axis="horizontal">
|
|
121
|
+
<article className="content-item">Horizontal auto</article>
|
|
122
|
+
<article className="content-item">Automatic trigger</article>
|
|
123
|
+
<article className="content-item">Continuous loop</article>
|
|
124
|
+
</ReactSway>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div className="trigger-mode-case trigger-mode-case-horizontal">
|
|
128
|
+
<ReactSway {...edgeOnlyProps} axis="horizontal" edgeHoverScroll edgeHoverSize={96}>
|
|
129
|
+
<article className="content-item">Horizontal edge</article>
|
|
130
|
+
<article className="content-item">Left boundary</article>
|
|
131
|
+
<article className="content-item">Right boundary</article>
|
|
132
|
+
</ReactSway>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
```css
|
|
140
|
+
.trigger-mode-grid {
|
|
141
|
+
display: grid;
|
|
142
|
+
gap: 16px;
|
|
143
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.trigger-mode-case {
|
|
147
|
+
height: 320px;
|
|
148
|
+
overflow: hidden;
|
|
149
|
+
position: relative;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.trigger-mode-case-horizontal {
|
|
153
|
+
align-items: center;
|
|
154
|
+
display: flex;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.trigger-mode-case-horizontal .content-item {
|
|
158
|
+
flex: 0 0 260px;
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Automatic cases start moving immediately and keep the default drag, touch, wheel, and keyboard interactions enabled. Edge-hover cases stay idle until the pointer enters top/bottom for vertical instances or left/right for horizontal instances.
|
|
163
|
+
|
|
164
|
+
### Wheel intent modes
|
|
165
|
+
|
|
166
|
+
By default, React Sway treats wheel gestures as axis-aware input. A horizontal instance consumes horizontal trackpad movement and Shift+wheel, while ordinary vertical wheel movement is left to the page. This prevents horizontal showcases from trapping page scroll when the pointer is above them.
|
|
167
|
+
|
|
168
|
+
Use `wheelMode="capture"` when the Sway should intentionally own every wheel event that reaches it.
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
function WheelModeExamples() {
|
|
172
|
+
return (
|
|
173
|
+
<>
|
|
174
|
+
<ReactSway axis="horizontal" wheelMode="axis">
|
|
175
|
+
<article className="content-item">Horizontal intent only</article>
|
|
176
|
+
<article className="content-item">Vertical wheel still scrolls the page</article>
|
|
177
|
+
</ReactSway>
|
|
178
|
+
|
|
179
|
+
<ReactSway axis="horizontal" wheelMode="capture">
|
|
180
|
+
<article className="content-item">Full wheel capture</article>
|
|
181
|
+
<article className="content-item">Vertical wheel drives the Sway</article>
|
|
182
|
+
</ReactSway>
|
|
183
|
+
</>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
function MinimalEdgeHoverExamples() {
|
|
190
|
+
return (
|
|
191
|
+
<>
|
|
192
|
+
<ReactSway draggable={false} edgeHoverScroll keyboard={false} wheelEnabled={false}>
|
|
193
|
+
<article className="content-item">Project Alpha</article>
|
|
194
|
+
<article className="content-item">Project Beta</article>
|
|
195
|
+
<article className="content-item">Project Gamma</article>
|
|
196
|
+
</ReactSway>
|
|
197
|
+
|
|
198
|
+
<ReactSway axis="horizontal" draggable={false} edgeHoverScroll keyboard={false} wheelEnabled={false}>
|
|
199
|
+
<article className="content-item">Project Alpha</article>
|
|
200
|
+
<article className="content-item">Project Beta</article>
|
|
201
|
+
<article className="content-item">Project Gamma</article>
|
|
202
|
+
</ReactSway>
|
|
203
|
+
</>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
86
208
|
## Props
|
|
87
209
|
|
|
88
210
|
| Prop | Type | Default | Description |
|
|
89
211
|
|------|------|---------|-------------|
|
|
212
|
+
| `axis` | `'vertical' \| 'horizontal'` | Inferred from `direction`, otherwise `'vertical'` | Scroll axis. Use `'horizontal'` for left/right loops. |
|
|
90
213
|
| `autoScroll` | `boolean` | `true` | Enable/disable auto-scrolling. |
|
|
91
|
-
| `children` | `ReactNode` |
|
|
92
|
-
| `direction` | `'down' \| 'up'` | `'up'` | Auto-scroll direction. |
|
|
214
|
+
| `children` | `ReactNode` | - | Content elements to render in the scroll container. |
|
|
215
|
+
| `direction` | `'down' \| 'left' \| 'right' \| 'up'` | `'up'` | Auto-scroll direction. `left` and `right` imply horizontal scrolling when `axis` is omitted. |
|
|
93
216
|
| `draggable` | `boolean` | `true` | Enable mouse/touch drag interaction. |
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
217
|
+
| `edgeHoverScroll` | `boolean` | `false` | Only auto-scroll while hovering the active axis boundary: top/bottom for vertical, left/right for horizontal. |
|
|
218
|
+
| `edgeHoverSize` | `number` | `96` | Thickness in pixels of each edge-hover activation zone. |
|
|
219
|
+
| `friction` | `number` | `0.95` | Momentum decay coefficient (0-1, lower = more friction). |
|
|
220
|
+
| `keyboard` | `boolean` | `true` | Enable keyboard controls (Space, Arrow keys, Home/End). |
|
|
96
221
|
| `lazy` | `boolean` | `true` | Enable lazy visibility detection via IntersectionObserver. |
|
|
97
222
|
| `lazyRootMargin` | `string` | `'100px'` | IntersectionObserver `rootMargin` for lazy visibility detection. |
|
|
98
223
|
| `lazyThreshold` | `number` | `0.01` | IntersectionObserver `threshold` for lazy visibility detection. |
|
|
99
|
-
| `onPause` | `() => void` |
|
|
100
|
-
| `onResume` | `() => void` |
|
|
101
|
-
| `onScroll` | `(position: number) => void` |
|
|
224
|
+
| `onPause` | `() => void` | - | Fired when scrolling pauses. |
|
|
225
|
+
| `onResume` | `() => void` | - | Fired when scrolling resumes after pause. |
|
|
226
|
+
| `onScroll` | `(position: number) => void` | - | Fired on every position change with the current scroll position. |
|
|
102
227
|
| `pauseOnInteraction` | `boolean` | `true` | Pause auto-scroll during user interaction. |
|
|
103
228
|
| `resumeDelay` | `number` | `2000` | Milliseconds before auto-scroll resumes after interaction. |
|
|
104
229
|
| `speed` | `number` | `0.5` | Auto-scroll speed in pixels per frame at 60fps. |
|
|
105
230
|
| `wheelEnabled` | `boolean` | `true` | Enable mouse wheel scrolling. |
|
|
231
|
+
| `wheelMode` | `'axis' \| 'capture'` | `'axis'` | Define whether wheel input only consumes gestures aligned with the Sway axis or captures every wheel event. |
|
|
106
232
|
|
|
107
233
|
## License
|
|
108
234
|
|
|
@@ -1,21 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Core ReactSway component implementing axis-aware infinite scrolling interactions.
|
|
3
|
+
*/
|
|
4
|
+
import { type ReactNode } from 'react';
|
|
5
|
+
export type SwayAxis = 'horizontal' | 'vertical';
|
|
6
|
+
export type SwayDirection = 'down' | 'left' | 'right' | 'up';
|
|
7
|
+
export type SwayWheelMode = 'axis' | 'capture';
|
|
4
8
|
/**
|
|
5
9
|
* Props for the ReactSway infinite scrolling component.
|
|
6
10
|
*/
|
|
7
|
-
interface ReactSwayProps {
|
|
11
|
+
export interface ReactSwayProps {
|
|
12
|
+
/** Scroll axis. Horizontal directions imply `horizontal` when omitted. @default 'vertical' */
|
|
13
|
+
axis?: SwayAxis;
|
|
8
14
|
/** Enable/disable auto-scrolling. @default true */
|
|
9
15
|
autoScroll?: boolean;
|
|
10
16
|
/** Content elements to render in the infinite scroll container. */
|
|
11
17
|
children: ReactNode;
|
|
12
18
|
/** Auto-scroll direction. @default 'up' */
|
|
13
|
-
direction?:
|
|
19
|
+
direction?: SwayDirection;
|
|
14
20
|
/** Enable mouse/touch drag interaction. @default true */
|
|
15
21
|
draggable?: boolean;
|
|
22
|
+
/** Only auto-scroll while the pointer hovers an axis boundary. @default false */
|
|
23
|
+
edgeHoverScroll?: boolean;
|
|
24
|
+
/** Edge-hover activation thickness in pixels. @default 96 */
|
|
25
|
+
edgeHoverSize?: number;
|
|
16
26
|
/** Momentum decay coefficient (0-1, lower = more friction). @default 0.95 */
|
|
17
27
|
friction?: number;
|
|
18
|
-
/** Enable keyboard controls (Space,
|
|
28
|
+
/** Enable keyboard controls (Space, Arrow keys, Home/End). @default true */
|
|
19
29
|
keyboard?: boolean;
|
|
20
30
|
/** Enable lazy visibility detection via IntersectionObserver. @default true */
|
|
21
31
|
lazy?: boolean;
|
|
@@ -37,12 +47,23 @@ interface ReactSwayProps {
|
|
|
37
47
|
speed?: number;
|
|
38
48
|
/** Enable mouse wheel scrolling. @default true */
|
|
39
49
|
wheelEnabled?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Defines which wheel gestures ReactSway may cancel.
|
|
52
|
+
*
|
|
53
|
+
* `axis` only consumes wheel gestures aligned with the Sway axis, letting
|
|
54
|
+
* cross-axis page scroll pass through. `capture` preserves legacy behavior
|
|
55
|
+
* by consuming every wheel event that reaches the component.
|
|
56
|
+
*
|
|
57
|
+
* @default 'axis'
|
|
58
|
+
*/
|
|
59
|
+
wheelMode?: SwayWheelMode;
|
|
40
60
|
}
|
|
41
61
|
/**
|
|
42
62
|
* A smooth, infinite scrolling container component.
|
|
43
63
|
*
|
|
44
64
|
* Renders children in a continuously looping scroll area with support for
|
|
45
|
-
* auto-scrolling, mouse drag, touch swipe, wheel,
|
|
65
|
+
* auto-scrolling, mouse drag, touch swipe, wheel, keyboard interactions,
|
|
66
|
+
* and optional edge-hover scrolling.
|
|
46
67
|
* Content is duplicated to create a seamless loop effect. Duplicate content
|
|
47
68
|
* is wrapped in `<aside>` elements with `aria-hidden="true"` for accessibility.
|
|
48
69
|
*
|
|
@@ -57,6 +78,5 @@ interface ReactSwayProps {
|
|
|
57
78
|
* </ReactSway>
|
|
58
79
|
* ```
|
|
59
80
|
*/
|
|
60
|
-
declare function ReactSway({ autoScroll, children, direction, draggable, friction, keyboard, lazy, lazyRootMargin, lazyThreshold, onPause, onResume, onScroll, pauseOnInteraction, resumeDelay, speed, wheelEnabled, }: ReactSwayProps):
|
|
61
|
-
|
|
62
|
-
export { ReactSway, type ReactSwayProps };
|
|
81
|
+
declare function ReactSway({ axis, autoScroll, children, direction, draggable, edgeHoverScroll, edgeHoverSize, friction, keyboard, lazy, lazyRootMargin, lazyThreshold, onPause, onResume, onScroll, pauseOnInteraction, resumeDelay, speed, wheelEnabled, wheelMode, }: ReactSwayProps): import("react").JSX.Element;
|
|
82
|
+
export default ReactSway;
|