react-css-marquee 0.0.13 → 1.0.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/LICENSE +18 -0
- package/README.md +110 -49
- package/dist/index.d.ts +123 -13
- package/dist/index.js +234 -85
- package/package.json +33 -13
- package/dist/index.js.map +0 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
5
|
+
the Software without restriction, including without limitation the rights to
|
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
7
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
8
|
+
subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
15
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
16
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,68 +1,129 @@
|
|
|
1
1
|
# React CSS Marquee
|
|
2
2
|
|
|
3
|
-
[
|
|
3
|
+
[](https://github.com/ellerbrock/typescript-badges/)
|
|
4
|
+
[](https://www.npmjs.com/package/react-css-marquee)
|
|
5
|
+
[](https://bundlephobia.com/package/react-css-marquee)
|
|
4
6
|
|
|
7
|
+
A lightweight React marquee component powered by CSS animations. Supports horizontal and vertical scrolling, gradient fade edges, pause controls, auto-fill, loop count, and more -- with zero dependencies.
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
npm install react-css-marquee
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { Marquee } from 'react-css-marquee'
|
|
19
|
+
|
|
20
|
+
<Marquee>
|
|
21
|
+
<img src="logo1.png" />
|
|
22
|
+
<img src="logo2.png" />
|
|
23
|
+
<img src="logo3.png" />
|
|
24
|
+
</Marquee>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Examples
|
|
8
28
|
|
|
9
|
-
|
|
29
|
+
### Gradient fade edges
|
|
10
30
|
|
|
11
|
-
|
|
12
|
-
<img style="width: 200px" src="https://s3.eu-central-1.amazonaws.com/samuel.weckstrom.xyz/github/marquee.gif">
|
|
13
|
-
</div>
|
|
31
|
+
Uses CSS `mask-image` so it works over any background without specifying a color.
|
|
14
32
|
|
|
33
|
+
```tsx
|
|
34
|
+
<Marquee gradient gradientWidth={200}>
|
|
35
|
+
<div>Item 1</div>
|
|
36
|
+
<div>Item 2</div>
|
|
37
|
+
<div>Item 3</div>
|
|
38
|
+
</Marquee>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Vertical scrolling
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
<Marquee direction="vertical" style={{ height: 400 }}>
|
|
45
|
+
<p>Line one</p>
|
|
46
|
+
<p>Line two</p>
|
|
47
|
+
<p>Line three</p>
|
|
48
|
+
</Marquee>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Sparse scrolling (no repeat)
|
|
15
52
|
|
|
16
|
-
|
|
53
|
+
Items scroll across individually without duplicating to fill the viewport.
|
|
17
54
|
|
|
18
|
-
|
|
55
|
+
```tsx
|
|
56
|
+
<Marquee repeat={false} speed={8}>
|
|
57
|
+
<h2>Breaking news headline</h2>
|
|
58
|
+
</Marquee>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Pause on hover
|
|
19
62
|
|
|
63
|
+
```tsx
|
|
64
|
+
<Marquee isPausedOnHover>
|
|
65
|
+
<Card />
|
|
66
|
+
<Card />
|
|
67
|
+
<Card />
|
|
68
|
+
</Marquee>
|
|
20
69
|
```
|
|
21
|
-
|
|
70
|
+
|
|
71
|
+
### Drag to scroll
|
|
72
|
+
|
|
73
|
+
Let users grab and scrub through the marquee content. The animation resumes from where the user left off.
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
<Marquee draggable>
|
|
77
|
+
<img src="photo1.jpg" />
|
|
78
|
+
<img src="photo2.jpg" />
|
|
79
|
+
<img src="photo3.jpg" />
|
|
80
|
+
</Marquee>
|
|
22
81
|
```
|
|
23
82
|
|
|
24
|
-
###
|
|
83
|
+
### Click to toggle pause
|
|
25
84
|
|
|
85
|
+
```tsx
|
|
86
|
+
<Marquee isPausedOnClick>
|
|
87
|
+
<span>Click anywhere on the marquee to pause / resume</span>
|
|
88
|
+
</Marquee>
|
|
26
89
|
```
|
|
27
|
-
import Marquee from 'react-css-marquee'
|
|
28
90
|
|
|
29
|
-
|
|
91
|
+
### Finite loop with callback
|
|
30
92
|
|
|
31
|
-
|
|
93
|
+
```tsx
|
|
94
|
+
<Marquee loop={3} onFinish={() => console.log('done')}>
|
|
95
|
+
<span>This plays 3 times</span>
|
|
96
|
+
</Marquee>
|
|
32
97
|
```
|
|
33
98
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|Default
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
99
|
+
## Props
|
|
100
|
+
|
|
101
|
+
| Prop | Type | Default | Description |
|
|
102
|
+
|------|------|---------|-------------|
|
|
103
|
+
| `children` | `ReactNode` | -- | Content to scroll |
|
|
104
|
+
| `className` | `string` | -- | CSS class for the container element |
|
|
105
|
+
| `delay` | `number` | `0` | Delay before animation starts, in seconds |
|
|
106
|
+
| `direction` | `'horizontal' \| 'vertical'` | `'horizontal'` | Scroll direction |
|
|
107
|
+
| `draggable` | `boolean` | `false` | Let users grab and drag to scrub through the marquee. Animation resumes on release |
|
|
108
|
+
| `repeat` | `boolean` | `true` | Duplicate children to fill the viewport for a seamless loop. Set `false` for spaced-out items that scroll across individually |
|
|
109
|
+
| `gap` | `number` | `40` | Gap between items in pixels |
|
|
110
|
+
| `gradient` | `boolean` | `false` | Fade out edges with a gradient overlay |
|
|
111
|
+
| `gradientWidth` | `number \| string` | `200` | Width of the gradient fade zone (number for px, string for any CSS unit) |
|
|
112
|
+
| `isPaused` | `boolean` | `false` | Pause the animation |
|
|
113
|
+
| `isPausedOnClick` | `boolean` | `false` | Click the marquee to toggle between paused and running |
|
|
114
|
+
| `isPausedOnHover` | `boolean` | `false` | Pause when hovering over the marquee |
|
|
115
|
+
| `isPausedOnMouseDown` | `boolean` | `false` | Pause while the mouse button is held down on the marquee |
|
|
116
|
+
| `loop` | `number` | `0` | Number of times to play the animation. `0` means infinite |
|
|
117
|
+
| `onCycleComplete` | `() => void` | -- | Called each time the animation completes one cycle |
|
|
118
|
+
| `onFinish` | `() => void` | -- | Called when a finite loop animation ends |
|
|
119
|
+
| `reverse` | `boolean` | `false` | Reverse the scroll direction |
|
|
120
|
+
| `speed` | `number` | `10` | Animation duration in seconds (higher = slower) |
|
|
121
|
+
| `style` | `CSSProperties` | -- | Inline styles for the container element |
|
|
122
|
+
|
|
123
|
+
## Accessibility
|
|
124
|
+
|
|
125
|
+
The component automatically pauses animation when the user has `prefers-reduced-motion: reduce` enabled. Duplicate items rendered for the seamless loop are marked with `aria-hidden` to prevent screen readers from reading them multiple times.
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -1,16 +1,126 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
text
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode, CSSProperties } from 'react';
|
|
3
|
+
|
|
4
|
+
type MarqueeProps = {
|
|
5
|
+
/** Content to scroll. Can be any React nodes — images, cards, text, etc. */
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
/** CSS class applied to the outer container element. */
|
|
8
|
+
className?: string;
|
|
9
|
+
/** Seconds to wait before the animation begins. Default: `0`. */
|
|
10
|
+
delay?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Axis along which content scrolls.
|
|
13
|
+
* - `'horizontal'` — scrolls left (or right when `reverse` is true). Default.
|
|
14
|
+
* - `'vertical'` — scrolls up (or down when `reverse` is true).
|
|
15
|
+
* For vertical marquees give the container a fixed height via `style`.
|
|
16
|
+
*/
|
|
17
|
+
direction?: 'horizontal' | 'vertical';
|
|
18
|
+
/**
|
|
19
|
+
* Allow users to click-and-drag to scrub through the marquee.
|
|
20
|
+
* The animation resumes from the dragged position on release.
|
|
21
|
+
* Default: `false`.
|
|
22
|
+
*/
|
|
23
|
+
draggable?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Duplicate children enough times to fill the viewport, creating a
|
|
26
|
+
* seamless infinite loop. Set to `false` for a "ticker" effect where
|
|
27
|
+
* items scroll across the screen individually without repeating.
|
|
28
|
+
* Default: `true`.
|
|
29
|
+
*/
|
|
30
|
+
repeat?: boolean;
|
|
31
|
+
/** Gap between items (and between repeated copies) in pixels. Default: `40`. */
|
|
32
|
+
gap?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Fade the leading and trailing edges of the marquee using a CSS
|
|
35
|
+
* `mask-image` gradient. Works over any background colour without
|
|
36
|
+
* needing to know it. Default: `false`.
|
|
37
|
+
*/
|
|
38
|
+
gradient?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Width of the gradient fade zone on each edge.
|
|
41
|
+
* Pass a `number` for pixels or a CSS string (e.g. `'10%'`).
|
|
42
|
+
* Only applies when `gradient` is `true`. Default: `200` (px).
|
|
43
|
+
*/
|
|
44
|
+
gradientWidth?: number | string;
|
|
45
|
+
/** Programmatically pause the animation. Default: `false`. */
|
|
46
|
+
isPaused?: boolean;
|
|
47
|
+
/** Clicking anywhere on the marquee toggles between paused and running. Default: `false`. */
|
|
48
|
+
isPausedOnClick?: boolean;
|
|
49
|
+
/** Pause while the pointer hovers over the marquee. Works on touch via pointer events. Default: `false`. */
|
|
50
|
+
isPausedOnHover?: boolean;
|
|
51
|
+
/** Pause while the mouse button (or touch) is held down on the marquee. Default: `false`. */
|
|
52
|
+
isPausedOnMouseDown?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Number of full animation cycles to play before stopping.
|
|
55
|
+
* `0` = infinite (default). Pair with `onFinish` to react when it ends.
|
|
56
|
+
*/
|
|
57
|
+
loop?: number;
|
|
58
|
+
/** Called each time the marquee completes one full cycle. */
|
|
59
|
+
onCycleComplete?: () => void;
|
|
60
|
+
/** Called once when a finite (`loop > 0`) animation finishes all its cycles. */
|
|
61
|
+
onFinish?: () => void;
|
|
62
|
+
/**
|
|
63
|
+
* Reverse the scroll direction.
|
|
64
|
+
* Horizontal: scrolls right instead of left.
|
|
65
|
+
* Vertical: scrolls down instead of up.
|
|
66
|
+
* Default: `false`.
|
|
67
|
+
*/
|
|
9
68
|
reverse?: boolean;
|
|
10
|
-
|
|
11
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Controls scroll speed as an animation duration in seconds.
|
|
71
|
+
* A **higher** number means **slower** movement (it's a CSS `animation-duration`).
|
|
72
|
+
* Default: `10` (one full cycle takes 10 seconds).
|
|
73
|
+
*/
|
|
12
74
|
speed?: number;
|
|
13
|
-
|
|
75
|
+
/** Inline styles applied to the outer container element. */
|
|
76
|
+
style?: CSSProperties;
|
|
14
77
|
};
|
|
15
|
-
|
|
16
|
-
|
|
78
|
+
/**
|
|
79
|
+
* A lightweight, zero-dependency React marquee powered by CSS animations.
|
|
80
|
+
*
|
|
81
|
+
* Renders children in a continuously scrolling track. By default children are
|
|
82
|
+
* duplicated to fill the viewport for a seamless infinite loop. Set
|
|
83
|
+
* `repeat={false}` for a "breaking news" ticker where each item crosses once.
|
|
84
|
+
*
|
|
85
|
+
* @example Basic horizontal logo strip
|
|
86
|
+
* ```tsx
|
|
87
|
+
* <Marquee gap={32} speed={15}>
|
|
88
|
+
* <img src="logo-a.svg" />
|
|
89
|
+
* <img src="logo-b.svg" />
|
|
90
|
+
* <img src="logo-c.svg" />
|
|
91
|
+
* </Marquee>
|
|
92
|
+
* ```
|
|
93
|
+
*
|
|
94
|
+
* @example Vertical feed with gradient edges, paused on hover
|
|
95
|
+
* ```tsx
|
|
96
|
+
* <Marquee direction="vertical" gradient gradientWidth={80} isPausedOnHover style={{ height: 400 }}>
|
|
97
|
+
* <Card title="Item 1" />
|
|
98
|
+
* <Card title="Item 2" />
|
|
99
|
+
* </Marquee>
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* @example Ticker — items scroll across once, no repeat
|
|
103
|
+
* ```tsx
|
|
104
|
+
* <Marquee repeat={false} speed={8}>
|
|
105
|
+
* <h2>Breaking: major update shipped</h2>
|
|
106
|
+
* </Marquee>
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* @example Finite loop with completion callback
|
|
110
|
+
* ```tsx
|
|
111
|
+
* <Marquee loop={3} onFinish={() => setDone(true)}>
|
|
112
|
+
* <span>Plays exactly three times</span>
|
|
113
|
+
* </Marquee>
|
|
114
|
+
* ```
|
|
115
|
+
*
|
|
116
|
+
* @example Draggable photo strip
|
|
117
|
+
* ```tsx
|
|
118
|
+
* <Marquee draggable gap={16}>
|
|
119
|
+
* <img src="photo1.jpg" />
|
|
120
|
+
* <img src="photo2.jpg" />
|
|
121
|
+
* </Marquee>
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
declare const Marquee: ({ children, className, delay, direction, draggable, repeat, gap, gradient, gradientWidth, isPaused, isPausedOnClick, isPausedOnHover, isPausedOnMouseDown, loop, onCycleComplete, onFinish, reverse, speed, style: userStyle, }: MarqueeProps) => react_jsx_runtime.JSX.Element;
|
|
125
|
+
|
|
126
|
+
export { Marquee, type MarqueeProps };
|
package/dist/index.js
CHANGED
|
@@ -1,88 +1,237 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
var ANIMATION_DIRECTION = props.reverse
|
|
49
|
-
? DIRECTION.REVERSE
|
|
50
|
-
: DIRECTION.LEFT;
|
|
51
|
-
var ROTATION = props.vertical && !props.flip
|
|
52
|
-
? 'rotate(90deg)'
|
|
53
|
-
: props.vertical && props.flip
|
|
54
|
-
? 'rotate(-90deg)'
|
|
55
|
-
: 'rotate(0deg)';
|
|
56
|
-
var MAX_DURATION = 10;
|
|
57
|
-
var ANIMATION_DURATION = (props.vertical ? height : width) / SPEED / MAX_DURATION;
|
|
58
|
-
var CONTAINER_ALIGN = props.vertical && !props.flip
|
|
59
|
-
? 'translateX(5%)'
|
|
60
|
-
: props.vertical && props.flip
|
|
61
|
-
? "translateX(-" + height + "px)"
|
|
62
|
-
: 'translateX(0)';
|
|
63
|
-
var TRANSFORM_ORIGIN = props.vertical && !props.flip
|
|
64
|
-
? ''
|
|
65
|
-
: props.vertical && props.flip
|
|
66
|
-
? ORIENTATION.TOP_LEFT
|
|
67
|
-
: '';
|
|
68
|
-
var ITEM_WIDTH = props.vertical ? height + "px" : width + "px";
|
|
69
|
-
var ITEM_HEIGHT = 'auto';
|
|
70
|
-
var SPACING = props.spacing || 4;
|
|
71
|
-
var TEXT = props.text || 'REACT CSS MARQUEE';
|
|
72
|
-
var marqueeText = ("" + TEXT + ' '.repeat(SPACING)).repeat((props.vertical ? height : width) / (SPACING + TEXT.length) / 6);
|
|
73
|
-
var handleMouseOver = function (event) {
|
|
74
|
-
if (props.hoverStop) {
|
|
75
|
-
var isMouseEnter = event.type === 'mouseenter';
|
|
76
|
-
setIsMouseOver(isMouseEnter);
|
|
1
|
+
import { useState, useRef, useEffect, useMemo } from 'react';
|
|
2
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
// src/index.tsx
|
|
5
|
+
var instanceCounter = 0;
|
|
6
|
+
var Marquee = ({
|
|
7
|
+
children,
|
|
8
|
+
className,
|
|
9
|
+
delay = 0,
|
|
10
|
+
direction = "horizontal",
|
|
11
|
+
draggable = false,
|
|
12
|
+
repeat = true,
|
|
13
|
+
gap = 40,
|
|
14
|
+
gradient = false,
|
|
15
|
+
gradientWidth = 200,
|
|
16
|
+
isPaused = false,
|
|
17
|
+
isPausedOnClick = false,
|
|
18
|
+
isPausedOnHover = false,
|
|
19
|
+
isPausedOnMouseDown = false,
|
|
20
|
+
loop = 0,
|
|
21
|
+
onCycleComplete,
|
|
22
|
+
onFinish,
|
|
23
|
+
reverse = false,
|
|
24
|
+
speed = 10,
|
|
25
|
+
style: userStyle
|
|
26
|
+
}) => {
|
|
27
|
+
const [uid] = useState(() => `rcm${++instanceCounter}`);
|
|
28
|
+
const horiz = direction === "horizontal";
|
|
29
|
+
const containerRef = useRef(null);
|
|
30
|
+
const groupRef = useRef(null);
|
|
31
|
+
const trackRef = useRef(null);
|
|
32
|
+
const [sizes, setSizes] = useState({ container: 0, group: 0 });
|
|
33
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
34
|
+
const [clickPaused, setClickPaused] = useState(false);
|
|
35
|
+
const [touchHoverPaused, setTouchHoverPaused] = useState(false);
|
|
36
|
+
const scrubRef = useRef(0);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const container = containerRef.current;
|
|
39
|
+
const group = groupRef.current;
|
|
40
|
+
if (!container || !group) return;
|
|
41
|
+
const ro = new ResizeObserver((entries) => {
|
|
42
|
+
setSizes((prev) => {
|
|
43
|
+
let next = prev;
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
const s = horiz ? entry.contentRect.width : entry.contentRect.height;
|
|
46
|
+
if (entry.target === container) next = { ...next, container: s };
|
|
47
|
+
if (entry.target === group) next = { ...next, group: s };
|
|
77
48
|
}
|
|
49
|
+
return next;
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
ro.observe(container);
|
|
53
|
+
ro.observe(group);
|
|
54
|
+
return () => ro.disconnect();
|
|
55
|
+
}, [horiz]);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const container = containerRef.current;
|
|
58
|
+
if (!container || !isPausedOnHover) return;
|
|
59
|
+
const onEnter = (e) => {
|
|
60
|
+
if (e.pointerType !== "touch") return;
|
|
61
|
+
setTouchHoverPaused(true);
|
|
62
|
+
};
|
|
63
|
+
const onLeave = (e) => {
|
|
64
|
+
if (e.pointerType !== "touch") return;
|
|
65
|
+
setTouchHoverPaused(false);
|
|
66
|
+
};
|
|
67
|
+
container.addEventListener("pointerenter", onEnter);
|
|
68
|
+
container.addEventListener("pointerleave", onLeave);
|
|
69
|
+
return () => {
|
|
70
|
+
container.removeEventListener("pointerenter", onEnter);
|
|
71
|
+
container.removeEventListener("pointerleave", onLeave);
|
|
72
|
+
};
|
|
73
|
+
}, [isPausedOnHover]);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
const track = trackRef.current;
|
|
76
|
+
if (!track || !onCycleComplete && !onFinish) return;
|
|
77
|
+
const onIteration = onCycleComplete ? () => onCycleComplete() : void 0;
|
|
78
|
+
const onEnd = onFinish ? () => onFinish() : void 0;
|
|
79
|
+
if (onIteration) track.addEventListener("animationiteration", onIteration);
|
|
80
|
+
if (onEnd) track.addEventListener("animationend", onEnd);
|
|
81
|
+
return () => {
|
|
82
|
+
if (onIteration)
|
|
83
|
+
track.removeEventListener("animationiteration", onIteration);
|
|
84
|
+
if (onEnd) track.removeEventListener("animationend", onEnd);
|
|
85
|
+
};
|
|
86
|
+
}, [onCycleComplete, onFinish]);
|
|
87
|
+
const totalDistance = repeat ? sizes.group + gap : sizes.container + sizes.group;
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const container = containerRef.current;
|
|
90
|
+
if (!container) return;
|
|
91
|
+
if (!draggable && !isPausedOnClick) return;
|
|
92
|
+
let dragging = false;
|
|
93
|
+
let startPos = 0;
|
|
94
|
+
let startScrub = 0;
|
|
95
|
+
let wasDragged = false;
|
|
96
|
+
const onPointerDown = (e) => {
|
|
97
|
+
if (!draggable) return;
|
|
98
|
+
dragging = true;
|
|
99
|
+
wasDragged = false;
|
|
100
|
+
startPos = horiz ? e.clientX : e.clientY;
|
|
101
|
+
startScrub = scrubRef.current;
|
|
102
|
+
container.setPointerCapture(e.pointerId);
|
|
103
|
+
setIsDragging(true);
|
|
104
|
+
};
|
|
105
|
+
const onPointerMove = (e) => {
|
|
106
|
+
if (!dragging || totalDistance <= 0) return;
|
|
107
|
+
const pos = horiz ? e.clientX : e.clientY;
|
|
108
|
+
const delta = pos - startPos;
|
|
109
|
+
if (Math.abs(delta) > 3) wasDragged = true;
|
|
110
|
+
const timeDelta = -delta / totalDistance * speed;
|
|
111
|
+
scrubRef.current = startScrub + timeDelta;
|
|
112
|
+
if (trackRef.current) {
|
|
113
|
+
trackRef.current.style.animationDelay = `${delay - scrubRef.current}s`;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const onPointerUp = () => {
|
|
117
|
+
if (!dragging) return;
|
|
118
|
+
dragging = false;
|
|
119
|
+
setIsDragging(false);
|
|
120
|
+
};
|
|
121
|
+
const onClick = () => {
|
|
122
|
+
if (wasDragged) {
|
|
123
|
+
wasDragged = false;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (isPausedOnClick) setClickPaused((p) => !p);
|
|
127
|
+
};
|
|
128
|
+
container.addEventListener("pointerdown", onPointerDown);
|
|
129
|
+
container.addEventListener("pointermove", onPointerMove);
|
|
130
|
+
container.addEventListener("pointerup", onPointerUp);
|
|
131
|
+
container.addEventListener("pointercancel", onPointerUp);
|
|
132
|
+
container.addEventListener("click", onClick);
|
|
133
|
+
return () => {
|
|
134
|
+
container.removeEventListener("pointerdown", onPointerDown);
|
|
135
|
+
container.removeEventListener("pointermove", onPointerMove);
|
|
136
|
+
container.removeEventListener("pointerup", onPointerUp);
|
|
137
|
+
container.removeEventListener("pointercancel", onPointerUp);
|
|
138
|
+
container.removeEventListener("click", onClick);
|
|
78
139
|
};
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
140
|
+
}, [draggable, isPausedOnClick, horiz, totalDistance, speed, delay]);
|
|
141
|
+
const copies = repeat && sizes.group > 0 ? Math.ceil(sizes.container / sizes.group) + 1 : 1;
|
|
142
|
+
const active = sizes.group > 0 && sizes.container > 0;
|
|
143
|
+
const paused = isPaused || isPausedOnClick && clickPaused || isDragging || touchHoverPaused;
|
|
144
|
+
const css = useMemo(() => {
|
|
145
|
+
let s;
|
|
146
|
+
if (repeat) {
|
|
147
|
+
const dist = sizes.group + gap;
|
|
148
|
+
const dx = horiz ? `-${dist}px` : "0";
|
|
149
|
+
const dy = horiz ? "0" : `-${dist}px`;
|
|
150
|
+
s = `@keyframes ${uid}{to{transform:translate(${dx},${dy})}}`;
|
|
151
|
+
} else {
|
|
152
|
+
const fromVal = `${sizes.container}px`;
|
|
153
|
+
const toVal = `-${sizes.group}px`;
|
|
154
|
+
const fromX = horiz ? fromVal : "0";
|
|
155
|
+
const fromY = horiz ? "0" : fromVal;
|
|
156
|
+
const toX = horiz ? toVal : "0";
|
|
157
|
+
const toY = horiz ? "0" : toVal;
|
|
158
|
+
s = `@keyframes ${uid}{from{transform:translate(${fromX},${fromY})}to{transform:translate(${toX},${toY})}}`;
|
|
159
|
+
}
|
|
160
|
+
if (isPausedOnHover) {
|
|
161
|
+
s += `[data-rcm="${uid}"]:hover [data-rcm-t]{animation-play-state:paused!important}`;
|
|
162
|
+
}
|
|
163
|
+
if (isPausedOnMouseDown) {
|
|
164
|
+
s += `[data-rcm="${uid}"]:active [data-rcm-t]{animation-play-state:paused!important}`;
|
|
165
|
+
}
|
|
166
|
+
s += `@media(prefers-reduced-motion:reduce){[data-rcm="${uid}"] [data-rcm-t]{animation-play-state:paused!important}}`;
|
|
167
|
+
return s;
|
|
168
|
+
}, [uid, isPausedOnHover, isPausedOnMouseDown, sizes, gap, horiz, repeat]);
|
|
169
|
+
const gapStyle = `${gap}px`;
|
|
170
|
+
const gwPx = typeof gradientWidth === "number" ? `${gradientWidth}px` : gradientWidth;
|
|
171
|
+
const maskDir = horiz ? "to right" : "to bottom";
|
|
172
|
+
const mask = gradient ? `linear-gradient(${maskDir}, transparent, black ${gwPx}, black calc(100% - ${gwPx}), transparent)` : void 0;
|
|
173
|
+
const animDelay = delay - scrubRef.current;
|
|
174
|
+
return /* @__PURE__ */ jsxs(
|
|
175
|
+
"div",
|
|
176
|
+
{
|
|
177
|
+
ref: containerRef,
|
|
178
|
+
"data-rcm": uid,
|
|
179
|
+
className,
|
|
180
|
+
style: {
|
|
181
|
+
overflow: "hidden",
|
|
182
|
+
width: "100%",
|
|
183
|
+
height: "100%",
|
|
184
|
+
...mask ? { maskImage: mask, WebkitMaskImage: mask } : void 0,
|
|
185
|
+
...draggable ? {
|
|
186
|
+
cursor: isDragging ? "grabbing" : "grab",
|
|
187
|
+
userSelect: "none",
|
|
188
|
+
// Allow scrolling in the perpendicular axis so the page
|
|
189
|
+
// remains scrollable when the marquee is draggable.
|
|
190
|
+
touchAction: horiz ? "pan-y" : "pan-x"
|
|
191
|
+
} : isPausedOnClick ? { cursor: "pointer" } : void 0,
|
|
192
|
+
...userStyle
|
|
193
|
+
},
|
|
194
|
+
children: [
|
|
195
|
+
/* @__PURE__ */ jsx("style", { children: css }),
|
|
196
|
+
/* @__PURE__ */ jsx(
|
|
197
|
+
"div",
|
|
198
|
+
{
|
|
199
|
+
ref: trackRef,
|
|
200
|
+
"data-rcm-t": "",
|
|
201
|
+
style: {
|
|
202
|
+
display: "flex",
|
|
203
|
+
flexDirection: horiz ? "row" : "column",
|
|
204
|
+
alignItems: "center",
|
|
205
|
+
width: horiz ? "max-content" : "100%",
|
|
206
|
+
height: horiz ? "100%" : "max-content",
|
|
207
|
+
gap: gapStyle,
|
|
208
|
+
animation: active ? `${uid} ${speed}s linear ${loop === 0 ? "infinite" : loop}` : "none",
|
|
209
|
+
animationPlayState: paused ? "paused" : "running",
|
|
210
|
+
animationDirection: reverse ? "reverse" : "normal",
|
|
211
|
+
animationDelay: animDelay ? `${animDelay}s` : void 0,
|
|
212
|
+
willChange: active ? "transform" : void 0
|
|
213
|
+
},
|
|
214
|
+
children: Array.from({ length: copies }, (_, i) => /* @__PURE__ */ jsx(
|
|
215
|
+
"div",
|
|
216
|
+
{
|
|
217
|
+
ref: i === 0 ? groupRef : void 0,
|
|
218
|
+
"aria-hidden": i > 0 || void 0,
|
|
219
|
+
style: {
|
|
220
|
+
display: "flex",
|
|
221
|
+
flexDirection: horiz ? "row" : "column",
|
|
222
|
+
alignItems: "center",
|
|
223
|
+
flexShrink: 0,
|
|
224
|
+
gap: gapStyle
|
|
225
|
+
},
|
|
226
|
+
children
|
|
227
|
+
},
|
|
228
|
+
i
|
|
229
|
+
))
|
|
230
|
+
}
|
|
231
|
+
)
|
|
232
|
+
]
|
|
233
|
+
}
|
|
234
|
+
);
|
|
86
235
|
};
|
|
87
|
-
|
|
88
|
-
|
|
236
|
+
|
|
237
|
+
export { Marquee };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-css-marquee",
|
|
3
|
-
"description": "
|
|
4
|
-
"version": "0.0
|
|
3
|
+
"description": "Lightweight React marquee component powered by CSS animations. Horizontal and vertical scrolling, gradient fade, pause on hover/click, auto-fill, and more.",
|
|
4
|
+
"version": "1.0.0",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"url": "https://github.com/samuelweckstrom/react-css-marquee",
|
|
@@ -11,25 +11,45 @@
|
|
|
11
11
|
},
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"keywords": [
|
|
14
|
+
"react",
|
|
14
15
|
"marquee",
|
|
16
|
+
"react-marquee",
|
|
17
|
+
"scroll",
|
|
18
|
+
"scrolling",
|
|
19
|
+
"infinite-scroll",
|
|
15
20
|
"ticker",
|
|
21
|
+
"news-ticker",
|
|
16
22
|
"scroller",
|
|
17
|
-
"
|
|
18
|
-
"
|
|
23
|
+
"horizontal-scroll",
|
|
24
|
+
"vertical-scroll",
|
|
25
|
+
"auto-scroll",
|
|
26
|
+
"css-animation",
|
|
19
27
|
"banner",
|
|
20
|
-
"
|
|
21
|
-
"
|
|
28
|
+
"carousel",
|
|
29
|
+
"react-component",
|
|
30
|
+
"typescript",
|
|
31
|
+
"gradient",
|
|
32
|
+
"pause-on-hover",
|
|
33
|
+
"infinite-loop",
|
|
34
|
+
"lightweight"
|
|
22
35
|
],
|
|
23
36
|
"files": [
|
|
24
37
|
"dist",
|
|
25
38
|
"README.md"
|
|
26
39
|
],
|
|
27
|
-
"scripts": {
|
|
28
|
-
"start": "tsc -w",
|
|
29
|
-
"build": "tsc -b"
|
|
30
|
-
},
|
|
31
40
|
"peerDependencies": {
|
|
32
|
-
"react": "^16.8"
|
|
33
|
-
|
|
41
|
+
"react": "^16.8 || ^17 || ^18 || ^19"
|
|
42
|
+
},
|
|
43
|
+
"sideEffects": false,
|
|
44
|
+
"type": "module",
|
|
45
|
+
"exports": {
|
|
46
|
+
".": {
|
|
47
|
+
"import": "./dist/index.js",
|
|
48
|
+
"types": "./dist/index.d.ts"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"dev": "tsup --watch",
|
|
53
|
+
"build": "tsup"
|
|
34
54
|
}
|
|
35
|
-
}
|
|
55
|
+
}
|
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"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"}
|