@silverlila/slider 0.0.0-stage → 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 +434 -2
- package/dist/chunk-OAWJ54V3.js +765 -0
- package/dist/core.d.ts +114 -0
- package/dist/core.js +14 -0
- package/dist/index.d.ts +117 -0
- package/dist/index.js +457 -0
- package/dist/styles.css +30 -0
- package/package.json +88 -4
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { ScrollAnimation, Axis } from 'use-scroller/core';
|
|
2
|
+
export { Axis, ScrollAnimation } from 'use-scroller/core';
|
|
3
|
+
|
|
4
|
+
type Align = 'start' | 'center' | 'end';
|
|
5
|
+
interface SliderOptions {
|
|
6
|
+
/** Scroll axis. Default `'x'`. */
|
|
7
|
+
axis?: Axis;
|
|
8
|
+
/** Where a snapped slide sits in the viewport. Default `'start'`. */
|
|
9
|
+
align?: Align;
|
|
10
|
+
/** Slides per snap: an integer >= 1, or `'auto'` for as many as fully fit. Default `1`. */
|
|
11
|
+
slidesPerMove?: number | 'auto';
|
|
12
|
+
/** Infinite loop through clones. Default `false`. */
|
|
13
|
+
loop?: boolean;
|
|
14
|
+
/** Wrap from the last snap to the first (and back) by scrolling across. Ignored with `loop`. */
|
|
15
|
+
rewind?: boolean;
|
|
16
|
+
/** Mouse and touch dragging via use-scroller's `dragScroll`. Default `true`. */
|
|
17
|
+
draggable?: boolean;
|
|
18
|
+
/** Snap shown at creation, clamped into range. Default `0`. */
|
|
19
|
+
startIndex?: number;
|
|
20
|
+
/** Animation for `next`/`prev`/`scrollTo`. Default `{ type: 'tween' }`. */
|
|
21
|
+
animation?: ScrollAnimation;
|
|
22
|
+
}
|
|
23
|
+
interface SliderState {
|
|
24
|
+
/** Active snap index; the target index while a navigation is pending. */
|
|
25
|
+
index: number;
|
|
26
|
+
snapCount: number;
|
|
27
|
+
/** Real slides only, clones excluded. */
|
|
28
|
+
slideCount: number;
|
|
29
|
+
canPrev: boolean;
|
|
30
|
+
canNext: boolean;
|
|
31
|
+
/** 0..1 along the snaps, following the real scroll position. */
|
|
32
|
+
progress: number;
|
|
33
|
+
/** Copies of the slide set needed on each side; 0 unless looping. */
|
|
34
|
+
loopCopies: number;
|
|
35
|
+
}
|
|
36
|
+
interface NavigateOptions {
|
|
37
|
+
/** Overrides `SliderOptions.animation` for this call. */
|
|
38
|
+
animation?: ScrollAnimation;
|
|
39
|
+
}
|
|
40
|
+
interface Slider {
|
|
41
|
+
readonly viewport: HTMLElement;
|
|
42
|
+
readonly options: Readonly<Required<Omit<SliderOptions, 'startIndex'>>>;
|
|
43
|
+
getState(): SliderState;
|
|
44
|
+
subscribe(listener: (state: SliderState) => void): () => void;
|
|
45
|
+
next(options?: NavigateOptions): void;
|
|
46
|
+
prev(options?: NavigateOptions): void;
|
|
47
|
+
scrollTo(index: number, options?: NavigateOptions): void;
|
|
48
|
+
destroy(): void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Timed advancing for a slider, with the pauses WCAG 2.2.2 users expect.
|
|
53
|
+
*
|
|
54
|
+
* Browsers leave focus on a button after a mouse click, so only focus that matches
|
|
55
|
+
* `:focus-visible` (keyboard focus) pauses; otherwise clicking Next or the toggle would keep
|
|
56
|
+
* autoplay paused until the user clicked somewhere else.
|
|
57
|
+
*
|
|
58
|
+
* `play()` clears the current hover and focus pauses, so a Play button starts it even though
|
|
59
|
+
* pressing it put the pointer and focus inside the slider.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
interface AutoplayOptions {
|
|
63
|
+
/** Milliseconds between moves; must be > 0. Default `4000`. */
|
|
64
|
+
delay?: number;
|
|
65
|
+
/** Element whose hover and keyboard focus pause autoplay. Default `slider.viewport`. */
|
|
66
|
+
root?: HTMLElement;
|
|
67
|
+
/** Pause while the pointer is over `root`. Default `true`. */
|
|
68
|
+
pauseOnHover?: boolean;
|
|
69
|
+
/** Stop for good on `pointerdown`, `wheel` or `keydown` on the viewport. Default `true`. */
|
|
70
|
+
stopOnInteraction?: boolean;
|
|
71
|
+
}
|
|
72
|
+
interface Autoplay {
|
|
73
|
+
play(): void;
|
|
74
|
+
stop(): void;
|
|
75
|
+
isPlaying(): boolean;
|
|
76
|
+
/** Called with the new playing state on change only. */
|
|
77
|
+
subscribe(listener: (playing: boolean) => void): () => void;
|
|
78
|
+
destroy(): void;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Advances `slider` every `delay` ms, back to the first snap at the end. Pauses while hovered,
|
|
82
|
+
* while keyboard focus is inside `root`, while a pointer is pressed on the viewport and while the
|
|
83
|
+
* tab is hidden. Starts stopped under `prefers-reduced-motion: reduce`.
|
|
84
|
+
*/
|
|
85
|
+
declare function autoplay(slider: Slider, options?: AutoplayOptions): Autoplay;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Renders a looping slider's clones into its viewport for vanilla use: `loopCopies` copies of the
|
|
89
|
+
* real slide set on each side, re-rendered whenever `loopCopies` or `slideCount` changes.
|
|
90
|
+
* Returns a cleanup that stops rendering and removes every clone.
|
|
91
|
+
*/
|
|
92
|
+
declare function renderClones(slider: Slider): () => void;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Writes `--slide-progress` (signed distance from the aligned spot, in slide steps, positive for
|
|
96
|
+
* upcoming slides) and `--slide-distance` (its absolute value) on every child of the viewport,
|
|
97
|
+
* clones included, whenever the scroll position or the layout changes. It follows the scroll
|
|
98
|
+
* position rather than slider state because a loop recentre moves the position without changing
|
|
99
|
+
* the state. Returns a cleanup that removes both properties.
|
|
100
|
+
*/
|
|
101
|
+
declare function slideProgress(slider: Slider): () => void;
|
|
102
|
+
|
|
103
|
+
declare function createSlider(viewport: HTMLElement, options?: SliderOptions): Slider;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Keeps a thumbnail slider on the main slider's index and lets a click on thumbnail slide `i`
|
|
107
|
+
* move the main slider to snap `i`. The thumbnail matching the main index carries
|
|
108
|
+
* `aria-current="true"` and `data-selected`. Assumes the main slider shows one slide per snap.
|
|
109
|
+
* Returns a cleanup that stops syncing and removes both attributes.
|
|
110
|
+
*/
|
|
111
|
+
declare function syncSliders(main: Slider, thumbs: Slider): () => void;
|
|
112
|
+
|
|
113
|
+
export { autoplay, createSlider, renderClones, slideProgress, syncSliders };
|
|
114
|
+
export type { Align, Autoplay, AutoplayOptions, NavigateOptions, Slider, SliderOptions, SliderState };
|
package/dist/core.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { SliderOptions, AutoplayOptions, SliderState, Slider as Slider$1, NavigateOptions } from './core.js';
|
|
2
|
+
export { Align, Autoplay, autoplay, createSlider, renderClones, slideProgress, syncSliders } from './core.js';
|
|
3
|
+
import { Axis } from 'use-scroller/core';
|
|
4
|
+
export { Axis, ScrollAnimation } from 'use-scroller/core';
|
|
5
|
+
import * as react from 'react';
|
|
6
|
+
import { ComponentProps, ReactNode } from 'react';
|
|
7
|
+
|
|
8
|
+
interface UseSliderOptions extends Omit<SliderOptions, 'startIndex'> {
|
|
9
|
+
/** Snap shown first when the index is uncontrolled. Default `0`. */
|
|
10
|
+
defaultIndex?: number;
|
|
11
|
+
/** Controlled snap index: the slider scrolls to it whenever it changes. */
|
|
12
|
+
index?: number;
|
|
13
|
+
/** Called whenever the active snap changes, never for the initial one. */
|
|
14
|
+
onIndexChange?: (index: number) => void;
|
|
15
|
+
/** Advance on a timer: `true` for the defaults, or the options to use. Off by default. */
|
|
16
|
+
autoplay?: boolean | Omit<AutoplayOptions, 'root'>;
|
|
17
|
+
/** Write `--slide-progress` and `--slide-distance` on every slide as it scrolls. */
|
|
18
|
+
slideProgress?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** A running autoplay: whether it is playing, and the switches to change that. */
|
|
21
|
+
interface AutoplayControls {
|
|
22
|
+
isPlaying: boolean;
|
|
23
|
+
play(): void;
|
|
24
|
+
stop(): void;
|
|
25
|
+
}
|
|
26
|
+
type DiscreteState = Omit<SliderState, 'progress'>;
|
|
27
|
+
interface UseSliderResult extends DiscreteState {
|
|
28
|
+
/** Callback ref for the scroll container; the engine is created once it is attached. */
|
|
29
|
+
viewportRef: (element: HTMLElement | null) => void;
|
|
30
|
+
/** Callback ref for the element wrapping the slider and its controls. */
|
|
31
|
+
rootRef: (element: HTMLElement | null) => void;
|
|
32
|
+
/** The engine, or `null` until the viewport is mounted. */
|
|
33
|
+
slider: Slider$1 | null;
|
|
34
|
+
/** `null` while the `autoplay` option is off or the engine does not exist yet. */
|
|
35
|
+
autoplay: AutoplayControls | null;
|
|
36
|
+
/** Each navigation method does nothing while `slider` is `null`. */
|
|
37
|
+
next(options?: NavigateOptions): void;
|
|
38
|
+
prev(options?: NavigateOptions): void;
|
|
39
|
+
scrollTo(index: number, options?: NavigateOptions): void;
|
|
40
|
+
}
|
|
41
|
+
/** Headless slider: attach `viewportRef` to a scroll container and build any markup around it. */
|
|
42
|
+
declare function useSlider(options?: UseSliderOptions): UseSliderResult;
|
|
43
|
+
|
|
44
|
+
interface SliderContextValue extends UseSliderResult {
|
|
45
|
+
/** The viewport's `id`, for `aria-controls` on the controls. */
|
|
46
|
+
viewportId: string;
|
|
47
|
+
axis: Axis;
|
|
48
|
+
}
|
|
49
|
+
/** The enclosing `<Slider>`'s state and navigation. Throws outside one. */
|
|
50
|
+
declare function useSliderContext(): SliderContextValue;
|
|
51
|
+
|
|
52
|
+
type SliderProps = UseSliderOptions & {
|
|
53
|
+
/** Slides per view (`--slider-per-view`); `'auto'` sizes slides by content. Default `1`. */
|
|
54
|
+
perView?: number | 'auto';
|
|
55
|
+
/** Space between slides, written to `--slider-gap`. A number is in pixels. */
|
|
56
|
+
gap?: number | string;
|
|
57
|
+
/** Accessible name of the carousel region. Default `'Slides'`. */
|
|
58
|
+
label?: string;
|
|
59
|
+
} & Omit<ComponentProps<'div'>, 'children' | 'onChange' | keyof UseSliderOptions> & {
|
|
60
|
+
children: ReactNode;
|
|
61
|
+
};
|
|
62
|
+
declare function SliderRoot({ axis, align, slidesPerMove, loop, rewind, draggable, animation, defaultIndex, index, onIndexChange, autoplay, slideProgress, perView, gap, label, style, ref, children, ...props }: SliderProps): react.JSX.Element;
|
|
63
|
+
/**
|
|
64
|
+
* The scroll container. Its children must be `<Slider.Slide>` elements (or components rendering
|
|
65
|
+
* one); in loop mode each is also rendered as hidden, inert clones on both sides.
|
|
66
|
+
*/
|
|
67
|
+
declare function SliderViewport({ ref, children, ...props }: ComponentProps<'div'>): react.JSX.Element;
|
|
68
|
+
/** One slide. Must be rendered inside `<Slider.Viewport>`. */
|
|
69
|
+
declare function SliderSlide(props: ComponentProps<'div'>): react.JSX.Element;
|
|
70
|
+
declare function SliderPrev({ onClick, children, ...props }: ComponentProps<'button'>): react.JSX.Element;
|
|
71
|
+
declare function SliderNext({ onClick, children, ...props }: ComponentProps<'button'>): react.JSX.Element;
|
|
72
|
+
/** One button per snap; the active one carries `aria-current="true"` and `data-selected`. */
|
|
73
|
+
declare function SliderDots(props: Omit<ComponentProps<'div'>, 'children'>): react.JSX.Element;
|
|
74
|
+
type SliderAutoplayToggleProps = Omit<ComponentProps<'button'>, 'children'> & {
|
|
75
|
+
/** The button's content, or a function of the playing state. Default `'Pause'` / `'Play'`. */
|
|
76
|
+
children?: ReactNode | ((isPlaying: boolean) => ReactNode);
|
|
77
|
+
};
|
|
78
|
+
/** Stops and restarts autoplay. Renders nothing when the slider has no `autoplay`. */
|
|
79
|
+
declare function SliderAutoplayToggle({ onClick, children, ...props }: SliderAutoplayToggleProps): react.JSX.Element;
|
|
80
|
+
type SliderThumbsProps = Omit<ComponentProps<'div'>, 'children'> & {
|
|
81
|
+
/** Thumbnails per view; `'auto'` sizes them by content. Default `'auto'`. */
|
|
82
|
+
perView?: number | 'auto';
|
|
83
|
+
/** Space between thumbnails. A number is in pixels. */
|
|
84
|
+
gap?: number | string;
|
|
85
|
+
/** Default `'x'`. */
|
|
86
|
+
axis?: Axis;
|
|
87
|
+
/** `<Slider.Thumb>` elements, one per main slide. */
|
|
88
|
+
children: ReactNode;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* A strip of thumbnails that is a slider of its own, kept on the enclosing slider's index:
|
|
92
|
+
* clicking thumbnail `i` moves the slider to snap `i`, and the matching thumbnail carries
|
|
93
|
+
* `aria-current="true"` and `data-selected`. Assumes the main slider shows one slide per snap.
|
|
94
|
+
*/
|
|
95
|
+
declare function SliderThumbs({ perView, gap, axis, style, children, ...props }: SliderThumbsProps): react.JSX.Element;
|
|
96
|
+
/** One thumbnail button. Must be rendered inside `<Slider.Thumbs>`. */
|
|
97
|
+
declare function SliderThumb(props: ComponentProps<'button'>): react.JSX.Element;
|
|
98
|
+
declare const Slider: typeof SliderRoot & {
|
|
99
|
+
Viewport: typeof SliderViewport;
|
|
100
|
+
Slide: typeof SliderSlide;
|
|
101
|
+
Prev: typeof SliderPrev;
|
|
102
|
+
Next: typeof SliderNext;
|
|
103
|
+
Dots: typeof SliderDots;
|
|
104
|
+
AutoplayToggle: typeof SliderAutoplayToggle;
|
|
105
|
+
Thumbs: typeof SliderThumbs;
|
|
106
|
+
Thumb: typeof SliderThumb;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Subscribes to a slice of a slider's state. The component re-renders only when the selected
|
|
111
|
+
* value changes by `isEqual`, so a selector returning an object should pass a shallow comparison.
|
|
112
|
+
* With `slider === null` (not mounted yet) it reads the initial state.
|
|
113
|
+
*/
|
|
114
|
+
declare function useSliderState<T>(slider: Slider$1 | null, selector: (state: SliderState) => T, isEqual?: (a: T, b: T) => boolean): T;
|
|
115
|
+
|
|
116
|
+
export { AutoplayOptions, NavigateOptions, Slider, SliderOptions, SliderState, useSlider, useSliderContext, useSliderState };
|
|
117
|
+
export type { AutoplayControls, SliderAutoplayToggleProps, SliderProps, SliderThumbsProps, UseSliderOptions, UseSliderResult };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import {
|
|
3
|
+
autoplay,
|
|
4
|
+
createSlider,
|
|
5
|
+
renderClones,
|
|
6
|
+
slideProgress,
|
|
7
|
+
syncSliders
|
|
8
|
+
} from "./chunk-OAWJ54V3.js";
|
|
9
|
+
|
|
10
|
+
// src/react/context.ts
|
|
11
|
+
import { createContext, use } from "react";
|
|
12
|
+
var SliderContext = createContext(null);
|
|
13
|
+
function useSliderContext() {
|
|
14
|
+
const context = use(SliderContext);
|
|
15
|
+
if (context === null) throw new Error("Slider components must be used inside <Slider>");
|
|
16
|
+
return context;
|
|
17
|
+
}
|
|
18
|
+
var SlideContext = createContext(null);
|
|
19
|
+
var ThumbContext = createContext(null);
|
|
20
|
+
|
|
21
|
+
// src/react/slider.tsx
|
|
22
|
+
import {
|
|
23
|
+
Children,
|
|
24
|
+
isValidElement,
|
|
25
|
+
use as use2,
|
|
26
|
+
useCallback as useCallback3,
|
|
27
|
+
useEffect as useEffect2,
|
|
28
|
+
useId,
|
|
29
|
+
useMemo as useMemo2
|
|
30
|
+
} from "react";
|
|
31
|
+
|
|
32
|
+
// src/react/use-slider.ts
|
|
33
|
+
import {
|
|
34
|
+
useCallback as useCallback2,
|
|
35
|
+
useEffect,
|
|
36
|
+
useLayoutEffect,
|
|
37
|
+
useMemo,
|
|
38
|
+
useRef as useRef2,
|
|
39
|
+
useState
|
|
40
|
+
} from "react";
|
|
41
|
+
|
|
42
|
+
// src/react/use-slider-state.ts
|
|
43
|
+
import { useCallback, useRef, useSyncExternalStore } from "react";
|
|
44
|
+
var INITIAL_STATE = {
|
|
45
|
+
index: 0,
|
|
46
|
+
snapCount: 0,
|
|
47
|
+
slideCount: 0,
|
|
48
|
+
canPrev: false,
|
|
49
|
+
canNext: false,
|
|
50
|
+
progress: 0,
|
|
51
|
+
loopCopies: 0
|
|
52
|
+
};
|
|
53
|
+
function ignore() {
|
|
54
|
+
}
|
|
55
|
+
function useSliderState(slider, selector, isEqual = Object.is) {
|
|
56
|
+
const last = useRef(null);
|
|
57
|
+
const subscribe = useCallback(
|
|
58
|
+
(onChange) => slider ? slider.subscribe(onChange) : ignore,
|
|
59
|
+
[slider]
|
|
60
|
+
);
|
|
61
|
+
function select(state) {
|
|
62
|
+
const next = selector(state);
|
|
63
|
+
const previous = last.current;
|
|
64
|
+
if (previous !== null && isEqual(previous.value, next)) return previous.value;
|
|
65
|
+
last.current = { value: next };
|
|
66
|
+
return next;
|
|
67
|
+
}
|
|
68
|
+
return useSyncExternalStore(
|
|
69
|
+
subscribe,
|
|
70
|
+
() => select(slider ? slider.getState() : INITIAL_STATE),
|
|
71
|
+
() => select(INITIAL_STATE)
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/react/use-slider.ts
|
|
76
|
+
function useSlider(options = {}) {
|
|
77
|
+
const { axis, align, slidesPerMove, loop, rewind, draggable, animation, index } = options;
|
|
78
|
+
const { autoplay: autoplayOption, slideProgress: tracksProgress = false } = options;
|
|
79
|
+
const [viewport, setViewport] = useState(null);
|
|
80
|
+
const [rootElement, setRootElement] = useState(null);
|
|
81
|
+
const [slider, setSlider] = useState(null);
|
|
82
|
+
const stableAnimation = useShallowStable(animation);
|
|
83
|
+
const latest = useRef2(options);
|
|
84
|
+
const previousIndex = useRef2(null);
|
|
85
|
+
const live = useRef2(null);
|
|
86
|
+
useLayoutEffect(() => {
|
|
87
|
+
latest.current = options;
|
|
88
|
+
});
|
|
89
|
+
useLayoutEffect(() => {
|
|
90
|
+
if (viewport === null) return;
|
|
91
|
+
const start = latest.current;
|
|
92
|
+
const engine = createSlider(viewport, {
|
|
93
|
+
axis,
|
|
94
|
+
align,
|
|
95
|
+
slidesPerMove,
|
|
96
|
+
loop,
|
|
97
|
+
rewind,
|
|
98
|
+
draggable,
|
|
99
|
+
animation: stableAnimation,
|
|
100
|
+
startIndex: previousIndex.current ?? start.index ?? start.defaultIndex
|
|
101
|
+
});
|
|
102
|
+
let reported = engine.getState().index;
|
|
103
|
+
engine.subscribe((state) => {
|
|
104
|
+
var _a, _b;
|
|
105
|
+
if (state.index === reported) return;
|
|
106
|
+
reported = state.index;
|
|
107
|
+
(_b = (_a = latest.current).onIndexChange) == null ? void 0 : _b.call(_a, state.index);
|
|
108
|
+
});
|
|
109
|
+
live.current = engine;
|
|
110
|
+
setSlider(engine);
|
|
111
|
+
return () => {
|
|
112
|
+
previousIndex.current = engine.getState().index;
|
|
113
|
+
live.current = null;
|
|
114
|
+
engine.destroy();
|
|
115
|
+
setSlider(null);
|
|
116
|
+
};
|
|
117
|
+
}, [viewport, axis, align, slidesPerMove, loop, rewind, draggable, stableAnimation]);
|
|
118
|
+
const discrete = useSliderState(slider, selectDiscrete, shallowEqual);
|
|
119
|
+
const autoplayControls = useAutoplay(slider, live, rootElement, autoplayOption);
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (!isLive(slider, live) || !tracksProgress) return;
|
|
122
|
+
return slideProgress(slider);
|
|
123
|
+
}, [slider, live, tracksProgress]);
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
const engine = live.current;
|
|
126
|
+
if (engine === null || index === void 0) return;
|
|
127
|
+
if (index !== engine.getState().index) engine.scrollTo(index);
|
|
128
|
+
}, [index, slider]);
|
|
129
|
+
const next = useCallback2((navigate) => slider == null ? void 0 : slider.next(navigate), [slider]);
|
|
130
|
+
const prev = useCallback2((navigate) => slider == null ? void 0 : slider.prev(navigate), [slider]);
|
|
131
|
+
const scrollTo = useCallback2(
|
|
132
|
+
(target, navigate) => slider == null ? void 0 : slider.scrollTo(target, navigate),
|
|
133
|
+
[slider]
|
|
134
|
+
);
|
|
135
|
+
return useMemo(
|
|
136
|
+
() => ({
|
|
137
|
+
...discrete,
|
|
138
|
+
viewportRef: setViewport,
|
|
139
|
+
rootRef: setRootElement,
|
|
140
|
+
slider,
|
|
141
|
+
autoplay: autoplayControls,
|
|
142
|
+
next,
|
|
143
|
+
prev,
|
|
144
|
+
scrollTo
|
|
145
|
+
}),
|
|
146
|
+
[discrete, slider, autoplayControls, next, prev, scrollTo]
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
function useAutoplay(slider, live, rootElement, option) {
|
|
150
|
+
const settings = option === true ? {} : option || null;
|
|
151
|
+
const enabled = settings !== null;
|
|
152
|
+
const delay = settings == null ? void 0 : settings.delay;
|
|
153
|
+
const pauseOnHover = settings == null ? void 0 : settings.pauseOnHover;
|
|
154
|
+
const stopOnInteraction = settings == null ? void 0 : settings.stopOnInteraction;
|
|
155
|
+
const [controls, setControls] = useState(null);
|
|
156
|
+
useLayoutEffect(() => {
|
|
157
|
+
if (!isLive(slider, live) || !enabled) return;
|
|
158
|
+
const player = autoplay(slider, {
|
|
159
|
+
delay,
|
|
160
|
+
pauseOnHover,
|
|
161
|
+
stopOnInteraction,
|
|
162
|
+
root: rootElement ?? slider.viewport
|
|
163
|
+
});
|
|
164
|
+
const publish = (isPlaying) => setControls({ isPlaying, play: () => player.play(), stop: () => player.stop() });
|
|
165
|
+
publish(player.isPlaying());
|
|
166
|
+
player.subscribe(publish);
|
|
167
|
+
return () => {
|
|
168
|
+
player.destroy();
|
|
169
|
+
setControls(null);
|
|
170
|
+
};
|
|
171
|
+
}, [slider, live, enabled, delay, pauseOnHover, stopOnInteraction, rootElement]);
|
|
172
|
+
return controls;
|
|
173
|
+
}
|
|
174
|
+
function isLive(slider, live) {
|
|
175
|
+
return slider !== null && slider === live.current;
|
|
176
|
+
}
|
|
177
|
+
function selectDiscrete(state) {
|
|
178
|
+
return {
|
|
179
|
+
index: state.index,
|
|
180
|
+
snapCount: state.snapCount,
|
|
181
|
+
slideCount: state.slideCount,
|
|
182
|
+
canPrev: state.canPrev,
|
|
183
|
+
canNext: state.canNext,
|
|
184
|
+
loopCopies: state.loopCopies
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function useShallowStable(value) {
|
|
188
|
+
const stable = useRef2(value);
|
|
189
|
+
if (!shallowEqual(stable.current, value)) stable.current = value;
|
|
190
|
+
return stable.current;
|
|
191
|
+
}
|
|
192
|
+
function shallowEqual(a, b) {
|
|
193
|
+
if (a === b) return true;
|
|
194
|
+
if (a === void 0 || b === void 0) return false;
|
|
195
|
+
const entries = Object.entries(a);
|
|
196
|
+
const other = new Map(Object.entries(b));
|
|
197
|
+
return entries.length === other.size && entries.every(([key, value]) => other.has(key) && Object.is(value, other.get(key)));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/react/slider.tsx
|
|
201
|
+
function SliderRoot({
|
|
202
|
+
axis,
|
|
203
|
+
align,
|
|
204
|
+
slidesPerMove,
|
|
205
|
+
loop,
|
|
206
|
+
rewind,
|
|
207
|
+
draggable,
|
|
208
|
+
animation,
|
|
209
|
+
defaultIndex,
|
|
210
|
+
index,
|
|
211
|
+
onIndexChange,
|
|
212
|
+
autoplay: autoplay2,
|
|
213
|
+
slideProgress: slideProgress2,
|
|
214
|
+
perView = 1,
|
|
215
|
+
gap,
|
|
216
|
+
label,
|
|
217
|
+
style,
|
|
218
|
+
ref,
|
|
219
|
+
children,
|
|
220
|
+
...props
|
|
221
|
+
}) {
|
|
222
|
+
const result = useSlider({
|
|
223
|
+
axis,
|
|
224
|
+
align,
|
|
225
|
+
slidesPerMove,
|
|
226
|
+
loop,
|
|
227
|
+
rewind,
|
|
228
|
+
draggable,
|
|
229
|
+
animation,
|
|
230
|
+
defaultIndex,
|
|
231
|
+
index,
|
|
232
|
+
onIndexChange,
|
|
233
|
+
autoplay: autoplay2,
|
|
234
|
+
slideProgress: slideProgress2
|
|
235
|
+
});
|
|
236
|
+
const viewportId = useId();
|
|
237
|
+
const resolvedAxis = axis ?? "x";
|
|
238
|
+
const context = useMemo2(
|
|
239
|
+
() => ({ ...result, viewportId, axis: resolvedAxis }),
|
|
240
|
+
[result, viewportId, resolvedAxis]
|
|
241
|
+
);
|
|
242
|
+
const rootRef = useMergedRef(result.rootRef, ref);
|
|
243
|
+
const variables = {
|
|
244
|
+
"--slider-per-view": perView === "auto" ? void 0 : perView,
|
|
245
|
+
"--slider-gap": typeof gap === "number" ? `${gap}px` : gap,
|
|
246
|
+
"--slider-align": align,
|
|
247
|
+
...style
|
|
248
|
+
};
|
|
249
|
+
return /* @__PURE__ */ React.createElement(SliderContext, { value: context }, /* @__PURE__ */ React.createElement(
|
|
250
|
+
"div",
|
|
251
|
+
{
|
|
252
|
+
ref: rootRef,
|
|
253
|
+
role: "region",
|
|
254
|
+
"aria-roledescription": "carousel",
|
|
255
|
+
"aria-label": label ?? "Slides",
|
|
256
|
+
"data-slider": "",
|
|
257
|
+
style: variables,
|
|
258
|
+
...props
|
|
259
|
+
},
|
|
260
|
+
children
|
|
261
|
+
));
|
|
262
|
+
}
|
|
263
|
+
function SliderViewport({ ref, children, ...props }) {
|
|
264
|
+
const { viewportRef, viewportId, axis, loopCopies } = useSliderContext();
|
|
265
|
+
const mergedRef = useMergedRef(viewportRef, ref);
|
|
266
|
+
const slides = Children.toArray(children);
|
|
267
|
+
const real = slides.map((child, i) => /* @__PURE__ */ React.createElement(SlideContext, { value: { index: i, clone: false }, key: keyOf(child, i) }, child));
|
|
268
|
+
const copies = (side) => Array.from(
|
|
269
|
+
{ length: loopCopies },
|
|
270
|
+
(_, copy) => slides.map((child, i) => /* @__PURE__ */ React.createElement(SlideContext, { value: { index: i, clone: true }, key: `${side}-${copy}-${keyOf(child, i)}` }, child))
|
|
271
|
+
).flat();
|
|
272
|
+
return /* @__PURE__ */ React.createElement(
|
|
273
|
+
"div",
|
|
274
|
+
{
|
|
275
|
+
ref: mergedRef,
|
|
276
|
+
id: viewportId,
|
|
277
|
+
"data-slider-viewport": "",
|
|
278
|
+
"data-axis": axis,
|
|
279
|
+
tabIndex: 0,
|
|
280
|
+
...props
|
|
281
|
+
},
|
|
282
|
+
[...copies("before"), ...real, ...copies("after")]
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
function SliderSlide(props) {
|
|
286
|
+
const { slideCount } = useSliderContext();
|
|
287
|
+
const slide = use2(SlideContext);
|
|
288
|
+
if (slide === null) throw new Error("<Slider.Slide> must be rendered inside <Slider.Viewport>");
|
|
289
|
+
if (slide.clone) {
|
|
290
|
+
return /* @__PURE__ */ React.createElement(
|
|
291
|
+
"div",
|
|
292
|
+
{
|
|
293
|
+
"data-slider-slide": "",
|
|
294
|
+
"data-slider-clone": slide.index,
|
|
295
|
+
"aria-hidden": "true",
|
|
296
|
+
inert: true,
|
|
297
|
+
...props,
|
|
298
|
+
id: void 0
|
|
299
|
+
}
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
return /* @__PURE__ */ React.createElement(
|
|
303
|
+
"div",
|
|
304
|
+
{
|
|
305
|
+
"data-slider-slide": "",
|
|
306
|
+
role: "group",
|
|
307
|
+
"aria-roledescription": "slide",
|
|
308
|
+
"aria-label": `${slide.index + 1} of ${slideCount}`,
|
|
309
|
+
...props
|
|
310
|
+
}
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
function SliderPrev({ onClick, children = "Previous", ...props }) {
|
|
314
|
+
const { prev, canPrev, viewportId } = useSliderContext();
|
|
315
|
+
return /* @__PURE__ */ React.createElement(
|
|
316
|
+
"button",
|
|
317
|
+
{
|
|
318
|
+
type: "button",
|
|
319
|
+
"aria-controls": viewportId,
|
|
320
|
+
"aria-label": "Previous slide",
|
|
321
|
+
disabled: !canPrev,
|
|
322
|
+
...props,
|
|
323
|
+
onClick: (event) => {
|
|
324
|
+
onClick == null ? void 0 : onClick(event);
|
|
325
|
+
if (!event.defaultPrevented) prev();
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
children
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
function SliderNext({ onClick, children = "Next", ...props }) {
|
|
332
|
+
const { next, canNext, viewportId } = useSliderContext();
|
|
333
|
+
return /* @__PURE__ */ React.createElement(
|
|
334
|
+
"button",
|
|
335
|
+
{
|
|
336
|
+
type: "button",
|
|
337
|
+
"aria-controls": viewportId,
|
|
338
|
+
"aria-label": "Next slide",
|
|
339
|
+
disabled: !canNext,
|
|
340
|
+
...props,
|
|
341
|
+
onClick: (event) => {
|
|
342
|
+
onClick == null ? void 0 : onClick(event);
|
|
343
|
+
if (!event.defaultPrevented) next();
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
children
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
function SliderDots(props) {
|
|
350
|
+
const { snapCount, index, scrollTo, viewportId } = useSliderContext();
|
|
351
|
+
return /* @__PURE__ */ React.createElement("div", { role: "group", "aria-label": "Choose slide", "data-slider-dots": "", ...props }, Array.from({ length: snapCount }, (_, i) => /* @__PURE__ */ React.createElement(
|
|
352
|
+
"button",
|
|
353
|
+
{
|
|
354
|
+
key: i,
|
|
355
|
+
type: "button",
|
|
356
|
+
"data-slider-dot": "",
|
|
357
|
+
"aria-controls": viewportId,
|
|
358
|
+
"aria-label": `Go to slide ${i + 1}`,
|
|
359
|
+
"aria-current": i === index ? "true" : void 0,
|
|
360
|
+
"data-selected": i === index ? "" : void 0,
|
|
361
|
+
onClick: () => scrollTo(i)
|
|
362
|
+
}
|
|
363
|
+
)));
|
|
364
|
+
}
|
|
365
|
+
function SliderAutoplayToggle({ onClick, children, ...props }) {
|
|
366
|
+
const { autoplay: autoplay2, viewportId } = useSliderContext();
|
|
367
|
+
if (autoplay2 === null) return null;
|
|
368
|
+
const { isPlaying, play, stop } = autoplay2;
|
|
369
|
+
return /* @__PURE__ */ React.createElement(
|
|
370
|
+
"button",
|
|
371
|
+
{
|
|
372
|
+
type: "button",
|
|
373
|
+
"aria-controls": viewportId,
|
|
374
|
+
"aria-label": isPlaying ? "Stop automatic slide show" : "Start automatic slide show",
|
|
375
|
+
"data-playing": isPlaying ? "" : void 0,
|
|
376
|
+
...props,
|
|
377
|
+
onClick: (event) => {
|
|
378
|
+
onClick == null ? void 0 : onClick(event);
|
|
379
|
+
if (event.defaultPrevented) return;
|
|
380
|
+
if (isPlaying) stop();
|
|
381
|
+
else play();
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
typeof children === "function" ? children(isPlaying) : children ?? (isPlaying ? "Pause" : "Play")
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
function SliderThumbs({
|
|
388
|
+
perView = "auto",
|
|
389
|
+
gap,
|
|
390
|
+
axis = "x",
|
|
391
|
+
style,
|
|
392
|
+
children,
|
|
393
|
+
...props
|
|
394
|
+
}) {
|
|
395
|
+
const main = useSliderContext();
|
|
396
|
+
const thumbs = useSlider({ axis });
|
|
397
|
+
useEffect2(() => {
|
|
398
|
+
if (main.slider === null || thumbs.slider === null) return;
|
|
399
|
+
return syncSliders(main.slider, thumbs.slider);
|
|
400
|
+
}, [main.slider, thumbs.slider]);
|
|
401
|
+
const variables = {
|
|
402
|
+
"--slider-per-view": perView === "auto" ? "initial" : perView,
|
|
403
|
+
"--slider-gap": gap === void 0 ? "initial" : typeof gap === "number" ? `${gap}px` : gap,
|
|
404
|
+
"--slider-align": "start",
|
|
405
|
+
...style
|
|
406
|
+
};
|
|
407
|
+
return /* @__PURE__ */ React.createElement("div", { "data-slider-thumbs": "", style: variables, ...props }, /* @__PURE__ */ React.createElement("div", { ref: thumbs.viewportRef, "data-slider-viewport": "", "data-axis": axis }, Children.toArray(children).map((child, i) => /* @__PURE__ */ React.createElement(ThumbContext, { value: i, key: keyOf(child, i) }, child))));
|
|
408
|
+
}
|
|
409
|
+
function SliderThumb(props) {
|
|
410
|
+
const { viewportId } = useSliderContext();
|
|
411
|
+
const index = use2(ThumbContext);
|
|
412
|
+
if (index === null) throw new Error("<Slider.Thumb> must be rendered inside <Slider.Thumbs>");
|
|
413
|
+
return /* @__PURE__ */ React.createElement(
|
|
414
|
+
"button",
|
|
415
|
+
{
|
|
416
|
+
type: "button",
|
|
417
|
+
"data-slider-thumb": "",
|
|
418
|
+
"aria-controls": viewportId,
|
|
419
|
+
"aria-label": `Go to slide ${index + 1}`,
|
|
420
|
+
...props
|
|
421
|
+
}
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
var Slider = Object.assign(SliderRoot, {
|
|
425
|
+
Viewport: SliderViewport,
|
|
426
|
+
Slide: SliderSlide,
|
|
427
|
+
Prev: SliderPrev,
|
|
428
|
+
Next: SliderNext,
|
|
429
|
+
Dots: SliderDots,
|
|
430
|
+
AutoplayToggle: SliderAutoplayToggle,
|
|
431
|
+
Thumbs: SliderThumbs,
|
|
432
|
+
Thumb: SliderThumb
|
|
433
|
+
});
|
|
434
|
+
function keyOf(child, position) {
|
|
435
|
+
return isValidElement(child) && child.key !== null ? child.key : String(position);
|
|
436
|
+
}
|
|
437
|
+
function useMergedRef(own, ref) {
|
|
438
|
+
return useCallback3(
|
|
439
|
+
(element) => {
|
|
440
|
+
own(element);
|
|
441
|
+
if (typeof ref === "function") ref(element);
|
|
442
|
+
else if (ref) ref.current = element;
|
|
443
|
+
},
|
|
444
|
+
[own, ref]
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
export {
|
|
448
|
+
Slider,
|
|
449
|
+
autoplay,
|
|
450
|
+
createSlider,
|
|
451
|
+
renderClones,
|
|
452
|
+
slideProgress,
|
|
453
|
+
syncSliders,
|
|
454
|
+
useSlider,
|
|
455
|
+
useSliderContext,
|
|
456
|
+
useSliderState
|
|
457
|
+
};
|