@vysmo/slideshow-react 0.1.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 +21 -0
- package/README.md +115 -0
- package/dist/Slideshow.d.ts +62 -0
- package/dist/Slideshow.d.ts.map +1 -0
- package/dist/Slideshow.js +99 -0
- package/dist/Slideshow.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/use-slideshow.d.ts +13 -0
- package/dist/use-slideshow.d.ts.map +1 -0
- package/dist/use-slideshow.js +31 -0
- package/dist/use-slideshow.js.map +1 -0
- package/package.json +55 -0
- package/src/Slideshow.tsx +179 -0
- package/src/__tests__/Slideshow.test.tsx +111 -0
- package/src/__tests__/ssr.test.ts +14 -0
- package/src/index.ts +2 -0
- package/src/use-slideshow.ts +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Maesto LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# @vysmo/slideshow-react
|
|
2
|
+
|
|
3
|
+
React bindings for [`@vysmo/slideshow`](https://www.npmjs.com/package/@vysmo/slideshow). Drop-in image slideshow driven by any of the 60 [`@vysmo/transitions`](https://www.npmjs.com/package/@vysmo/transitions) shaders, with opt-in arrows / dots / counter / progress / captions — wrapped in one component plus a hook for imperative control.
|
|
4
|
+
|
|
5
|
+
[Live demos](https://vysmo.com/slideshow) · [Source](https://github.com/vysmodev)
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @vysmo/slideshow @vysmo/slideshow-react
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`react` ≥ 18 is a peer dependency. `@vysmo/transitions` is required if you want to pass a specific transition (`dissolve` is the default).
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { Slideshow } from "@vysmo/slideshow-react";
|
|
19
|
+
import { paintBleed } from "@vysmo/transitions";
|
|
20
|
+
|
|
21
|
+
export function Hero() {
|
|
22
|
+
return (
|
|
23
|
+
<Slideshow
|
|
24
|
+
slides={["/01.jpg", "/02.jpg", "/03.jpg"]}
|
|
25
|
+
transition={paintBleed}
|
|
26
|
+
transitionDuration={900}
|
|
27
|
+
autoplayDelay={4000}
|
|
28
|
+
arrows
|
|
29
|
+
dots
|
|
30
|
+
style={{ width: "100%", aspectRatio: "16 / 9" }}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The component renders a `<div>`, mounts the slideshow into it, and tears down on unmount. Click halves, keyboard nav, and autoplay are wired by default; arrows / dots / counter / progress / captions are opt-in.
|
|
37
|
+
|
|
38
|
+
## Per-slide transition
|
|
39
|
+
|
|
40
|
+
Pass a function `(from, to) => Transition` to vary the transition per slide change:
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { paintBleed, glitch, ripple, crossZoom } from "@vysmo/transitions";
|
|
44
|
+
|
|
45
|
+
const transitions = [paintBleed, glitch, ripple, crossZoom];
|
|
46
|
+
|
|
47
|
+
<Slideshow
|
|
48
|
+
slides={[...]}
|
|
49
|
+
transition={(from, to) => transitions[to % transitions.length]!}
|
|
50
|
+
/>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Imperative control
|
|
54
|
+
|
|
55
|
+
For custom Next/Prev buttons, scroll-bound `go()`, autoplay toggles, etc., use the hook and call methods on the returned handle:
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { useRef } from "react";
|
|
59
|
+
import { useSlideshow } from "@vysmo/slideshow-react";
|
|
60
|
+
|
|
61
|
+
function ControlledSlideshow({ slides }) {
|
|
62
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
63
|
+
const slideshow = useSlideshow(ref, { slides, autoplayDelay: 4000 });
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<>
|
|
67
|
+
<div ref={ref} style={{ width: "100%", aspectRatio: "16 / 9" }} />
|
|
68
|
+
<button onClick={() => slideshow?.prev()}>‹</button>
|
|
69
|
+
<button onClick={() => slideshow?.next()}>›</button>
|
|
70
|
+
<button onClick={() => slideshow?.isPlaying ? slideshow.pause() : slideshow?.play()}>
|
|
71
|
+
Play / Pause
|
|
72
|
+
</button>
|
|
73
|
+
</>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The handle is `null` until the slideshow is mounted; use optional chaining or a guard.
|
|
79
|
+
|
|
80
|
+
## Props
|
|
81
|
+
|
|
82
|
+
| Prop | Type | Default | Notes |
|
|
83
|
+
|---|---|---|---|
|
|
84
|
+
| `slides` | `readonly SlideSource[]` | — | URLs (decoded), `HTMLImageElement`s, or canvases. |
|
|
85
|
+
| `initial` | `number` | `0` | Starting index. |
|
|
86
|
+
| `transition` | `Transition \| (from, to) => Transition` | `dissolve` | Single transition or per-slide selector. |
|
|
87
|
+
| `transitionDuration` | `number` | `800` | Transition ms. |
|
|
88
|
+
| `ease` | `EasingFn` | linear | Easing for the transition. |
|
|
89
|
+
| `autoplayDelay` | `number` | — | Dwell time in ms; omit / `0` to disable. |
|
|
90
|
+
| `autoplay` | `boolean` | `true` if `autoplayDelay > 0` | Start autoplay on mount. |
|
|
91
|
+
| `loop` | `boolean` | `true` | Wrap last → first. |
|
|
92
|
+
| `clickNavigation` | `boolean` | `true` | Click halves to advance. |
|
|
93
|
+
| `keyboardNavigation` | `boolean` | `true` | Arrow keys, Home/End, Space. |
|
|
94
|
+
| `pauseOnHidden` | `boolean` | `true` | Pause autoplay while the tab is hidden. |
|
|
95
|
+
| `pauseOnHover` | `boolean` | `false` | Pause autoplay on hover. |
|
|
96
|
+
| `swipeNavigation` | `boolean \| SwipeOptions` | `false` | Touch / pointer swipe. |
|
|
97
|
+
| `arrows` | `boolean \| ArrowsOptions` | `false` | Visible nav arrows. |
|
|
98
|
+
| `dots` | `boolean \| DotsOptions` | `false` | Page-indicator dots. |
|
|
99
|
+
| `counter` | `boolean \| CounterOptions` | `false` | Slide-counter overlay. |
|
|
100
|
+
| `progress` | `boolean \| ProgressOptions` | `false` | Autoplay countdown bar. |
|
|
101
|
+
| `captions` | `false \| CaptionsOptions` | `false` | Per-slide caption overlay. |
|
|
102
|
+
| `ariaLabel` | `string` | `"Slideshow"` | Accessible label. |
|
|
103
|
+
| `onChange` | `(current, previous) => void` | — | Slide change callback. |
|
|
104
|
+
| `onTransitionStart` | `(from, to) => void` | — | Transition begins. |
|
|
105
|
+
| `onTransitionEnd` | `(from, to) => void` | — | Transition ends. |
|
|
106
|
+
| `className` | `string` | — | Forwarded to the host `<div>`. |
|
|
107
|
+
| `style` | `CSSProperties` | — | Forwarded to the host `<div>`. Size the slideshow here. |
|
|
108
|
+
|
|
109
|
+
## SSR
|
|
110
|
+
|
|
111
|
+
The wrapper is SSR-safe: `useEffect` bodies don't run on the server, and the module body itself doesn't touch `window` / `document`. Server renders an empty `<div>`; client mounts the slideshow.
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { CSSProperties, ReactElement } from "react";
|
|
2
|
+
import { type SlideshowOptions, type SlideSource, type ArrowsOptions, type DotsOptions, type CounterOptions, type ProgressOptions, type CaptionsOptions, type SwipeOptions } from "@vysmo/slideshow";
|
|
3
|
+
export interface SlideshowProps {
|
|
4
|
+
/** Slide sources. URL strings (decoded), `HTMLImageElement`s, or canvases. */
|
|
5
|
+
slides: readonly SlideSource[];
|
|
6
|
+
/** Starting slide index. Default `0`. */
|
|
7
|
+
initial?: number;
|
|
8
|
+
/** Transition (or `(from, to) => Transition` selector for variety). Default `dissolve`. */
|
|
9
|
+
transition?: SlideshowOptions["transition"];
|
|
10
|
+
/** Transition duration ms. Default `800`. */
|
|
11
|
+
transitionDuration?: number;
|
|
12
|
+
/** Easing for transition progress. Default linear. */
|
|
13
|
+
ease?: SlideshowOptions["ease"];
|
|
14
|
+
/** Autoplay dwell time ms. `0` / omit to disable. */
|
|
15
|
+
autoplayDelay?: number;
|
|
16
|
+
/** Start autoplay on mount. Defaults to `true` when `autoplayDelay > 0`. */
|
|
17
|
+
autoplay?: boolean;
|
|
18
|
+
/** Wrap last → first. Default `true`. */
|
|
19
|
+
loop?: boolean;
|
|
20
|
+
/** Click halves to navigate. Default `true`. */
|
|
21
|
+
clickNavigation?: boolean;
|
|
22
|
+
/** Arrow keys / Home / End / Space. Default `true`. */
|
|
23
|
+
keyboardNavigation?: boolean;
|
|
24
|
+
/** Pause autoplay while the tab is hidden. Default `true`. */
|
|
25
|
+
pauseOnHidden?: boolean;
|
|
26
|
+
/** Pause autoplay while the pointer is over the slideshow. Default `false`. */
|
|
27
|
+
pauseOnHover?: boolean;
|
|
28
|
+
/** Touch / pointer swipe. */
|
|
29
|
+
swipeNavigation?: boolean | SwipeOptions;
|
|
30
|
+
/** Visible nav arrows. */
|
|
31
|
+
arrows?: boolean | ArrowsOptions;
|
|
32
|
+
/** Page-indicator dots. */
|
|
33
|
+
dots?: boolean | DotsOptions;
|
|
34
|
+
/** Slide-counter text overlay. */
|
|
35
|
+
counter?: boolean | CounterOptions;
|
|
36
|
+
/** Autoplay countdown bar. */
|
|
37
|
+
progress?: boolean | ProgressOptions;
|
|
38
|
+
/** Per-slide caption overlay. */
|
|
39
|
+
captions?: false | CaptionsOptions;
|
|
40
|
+
/** Accessible label. Default `"Slideshow"`. */
|
|
41
|
+
ariaLabel?: string;
|
|
42
|
+
/** Fires when the active slide changes. */
|
|
43
|
+
onChange?: (current: number, previous: number) => void;
|
|
44
|
+
/** Fires when a transition begins. */
|
|
45
|
+
onTransitionStart?: (from: number, to: number) => void;
|
|
46
|
+
/** Fires when a transition finishes. */
|
|
47
|
+
onTransitionEnd?: (from: number, to: number) => void;
|
|
48
|
+
/** Forwarded to the host `<div>`. */
|
|
49
|
+
className?: string;
|
|
50
|
+
/** Forwarded to the host `<div>`. Size the slideshow here (width / height). */
|
|
51
|
+
style?: CSSProperties;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* React wrapper around `@vysmo/slideshow`. Renders a `<div>` host,
|
|
55
|
+
* mounts the slideshow into it, and tears down on unmount.
|
|
56
|
+
*
|
|
57
|
+
* For imperative control (custom Next/Prev buttons, scroll-driven
|
|
58
|
+
* `go()`), reach for `useSlideshow(containerRef, options)` instead — it
|
|
59
|
+
* returns the handle.
|
|
60
|
+
*/
|
|
61
|
+
export declare function Slideshow({ slides, initial, transition, transitionDuration, ease, autoplayDelay, autoplay, loop, clickNavigation, keyboardNavigation, pauseOnHidden, pauseOnHover, swipeNavigation, arrows, dots, counter, progress, captions, ariaLabel, onChange, onTransitionStart, onTransitionEnd, className, style, }: SlideshowProps): ReactElement;
|
|
62
|
+
//# sourceMappingURL=Slideshow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Slideshow.d.ts","sourceRoot":"","sources":["../src/Slideshow.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AACzD,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,YAAY,EAClB,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,cAAc;IAC7B,8EAA8E;IAC9E,MAAM,EAAE,SAAS,WAAW,EAAE,CAAC;IAC/B,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2FAA2F;IAC3F,UAAU,CAAC,EAAE,gBAAgB,CAAC,YAAY,CAAC,CAAC;IAC5C,6CAA6C;IAC7C,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,sDAAsD;IACtD,IAAI,CAAC,EAAE,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAChC,qDAAqD;IACrD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yCAAyC;IACzC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,gDAAgD;IAChD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,uDAAuD;IACvD,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,8DAA8D;IAC9D,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,+EAA+E;IAC/E,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,6BAA6B;IAC7B,eAAe,CAAC,EAAE,OAAO,GAAG,YAAY,CAAC;IACzC,0BAA0B;IAC1B,MAAM,CAAC,EAAE,OAAO,GAAG,aAAa,CAAC;IACjC,2BAA2B;IAC3B,IAAI,CAAC,EAAE,OAAO,GAAG,WAAW,CAAC;IAC7B,kCAAkC;IAClC,OAAO,CAAC,EAAE,OAAO,GAAG,cAAc,CAAC;IACnC,8BAA8B;IAC9B,QAAQ,CAAC,EAAE,OAAO,GAAG,eAAe,CAAC;IACrC,iCAAiC;IACjC,QAAQ,CAAC,EAAE,KAAK,GAAG,eAAe,CAAC;IACnC,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACvD,sCAAsC;IACtC,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACvD,wCAAwC;IACxC,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACrD,qCAAqC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,EACxB,MAAM,EACN,OAAO,EACP,UAAU,EACV,kBAAkB,EAClB,IAAI,EACJ,aAAa,EACb,QAAQ,EACR,IAAI,EACJ,eAAe,EACf,kBAAkB,EAClB,aAAa,EACb,YAAY,EACZ,eAAe,EACf,MAAM,EACN,IAAI,EACJ,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,iBAAiB,EACjB,eAAe,EACf,SAAS,EACT,KAAK,GACN,EAAE,cAAc,GAAG,YAAY,CA6E/B"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
import { createSlideshow, } from "@vysmo/slideshow";
|
|
5
|
+
/**
|
|
6
|
+
* React wrapper around `@vysmo/slideshow`. Renders a `<div>` host,
|
|
7
|
+
* mounts the slideshow into it, and tears down on unmount.
|
|
8
|
+
*
|
|
9
|
+
* For imperative control (custom Next/Prev buttons, scroll-driven
|
|
10
|
+
* `go()`), reach for `useSlideshow(containerRef, options)` instead — it
|
|
11
|
+
* returns the handle.
|
|
12
|
+
*/
|
|
13
|
+
export function Slideshow({ slides, initial, transition, transitionDuration, ease, autoplayDelay, autoplay, loop, clickNavigation, keyboardNavigation, pauseOnHidden, pauseOnHover, swipeNavigation, arrows, dots, counter, progress, captions, ariaLabel, onChange, onTransitionStart, onTransitionEnd, className, style, }) {
|
|
14
|
+
const containerRef = useRef(null);
|
|
15
|
+
const handleRef = useRef(null);
|
|
16
|
+
// Stash callbacks so reference changes don't recreate the slideshow.
|
|
17
|
+
const onChangeRef = useRef(onChange);
|
|
18
|
+
onChangeRef.current = onChange;
|
|
19
|
+
const onTransitionStartRef = useRef(onTransitionStart);
|
|
20
|
+
onTransitionStartRef.current = onTransitionStart;
|
|
21
|
+
const onTransitionEndRef = useRef(onTransitionEnd);
|
|
22
|
+
onTransitionEndRef.current = onTransitionEnd;
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const container = containerRef.current;
|
|
25
|
+
if (!container)
|
|
26
|
+
return;
|
|
27
|
+
const opts = { container, slides };
|
|
28
|
+
if (initial !== undefined)
|
|
29
|
+
opts.initial = initial;
|
|
30
|
+
if (transition !== undefined)
|
|
31
|
+
opts.transition = transition;
|
|
32
|
+
if (transitionDuration !== undefined)
|
|
33
|
+
opts.transitionDuration = transitionDuration;
|
|
34
|
+
if (ease !== undefined)
|
|
35
|
+
opts.ease = ease;
|
|
36
|
+
if (autoplayDelay !== undefined)
|
|
37
|
+
opts.autoplayDelay = autoplayDelay;
|
|
38
|
+
if (autoplay !== undefined)
|
|
39
|
+
opts.autoplay = autoplay;
|
|
40
|
+
if (loop !== undefined)
|
|
41
|
+
opts.loop = loop;
|
|
42
|
+
if (clickNavigation !== undefined)
|
|
43
|
+
opts.clickNavigation = clickNavigation;
|
|
44
|
+
if (keyboardNavigation !== undefined)
|
|
45
|
+
opts.keyboardNavigation = keyboardNavigation;
|
|
46
|
+
if (pauseOnHidden !== undefined)
|
|
47
|
+
opts.pauseOnHidden = pauseOnHidden;
|
|
48
|
+
if (pauseOnHover !== undefined)
|
|
49
|
+
opts.pauseOnHover = pauseOnHover;
|
|
50
|
+
if (swipeNavigation !== undefined)
|
|
51
|
+
opts.swipeNavigation = swipeNavigation;
|
|
52
|
+
if (arrows !== undefined)
|
|
53
|
+
opts.arrows = arrows;
|
|
54
|
+
if (dots !== undefined)
|
|
55
|
+
opts.dots = dots;
|
|
56
|
+
if (counter !== undefined)
|
|
57
|
+
opts.counter = counter;
|
|
58
|
+
if (progress !== undefined)
|
|
59
|
+
opts.progress = progress;
|
|
60
|
+
if (captions !== undefined)
|
|
61
|
+
opts.captions = captions;
|
|
62
|
+
if (ariaLabel !== undefined)
|
|
63
|
+
opts.ariaLabel = ariaLabel;
|
|
64
|
+
const handle = createSlideshow(opts);
|
|
65
|
+
handleRef.current = handle;
|
|
66
|
+
const offChange = handle.on("change", (cur, prev) => onChangeRef.current?.(cur, prev));
|
|
67
|
+
const offStart = handle.on("transitionstart", (from, to) => onTransitionStartRef.current?.(from, to));
|
|
68
|
+
const offEnd = handle.on("transitionend", (from, to) => onTransitionEndRef.current?.(from, to));
|
|
69
|
+
return () => {
|
|
70
|
+
offChange();
|
|
71
|
+
offStart();
|
|
72
|
+
offEnd();
|
|
73
|
+
handle.destroy();
|
|
74
|
+
handleRef.current = null;
|
|
75
|
+
};
|
|
76
|
+
}, [
|
|
77
|
+
slides,
|
|
78
|
+
initial,
|
|
79
|
+
transition,
|
|
80
|
+
transitionDuration,
|
|
81
|
+
ease,
|
|
82
|
+
autoplayDelay,
|
|
83
|
+
autoplay,
|
|
84
|
+
loop,
|
|
85
|
+
clickNavigation,
|
|
86
|
+
keyboardNavigation,
|
|
87
|
+
pauseOnHidden,
|
|
88
|
+
pauseOnHover,
|
|
89
|
+
swipeNavigation,
|
|
90
|
+
arrows,
|
|
91
|
+
dots,
|
|
92
|
+
counter,
|
|
93
|
+
progress,
|
|
94
|
+
captions,
|
|
95
|
+
ariaLabel,
|
|
96
|
+
]);
|
|
97
|
+
return _jsx("div", { ref: containerRef, className: className, style: style });
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=Slideshow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Slideshow.js","sourceRoot":"","sources":["../src/Slideshow.tsx"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAuB,MAAM,gBAAgB,CAAC;AAChE,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RefObject } from "react";
|
|
2
|
+
import { type SlideshowHandle, type SlideshowOptions } from "@vysmo/slideshow";
|
|
3
|
+
/**
|
|
4
|
+
* Mount a slideshow into a container ref you own and get the handle for
|
|
5
|
+
* imperative control (`next` / `prev` / `go` / `play` / `pause`).
|
|
6
|
+
*
|
|
7
|
+
* Re-creates the slideshow when *any* member of the `options` object
|
|
8
|
+
* changes — memoize the options and the `slides` array if you want
|
|
9
|
+
* stability. Returns `null` until the container is in the DOM and the
|
|
10
|
+
* slideshow is mounted.
|
|
11
|
+
*/
|
|
12
|
+
export declare function useSlideshow(containerRef: RefObject<HTMLElement | null>, options: Omit<SlideshowOptions, "container">): SlideshowHandle | null;
|
|
13
|
+
//# sourceMappingURL=use-slideshow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-slideshow.d.ts","sourceRoot":"","sources":["../src/use-slideshow.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACtB,MAAM,kBAAkB,CAAC;AAE1B;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAC1B,YAAY,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,EAC3C,OAAO,EAAE,IAAI,CAAC,gBAAgB,EAAE,WAAW,CAAC,GAC3C,eAAe,GAAG,IAAI,CAkBxB"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { createSlideshow, } from "@vysmo/slideshow";
|
|
4
|
+
/**
|
|
5
|
+
* Mount a slideshow into a container ref you own and get the handle for
|
|
6
|
+
* imperative control (`next` / `prev` / `go` / `play` / `pause`).
|
|
7
|
+
*
|
|
8
|
+
* Re-creates the slideshow when *any* member of the `options` object
|
|
9
|
+
* changes — memoize the options and the `slides` array if you want
|
|
10
|
+
* stability. Returns `null` until the container is in the DOM and the
|
|
11
|
+
* slideshow is mounted.
|
|
12
|
+
*/
|
|
13
|
+
export function useSlideshow(containerRef, options) {
|
|
14
|
+
const [handle, setHandle] = useState(null);
|
|
15
|
+
const optionsRef = useRef(options);
|
|
16
|
+
optionsRef.current = options;
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const container = containerRef.current;
|
|
19
|
+
if (!container)
|
|
20
|
+
return;
|
|
21
|
+
const h = createSlideshow({ ...optionsRef.current, container });
|
|
22
|
+
setHandle(h);
|
|
23
|
+
return () => {
|
|
24
|
+
h.destroy();
|
|
25
|
+
setHandle(null);
|
|
26
|
+
};
|
|
27
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
28
|
+
}, [containerRef, options]);
|
|
29
|
+
return handle;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=use-slideshow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-slideshow.js","sourceRoot":"","sources":["../src/use-slideshow.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEpD,OAAO,EACL,eAAe,GAGhB,MAAM,kBAAkB,CAAC;AAE1B;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAC1B,YAA2C,EAC3C,OAA4C;IAE5C,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAyB,IAAI,CAAC,CAAC;IACnE,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IACnC,UAAU,CAAC,OAAO,GAAG,OAAO,CAAC;IAE7B,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,SAAS,GAAG,YAAY,CAAC,OAAO,CAAC;QACvC,IAAI,CAAC,SAAS;YAAE,OAAO;QACvB,MAAM,CAAC,GAAG,eAAe,CAAC,EAAE,GAAG,UAAU,CAAC,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC;QAChE,SAAS,CAAC,CAAC,CAAC,CAAC;QACb,OAAO,GAAG,EAAE;YACV,CAAC,CAAC,OAAO,EAAE,CAAC;YACZ,SAAS,CAAC,IAAI,CAAC,CAAC;QAClB,CAAC,CAAC;QACF,uDAAuD;IACzD,CAAC,EAAE,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,CAAC;IAE5B,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vysmo/slideshow-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React bindings for @vysmo/slideshow — a <Slideshow> component that drives any of the 60 WebGL transitions between slides, with opt-in arrows / dots / counter / progress / captions; useSlideshow hook for imperative control.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"react",
|
|
8
|
+
"slideshow",
|
|
9
|
+
"carousel",
|
|
10
|
+
"webgl",
|
|
11
|
+
"transitions",
|
|
12
|
+
"vysmo"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"module": "./src/index.ts",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"src",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"react": ">=18",
|
|
33
|
+
"@vysmo/slideshow": "0.1.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/react": "^19.2.14",
|
|
37
|
+
"@types/react-dom": "^19.2.3",
|
|
38
|
+
"@vitest/browser": "^3.2.4",
|
|
39
|
+
"playwright": "^1.59.1",
|
|
40
|
+
"react": "^19.2.5",
|
|
41
|
+
"react-dom": "^19.2.5",
|
|
42
|
+
"typescript": "^5.6.3",
|
|
43
|
+
"vitest": "^3.2.4",
|
|
44
|
+
"@vysmo/slideshow": "0.1.0",
|
|
45
|
+
"@vysmo/transitions": "0.1.0"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsc -p tsconfig.json",
|
|
49
|
+
"typecheck": "tsc -p tsconfig.typecheck.json",
|
|
50
|
+
"test": "vitest run && vitest run --config vitest.ssr.config.ts",
|
|
51
|
+
"test:browser": "vitest run",
|
|
52
|
+
"test:ssr": "vitest run --config vitest.ssr.config.ts",
|
|
53
|
+
"test:watch": "vitest"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
import type { CSSProperties, ReactElement } from "react";
|
|
5
|
+
import {
|
|
6
|
+
createSlideshow,
|
|
7
|
+
type SlideshowHandle,
|
|
8
|
+
type SlideshowOptions,
|
|
9
|
+
type SlideSource,
|
|
10
|
+
type ArrowsOptions,
|
|
11
|
+
type DotsOptions,
|
|
12
|
+
type CounterOptions,
|
|
13
|
+
type ProgressOptions,
|
|
14
|
+
type CaptionsOptions,
|
|
15
|
+
type SwipeOptions,
|
|
16
|
+
} from "@vysmo/slideshow";
|
|
17
|
+
|
|
18
|
+
export interface SlideshowProps {
|
|
19
|
+
/** Slide sources. URL strings (decoded), `HTMLImageElement`s, or canvases. */
|
|
20
|
+
slides: readonly SlideSource[];
|
|
21
|
+
/** Starting slide index. Default `0`. */
|
|
22
|
+
initial?: number;
|
|
23
|
+
/** Transition (or `(from, to) => Transition` selector for variety). Default `dissolve`. */
|
|
24
|
+
transition?: SlideshowOptions["transition"];
|
|
25
|
+
/** Transition duration ms. Default `800`. */
|
|
26
|
+
transitionDuration?: number;
|
|
27
|
+
/** Easing for transition progress. Default linear. */
|
|
28
|
+
ease?: SlideshowOptions["ease"];
|
|
29
|
+
/** Autoplay dwell time ms. `0` / omit to disable. */
|
|
30
|
+
autoplayDelay?: number;
|
|
31
|
+
/** Start autoplay on mount. Defaults to `true` when `autoplayDelay > 0`. */
|
|
32
|
+
autoplay?: boolean;
|
|
33
|
+
/** Wrap last → first. Default `true`. */
|
|
34
|
+
loop?: boolean;
|
|
35
|
+
/** Click halves to navigate. Default `true`. */
|
|
36
|
+
clickNavigation?: boolean;
|
|
37
|
+
/** Arrow keys / Home / End / Space. Default `true`. */
|
|
38
|
+
keyboardNavigation?: boolean;
|
|
39
|
+
/** Pause autoplay while the tab is hidden. Default `true`. */
|
|
40
|
+
pauseOnHidden?: boolean;
|
|
41
|
+
/** Pause autoplay while the pointer is over the slideshow. Default `false`. */
|
|
42
|
+
pauseOnHover?: boolean;
|
|
43
|
+
/** Touch / pointer swipe. */
|
|
44
|
+
swipeNavigation?: boolean | SwipeOptions;
|
|
45
|
+
/** Visible nav arrows. */
|
|
46
|
+
arrows?: boolean | ArrowsOptions;
|
|
47
|
+
/** Page-indicator dots. */
|
|
48
|
+
dots?: boolean | DotsOptions;
|
|
49
|
+
/** Slide-counter text overlay. */
|
|
50
|
+
counter?: boolean | CounterOptions;
|
|
51
|
+
/** Autoplay countdown bar. */
|
|
52
|
+
progress?: boolean | ProgressOptions;
|
|
53
|
+
/** Per-slide caption overlay. */
|
|
54
|
+
captions?: false | CaptionsOptions;
|
|
55
|
+
/** Accessible label. Default `"Slideshow"`. */
|
|
56
|
+
ariaLabel?: string;
|
|
57
|
+
/** Fires when the active slide changes. */
|
|
58
|
+
onChange?: (current: number, previous: number) => void;
|
|
59
|
+
/** Fires when a transition begins. */
|
|
60
|
+
onTransitionStart?: (from: number, to: number) => void;
|
|
61
|
+
/** Fires when a transition finishes. */
|
|
62
|
+
onTransitionEnd?: (from: number, to: number) => void;
|
|
63
|
+
/** Forwarded to the host `<div>`. */
|
|
64
|
+
className?: string;
|
|
65
|
+
/** Forwarded to the host `<div>`. Size the slideshow here (width / height). */
|
|
66
|
+
style?: CSSProperties;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* React wrapper around `@vysmo/slideshow`. Renders a `<div>` host,
|
|
71
|
+
* mounts the slideshow into it, and tears down on unmount.
|
|
72
|
+
*
|
|
73
|
+
* For imperative control (custom Next/Prev buttons, scroll-driven
|
|
74
|
+
* `go()`), reach for `useSlideshow(containerRef, options)` instead — it
|
|
75
|
+
* returns the handle.
|
|
76
|
+
*/
|
|
77
|
+
export function Slideshow({
|
|
78
|
+
slides,
|
|
79
|
+
initial,
|
|
80
|
+
transition,
|
|
81
|
+
transitionDuration,
|
|
82
|
+
ease,
|
|
83
|
+
autoplayDelay,
|
|
84
|
+
autoplay,
|
|
85
|
+
loop,
|
|
86
|
+
clickNavigation,
|
|
87
|
+
keyboardNavigation,
|
|
88
|
+
pauseOnHidden,
|
|
89
|
+
pauseOnHover,
|
|
90
|
+
swipeNavigation,
|
|
91
|
+
arrows,
|
|
92
|
+
dots,
|
|
93
|
+
counter,
|
|
94
|
+
progress,
|
|
95
|
+
captions,
|
|
96
|
+
ariaLabel,
|
|
97
|
+
onChange,
|
|
98
|
+
onTransitionStart,
|
|
99
|
+
onTransitionEnd,
|
|
100
|
+
className,
|
|
101
|
+
style,
|
|
102
|
+
}: SlideshowProps): ReactElement {
|
|
103
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
104
|
+
const handleRef = useRef<SlideshowHandle | null>(null);
|
|
105
|
+
|
|
106
|
+
// Stash callbacks so reference changes don't recreate the slideshow.
|
|
107
|
+
const onChangeRef = useRef(onChange);
|
|
108
|
+
onChangeRef.current = onChange;
|
|
109
|
+
const onTransitionStartRef = useRef(onTransitionStart);
|
|
110
|
+
onTransitionStartRef.current = onTransitionStart;
|
|
111
|
+
const onTransitionEndRef = useRef(onTransitionEnd);
|
|
112
|
+
onTransitionEndRef.current = onTransitionEnd;
|
|
113
|
+
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
const container = containerRef.current;
|
|
116
|
+
if (!container) return;
|
|
117
|
+
|
|
118
|
+
const opts: SlideshowOptions = { container, slides };
|
|
119
|
+
if (initial !== undefined) opts.initial = initial;
|
|
120
|
+
if (transition !== undefined) opts.transition = transition;
|
|
121
|
+
if (transitionDuration !== undefined) opts.transitionDuration = transitionDuration;
|
|
122
|
+
if (ease !== undefined) opts.ease = ease;
|
|
123
|
+
if (autoplayDelay !== undefined) opts.autoplayDelay = autoplayDelay;
|
|
124
|
+
if (autoplay !== undefined) opts.autoplay = autoplay;
|
|
125
|
+
if (loop !== undefined) opts.loop = loop;
|
|
126
|
+
if (clickNavigation !== undefined) opts.clickNavigation = clickNavigation;
|
|
127
|
+
if (keyboardNavigation !== undefined) opts.keyboardNavigation = keyboardNavigation;
|
|
128
|
+
if (pauseOnHidden !== undefined) opts.pauseOnHidden = pauseOnHidden;
|
|
129
|
+
if (pauseOnHover !== undefined) opts.pauseOnHover = pauseOnHover;
|
|
130
|
+
if (swipeNavigation !== undefined) opts.swipeNavigation = swipeNavigation;
|
|
131
|
+
if (arrows !== undefined) opts.arrows = arrows;
|
|
132
|
+
if (dots !== undefined) opts.dots = dots;
|
|
133
|
+
if (counter !== undefined) opts.counter = counter;
|
|
134
|
+
if (progress !== undefined) opts.progress = progress;
|
|
135
|
+
if (captions !== undefined) opts.captions = captions;
|
|
136
|
+
if (ariaLabel !== undefined) opts.ariaLabel = ariaLabel;
|
|
137
|
+
|
|
138
|
+
const handle = createSlideshow(opts);
|
|
139
|
+
handleRef.current = handle;
|
|
140
|
+
|
|
141
|
+
const offChange = handle.on("change", (cur, prev) => onChangeRef.current?.(cur, prev));
|
|
142
|
+
const offStart = handle.on("transitionstart", (from, to) =>
|
|
143
|
+
onTransitionStartRef.current?.(from, to),
|
|
144
|
+
);
|
|
145
|
+
const offEnd = handle.on("transitionend", (from, to) =>
|
|
146
|
+
onTransitionEndRef.current?.(from, to),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
return () => {
|
|
150
|
+
offChange();
|
|
151
|
+
offStart();
|
|
152
|
+
offEnd();
|
|
153
|
+
handle.destroy();
|
|
154
|
+
handleRef.current = null;
|
|
155
|
+
};
|
|
156
|
+
}, [
|
|
157
|
+
slides,
|
|
158
|
+
initial,
|
|
159
|
+
transition,
|
|
160
|
+
transitionDuration,
|
|
161
|
+
ease,
|
|
162
|
+
autoplayDelay,
|
|
163
|
+
autoplay,
|
|
164
|
+
loop,
|
|
165
|
+
clickNavigation,
|
|
166
|
+
keyboardNavigation,
|
|
167
|
+
pauseOnHidden,
|
|
168
|
+
pauseOnHover,
|
|
169
|
+
swipeNavigation,
|
|
170
|
+
arrows,
|
|
171
|
+
dots,
|
|
172
|
+
counter,
|
|
173
|
+
progress,
|
|
174
|
+
captions,
|
|
175
|
+
ariaLabel,
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
return <div ref={containerRef} className={className} style={style} />;
|
|
179
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
3
|
+
import { act } from "react";
|
|
4
|
+
import { paintBleed } from "@vysmo/transitions";
|
|
5
|
+
import { Slideshow } from "../Slideshow.js";
|
|
6
|
+
|
|
7
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
8
|
+
|
|
9
|
+
function makeSolid(r: number, g: number, b: number, size = 32): HTMLCanvasElement {
|
|
10
|
+
const canvas = document.createElement("canvas");
|
|
11
|
+
canvas.width = size;
|
|
12
|
+
canvas.height = size;
|
|
13
|
+
const ctx = canvas.getContext("2d");
|
|
14
|
+
if (!ctx) throw new Error("2D context unavailable");
|
|
15
|
+
ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
|
|
16
|
+
ctx.fillRect(0, 0, size, size);
|
|
17
|
+
return canvas;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("<Slideshow>", () => {
|
|
21
|
+
let container: HTMLDivElement;
|
|
22
|
+
let root: Root;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
container = document.createElement("div");
|
|
26
|
+
container.style.width = "200px";
|
|
27
|
+
container.style.height = "200px";
|
|
28
|
+
document.body.appendChild(container);
|
|
29
|
+
root = createRoot(container);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(async () => {
|
|
33
|
+
await act(async () => {
|
|
34
|
+
root.unmount();
|
|
35
|
+
});
|
|
36
|
+
container.remove();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("mounts a slideshow into its host div", async () => {
|
|
40
|
+
const slides = [makeSolid(255, 0, 0), makeSolid(0, 255, 0), makeSolid(0, 0, 255)];
|
|
41
|
+
await act(async () => {
|
|
42
|
+
root.render(<Slideshow slides={slides} />);
|
|
43
|
+
});
|
|
44
|
+
const canvas = container.querySelector("canvas");
|
|
45
|
+
expect(canvas).toBeTruthy();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("forwards className and style to the host", async () => {
|
|
49
|
+
const slides = [makeSolid(255, 0, 0), makeSolid(0, 0, 255)];
|
|
50
|
+
await act(async () => {
|
|
51
|
+
root.render(
|
|
52
|
+
<Slideshow
|
|
53
|
+
slides={slides}
|
|
54
|
+
className="my-slideshow"
|
|
55
|
+
style={{ borderRadius: "8px" }}
|
|
56
|
+
/>,
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
const host = container.firstElementChild as HTMLDivElement;
|
|
60
|
+
expect(host.classList.contains("my-slideshow")).toBe(true);
|
|
61
|
+
expect(host.style.borderRadius).toBe("8px");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("renders chrome (arrows, dots) when opted in", async () => {
|
|
65
|
+
const slides = [makeSolid(255, 0, 0), makeSolid(0, 255, 0), makeSolid(0, 0, 255)];
|
|
66
|
+
await act(async () => {
|
|
67
|
+
root.render(<Slideshow slides={slides} arrows dots />);
|
|
68
|
+
});
|
|
69
|
+
// Both chrome elements live inside the slideshow's wrapper div.
|
|
70
|
+
const buttons = container.querySelectorAll("button");
|
|
71
|
+
expect(buttons.length).toBeGreaterThan(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("wires onChange when a slide change happens", async () => {
|
|
75
|
+
const slides = [makeSolid(255, 0, 0), makeSolid(0, 255, 0), makeSolid(0, 0, 255)];
|
|
76
|
+
let captured: { current: number; previous: number } | null = null;
|
|
77
|
+
await act(async () => {
|
|
78
|
+
root.render(
|
|
79
|
+
<Slideshow
|
|
80
|
+
slides={slides}
|
|
81
|
+
transition={paintBleed}
|
|
82
|
+
transitionDuration={50}
|
|
83
|
+
onChange={(current, previous) => {
|
|
84
|
+
captured = { current, previous };
|
|
85
|
+
}}
|
|
86
|
+
/>,
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
const wrapper = container.querySelector("[role='region']") as HTMLDivElement;
|
|
90
|
+
wrapper.focus();
|
|
91
|
+
wrapper.dispatchEvent(
|
|
92
|
+
new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true }),
|
|
93
|
+
);
|
|
94
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
95
|
+
expect(captured).not.toBeNull();
|
|
96
|
+
expect(captured!.current).toBe(1);
|
|
97
|
+
expect(captured!.previous).toBe(0);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("destroys on unmount without throwing", async () => {
|
|
101
|
+
const slides = [makeSolid(255, 0, 0), makeSolid(0, 255, 0)];
|
|
102
|
+
await act(async () => {
|
|
103
|
+
root.render(<Slideshow slides={slides} />);
|
|
104
|
+
});
|
|
105
|
+
await act(async () => {
|
|
106
|
+
root.unmount();
|
|
107
|
+
});
|
|
108
|
+
root = createRoot(container);
|
|
109
|
+
expect(container.querySelector("canvas")).toBeNull();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
describe("SSR safety", () => {
|
|
4
|
+
it("window is undefined in this runtime", () => {
|
|
5
|
+
expect(typeof window).toBe("undefined");
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it("module loads without DOM globals", async () => {
|
|
9
|
+
const mod = await import("../index.js");
|
|
10
|
+
expect(mod).toBeDefined();
|
|
11
|
+
expect(typeof mod.Slideshow).toBe("function");
|
|
12
|
+
expect(typeof mod.useSlideshow).toBe("function");
|
|
13
|
+
});
|
|
14
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import type { RefObject } from "react";
|
|
5
|
+
import {
|
|
6
|
+
createSlideshow,
|
|
7
|
+
type SlideshowHandle,
|
|
8
|
+
type SlideshowOptions,
|
|
9
|
+
} from "@vysmo/slideshow";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Mount a slideshow into a container ref you own and get the handle for
|
|
13
|
+
* imperative control (`next` / `prev` / `go` / `play` / `pause`).
|
|
14
|
+
*
|
|
15
|
+
* Re-creates the slideshow when *any* member of the `options` object
|
|
16
|
+
* changes — memoize the options and the `slides` array if you want
|
|
17
|
+
* stability. Returns `null` until the container is in the DOM and the
|
|
18
|
+
* slideshow is mounted.
|
|
19
|
+
*/
|
|
20
|
+
export function useSlideshow(
|
|
21
|
+
containerRef: RefObject<HTMLElement | null>,
|
|
22
|
+
options: Omit<SlideshowOptions, "container">,
|
|
23
|
+
): SlideshowHandle | null {
|
|
24
|
+
const [handle, setHandle] = useState<SlideshowHandle | null>(null);
|
|
25
|
+
const optionsRef = useRef(options);
|
|
26
|
+
optionsRef.current = options;
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const container = containerRef.current;
|
|
30
|
+
if (!container) return;
|
|
31
|
+
const h = createSlideshow({ ...optionsRef.current, container });
|
|
32
|
+
setHandle(h);
|
|
33
|
+
return () => {
|
|
34
|
+
h.destroy();
|
|
35
|
+
setHandle(null);
|
|
36
|
+
};
|
|
37
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
38
|
+
}, [containerRef, options]);
|
|
39
|
+
|
|
40
|
+
return handle;
|
|
41
|
+
}
|