@payfit/unity-components 2.9.8 → 2.9.9
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/dist/esm/components/carousel/Carousel.context.d.ts +60 -0
- package/dist/esm/components/carousel/Carousel.context.js +14 -0
- package/dist/esm/components/carousel/Carousel.d.ts +72 -0
- package/dist/esm/components/carousel/Carousel.js +106 -0
- package/dist/esm/components/carousel/Carousel.options.d.ts +24 -0
- package/dist/esm/components/carousel/Carousel.options.js +64 -0
- package/dist/esm/components/carousel/hooks/useCarouselAccessibility.d.ts +21 -0
- package/dist/esm/components/carousel/hooks/useCarouselAccessibility.js +87 -0
- package/dist/esm/components/carousel/hooks/useCarouselState.d.ts +14 -0
- package/dist/esm/components/carousel/hooks/useCarouselState.js +62 -0
- package/dist/esm/components/carousel/parts/CarouselContent.d.ts +103 -0
- package/dist/esm/components/carousel/parts/CarouselContent.js +69 -0
- package/dist/esm/components/carousel/parts/CarouselHeader.d.ts +87 -0
- package/dist/esm/components/carousel/parts/CarouselHeader.js +58 -0
- package/dist/esm/components/carousel/parts/CarouselNav.d.ts +59 -0
- package/dist/esm/components/carousel/parts/CarouselNav.js +80 -0
- package/dist/esm/components/carousel/parts/CarouselSlide.d.ts +38 -0
- package/dist/esm/components/carousel/parts/CarouselSlide.js +35 -0
- package/dist/esm/components/carousel/types.d.ts +8 -0
- package/dist/esm/components/icon-button/IconButton.d.ts +1 -0
- package/dist/esm/hooks/use-responsive-value.d.ts +11 -0
- package/dist/esm/hooks/use-responsive-value.js +29 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.js +87 -72
- package/i18n/en-GB.json +13 -0
- package/i18n/es-ES.json +13 -0
- package/i18n/fr-FR.json +13 -0
- package/package.json +10 -7
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { EmblaCarouselApi, EmblaCarouselRef } from './types.js';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Context type for the Carousel component.
|
|
5
|
+
* Provides shared state and methods to all carousel sub-components.
|
|
6
|
+
*/
|
|
7
|
+
type CarouselContextProps = {
|
|
8
|
+
/** Ref to the Embla carousel viewport element */
|
|
9
|
+
carouselRef: EmblaCarouselRef;
|
|
10
|
+
/** Embla Carousel API instance */
|
|
11
|
+
api: EmblaCarouselApi;
|
|
12
|
+
/** Navigate to the previous slide or page */
|
|
13
|
+
goToPrev: () => void;
|
|
14
|
+
/** Navigate to the next slide or page */
|
|
15
|
+
goToNext: () => void;
|
|
16
|
+
/** Navigate to a specific snap index */
|
|
17
|
+
goTo: (index: number) => void;
|
|
18
|
+
/** Check if navigation to next is possible */
|
|
19
|
+
canGoToNext: () => boolean | undefined;
|
|
20
|
+
/** Check if navigation to previous is possible */
|
|
21
|
+
canGoToPrev: () => boolean | undefined;
|
|
22
|
+
/** Set of slide indexes currently visible in the viewport */
|
|
23
|
+
visibleSlideIndexes: ReadonlySet<number>;
|
|
24
|
+
/** Current snap/page index (0-based) */
|
|
25
|
+
selectedSnap: number;
|
|
26
|
+
/** Total number of snaps/pages */
|
|
27
|
+
snapCount: number;
|
|
28
|
+
/** Generated IDs for accessibility attributes */
|
|
29
|
+
a11yIds: {
|
|
30
|
+
root: string;
|
|
31
|
+
track: string;
|
|
32
|
+
previousButton: string;
|
|
33
|
+
nextButton: string;
|
|
34
|
+
title: string;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Context for sharing carousel state between components.
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
export declare const CarouselContext: React.Context<CarouselContextProps | null>;
|
|
42
|
+
/**
|
|
43
|
+
* Hook to access the carousel context.
|
|
44
|
+
* Must be used within a `Carousel` component.
|
|
45
|
+
* @returns {CarouselContextProps} Carousel context value
|
|
46
|
+
* @throws {Error} If used outside of a Carousel component
|
|
47
|
+
* @example
|
|
48
|
+
* ```tsx
|
|
49
|
+
* function CustomCarouselPart() {
|
|
50
|
+
* const { goToNext, canGoToNext } = useCarousel()
|
|
51
|
+
* return (
|
|
52
|
+
* <button onClick={goToNext} disabled={!canGoToNext()}>
|
|
53
|
+
* Next
|
|
54
|
+
* </button>
|
|
55
|
+
* )
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export declare function useCarousel(): CarouselContextProps;
|
|
60
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as t from "react";
|
|
2
|
+
const o = t.createContext(
|
|
3
|
+
null
|
|
4
|
+
);
|
|
5
|
+
function n() {
|
|
6
|
+
const e = t.useContext(o);
|
|
7
|
+
if (!e)
|
|
8
|
+
throw new Error("useCarousel must be used within a Carousel Component");
|
|
9
|
+
return e;
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
o as CarouselContext,
|
|
13
|
+
n as useCarousel
|
|
14
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { CarouselOptions } from './types.js';
|
|
3
|
+
export declare const carousel: import('tailwind-variants').TVReturnType<{} | {} | {}, undefined, "uy:relative uy:flex uy:flex-col uy:gap-200 uy:md:gap-300", {} | {}, undefined, import('tailwind-variants').TVReturnType<unknown, undefined, "uy:relative uy:flex uy:flex-col uy:gap-200 uy:md:gap-300", unknown, unknown, undefined>>;
|
|
4
|
+
export interface CarouselProps {
|
|
5
|
+
options?: Pick<NonNullable<CarouselOptions>, 'align' | 'loop' | 'slidesToScroll' | 'containScroll' | 'inViewThreshold' | 'inViewMargin' | 'duration' | 'draggable' | 'dragFree' | 'dragThreshold' | 'ssr'>;
|
|
6
|
+
/**
|
|
7
|
+
* Accessible label for the carousel region.
|
|
8
|
+
* When provided, takes precedence over the title set in `CarouselHeader` for labeling.
|
|
9
|
+
*/
|
|
10
|
+
'aria-label'?: string;
|
|
11
|
+
/** Additional CSS classes for the root element. */
|
|
12
|
+
className?: string;
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Root container of the Carousel. Manages shared state and provides context to all sub-components.
|
|
17
|
+
* The Carousel component displays a series of slides in a horizontal, scrollable layout. It's built
|
|
18
|
+
* on top of Embla Carousel and provides accessible navigation via keyboard, mouse, and touch.
|
|
19
|
+
* ## Composition
|
|
20
|
+
* Compose the Carousel with its child components:
|
|
21
|
+
* - `CarouselHeader` – Optional header with title and action slot
|
|
22
|
+
* - `CarouselContent` – Scrollable viewport that houses slides
|
|
23
|
+
* - `CarouselSlide` – Individual slide wrapper
|
|
24
|
+
* ## Features
|
|
25
|
+
* - **Responsive**: Configure slides per page and spacing per breakpoint
|
|
26
|
+
* - **Accessible**: Full keyboard navigation (Arrow keys, PageUp/PageDown, Home/End)
|
|
27
|
+
* - **Touch-enabled**: Native swipe gestures on mobile
|
|
28
|
+
* - **Flexible layout**: Control alignment, looping, and scroll behavior
|
|
29
|
+
* - **Screen reader support**: ARIA labels and live region announcements
|
|
30
|
+
* @example
|
|
31
|
+
* ```tsx
|
|
32
|
+
* import { Carousel, CarouselContent, CarouselHeader, CarouselSlide } from '@payfit/unity-components'
|
|
33
|
+
*
|
|
34
|
+
* <Carousel>
|
|
35
|
+
* <CarouselHeader title="Featured items" />
|
|
36
|
+
* <CarouselContent itemsPerPage={3} gap="$200">
|
|
37
|
+
* <CarouselSlide>Item 1</CarouselSlide>
|
|
38
|
+
* <CarouselSlide>Item 2</CarouselSlide>
|
|
39
|
+
* <CarouselSlide>Item 3</CarouselSlide>
|
|
40
|
+
* </CarouselContent>
|
|
41
|
+
* </Carousel>
|
|
42
|
+
* ```
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* <Carousel>
|
|
46
|
+
* <CarouselHeader title="Responsive carousel" />
|
|
47
|
+
* <CarouselContent
|
|
48
|
+
* itemsPerPage={{ base: 1, md: 2, lg: 3 }}
|
|
49
|
+
* gap={{ base: '$100', md: '$200' }}
|
|
50
|
+
* >
|
|
51
|
+
* {slides.map((slide) => (
|
|
52
|
+
* <CarouselSlide key={slide.id}>{slide.content}</CarouselSlide>
|
|
53
|
+
* ))}
|
|
54
|
+
* </CarouselContent>
|
|
55
|
+
* </Carousel>
|
|
56
|
+
* ```
|
|
57
|
+
* ## Accessibility
|
|
58
|
+
* - Arrow keys navigate between individual slides
|
|
59
|
+
* - PageUp/PageDown jump to the first slide of the next/previous page
|
|
60
|
+
* - Home/End keys jump to the first/last slide
|
|
61
|
+
* - Tab key exits the carousel to the next focusable element
|
|
62
|
+
* - Screen readers announce the current slide and total count
|
|
63
|
+
* @param {CarouselProps} props - Component props
|
|
64
|
+
* @param {CarouselProps['options']} props.options - Embla Carousel configuration options
|
|
65
|
+
* @param {CarouselProps['aria-label']} props.aria-label - Accessible label for the carousel region (overrides title from CarouselHeader)
|
|
66
|
+
* @param {CarouselProps['className']} props.className - Additional CSS classes
|
|
67
|
+
* @param {CarouselProps['children']} props.children - Carousel content (CarouselHeader, CarouselContent, CarouselSlide)
|
|
68
|
+
* @see {@link CarouselProps} for all available props
|
|
69
|
+
* @see {@link https://www.embla-carousel.com/api/options/|Embla Carousel Options}
|
|
70
|
+
*/
|
|
71
|
+
declare const Carousel: import('react').ForwardRefExoticComponent<CarouselProps & import('react').RefAttributes<HTMLDivElement>>;
|
|
72
|
+
export { Carousel };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { jsx as r, jsxs as I } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef as R, useMemo as h } from "react";
|
|
3
|
+
import { uyTv as j } from "@payfit/unity-themes";
|
|
4
|
+
import w from "embla-carousel-accessibility";
|
|
5
|
+
import A from "embla-carousel-react";
|
|
6
|
+
import { useIntl as M } from "react-intl";
|
|
7
|
+
import { CarouselContext as P } from "./Carousel.context.js";
|
|
8
|
+
import { createCarouselAccessibilityOptions as D } from "./Carousel.options.js";
|
|
9
|
+
import { useCarouselAccessibility as G } from "./hooks/useCarouselAccessibility.js";
|
|
10
|
+
import { useCarouselState as K } from "./hooks/useCarouselState.js";
|
|
11
|
+
import { CarouselNav as O } from "./parts/CarouselNav.js";
|
|
12
|
+
const E = j({
|
|
13
|
+
base: "uy:relative uy:flex uy:flex-col uy:gap-200 uy:md:gap-300"
|
|
14
|
+
}), k = R(
|
|
15
|
+
({
|
|
16
|
+
children: t,
|
|
17
|
+
className: u,
|
|
18
|
+
options: s = {
|
|
19
|
+
slidesToScroll: "auto"
|
|
20
|
+
},
|
|
21
|
+
"aria-label": a,
|
|
22
|
+
...c
|
|
23
|
+
}, n) => {
|
|
24
|
+
const e = M(), m = h(
|
|
25
|
+
() => D(e),
|
|
26
|
+
[e]
|
|
27
|
+
), [d, o] = A(
|
|
28
|
+
{
|
|
29
|
+
draggable: !0,
|
|
30
|
+
...s,
|
|
31
|
+
axis: "x"
|
|
32
|
+
},
|
|
33
|
+
[w(m)]
|
|
34
|
+
), {
|
|
35
|
+
canGoToNext: p,
|
|
36
|
+
canGoToPrev: f,
|
|
37
|
+
visibleSlideIndexes: i,
|
|
38
|
+
focusedSlideIndex: y,
|
|
39
|
+
goToPrev: v,
|
|
40
|
+
goToNext: x,
|
|
41
|
+
goTo: g,
|
|
42
|
+
goToSlide: b,
|
|
43
|
+
selectedSnap: C,
|
|
44
|
+
snapCount: T
|
|
45
|
+
} = K(o), { liveRegionRef: N, a11yIds: l, onKeyDown: S } = G({
|
|
46
|
+
api: o,
|
|
47
|
+
visibleSlideIndexes: i,
|
|
48
|
+
focusedSlideIndex: y,
|
|
49
|
+
loop: s.loop ?? !1,
|
|
50
|
+
goToSlide: b
|
|
51
|
+
});
|
|
52
|
+
return /* @__PURE__ */ r(
|
|
53
|
+
P.Provider,
|
|
54
|
+
{
|
|
55
|
+
value: {
|
|
56
|
+
carouselRef: d,
|
|
57
|
+
api: o,
|
|
58
|
+
goToNext: x,
|
|
59
|
+
goToPrev: v,
|
|
60
|
+
goTo: g,
|
|
61
|
+
canGoToPrev: f,
|
|
62
|
+
canGoToNext: p,
|
|
63
|
+
visibleSlideIndexes: i,
|
|
64
|
+
selectedSnap: C,
|
|
65
|
+
snapCount: T,
|
|
66
|
+
a11yIds: l
|
|
67
|
+
},
|
|
68
|
+
children: /* @__PURE__ */ I(
|
|
69
|
+
"div",
|
|
70
|
+
{
|
|
71
|
+
ref: n,
|
|
72
|
+
className: E({ className: u }),
|
|
73
|
+
role: "region",
|
|
74
|
+
"aria-roledescription": e.formatMessage({
|
|
75
|
+
id: "unity:component:carousel:roledescription",
|
|
76
|
+
defaultMessage: "carousel"
|
|
77
|
+
}),
|
|
78
|
+
"aria-label": a,
|
|
79
|
+
"aria-labelledby": a ? void 0 : l.title,
|
|
80
|
+
"data-unity-slot": "carousel",
|
|
81
|
+
onKeyDownCapture: S,
|
|
82
|
+
...c,
|
|
83
|
+
children: [
|
|
84
|
+
t,
|
|
85
|
+
/* @__PURE__ */ r(O, { className: "uy:flex uy:justify-center uy:md:hidden" }),
|
|
86
|
+
/* @__PURE__ */ r(
|
|
87
|
+
"div",
|
|
88
|
+
{
|
|
89
|
+
ref: N,
|
|
90
|
+
"aria-live": "polite",
|
|
91
|
+
"aria-atomic": "true",
|
|
92
|
+
className: "uy:sr-only"
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
k.displayName = "Carousel";
|
|
103
|
+
export {
|
|
104
|
+
k as Carousel,
|
|
105
|
+
E as carousel
|
|
106
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { AccessibilityOptionsType } from 'embla-carousel-accessibility';
|
|
2
|
+
import { IntlShape } from 'react-intl';
|
|
3
|
+
type AriaTextCallbackType = AccessibilityOptionsType['slideAriaLabel'];
|
|
4
|
+
/**
|
|
5
|
+
* Creates a function that generates aria labels for carousel slides
|
|
6
|
+
* based on grouping configuration.
|
|
7
|
+
* @param intl - The react-intl instance for translations
|
|
8
|
+
* @returns A callback function for the Embla accessibility plugin
|
|
9
|
+
*/
|
|
10
|
+
export declare function createSlideAriaLabel(intl: IntlShape): AriaTextCallbackType;
|
|
11
|
+
/**
|
|
12
|
+
* Creates a function that generates live region content for announcing
|
|
13
|
+
* slide changes to screen readers.
|
|
14
|
+
* @param intl - The react-intl instance for translations
|
|
15
|
+
* @returns A callback function for the Embla accessibility plugin
|
|
16
|
+
*/
|
|
17
|
+
export declare function createLiveRegionContent(intl: IntlShape): AriaTextCallbackType;
|
|
18
|
+
/**
|
|
19
|
+
* Creates the default accessibility options for the Embla carousel plugin.
|
|
20
|
+
* @param intl - The react-intl instance for translations
|
|
21
|
+
* @returns Options object for the Embla Accessibility plugin
|
|
22
|
+
*/
|
|
23
|
+
export declare function createCarouselAccessibilityOptions(intl: IntlShape): AccessibilityOptionsType;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
function g(e) {
|
|
2
|
+
return (i, a, l, s, r, n) => {
|
|
3
|
+
const o = a + 1, u = l + 1, t = r + 1;
|
|
4
|
+
return i ? a === l ? e.formatMessage(
|
|
5
|
+
{
|
|
6
|
+
id: "unity:component:carousel:a11y:slide:label:group-single",
|
|
7
|
+
defaultMessage: "Slide {slide} of {totalSlides} (group {snap} of {totalSnaps})"
|
|
8
|
+
},
|
|
9
|
+
{ slide: o, totalSlides: s, snap: t, totalSnaps: n }
|
|
10
|
+
) : e.formatMessage(
|
|
11
|
+
{
|
|
12
|
+
id: "unity:component:carousel:a11y:slide:label:group-multiple",
|
|
13
|
+
defaultMessage: "Slides {slide}-{lastSlide} of {totalSlides} (group {snap} of {totalSnaps})"
|
|
14
|
+
},
|
|
15
|
+
{ slide: o, lastSlide: u, totalSlides: s, snap: t, totalSnaps: n }
|
|
16
|
+
) : e.formatMessage(
|
|
17
|
+
{
|
|
18
|
+
id: "unity:component:carousel:a11y:slide:label:single",
|
|
19
|
+
defaultMessage: "Slide {slide} of {totalSlides}"
|
|
20
|
+
},
|
|
21
|
+
{ slide: o, totalSlides: s }
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function c(e) {
|
|
26
|
+
return (i, a, l, s, r, n) => {
|
|
27
|
+
const o = a + 1, u = l + 1, t = r + 1;
|
|
28
|
+
return i ? a === l ? e.formatMessage(
|
|
29
|
+
{
|
|
30
|
+
id: "unity:component:carousel:a11y:live-region:group-single",
|
|
31
|
+
defaultMessage: "Showing slide {slide} of {totalSlides} (group {snap} of {totalSnaps})"
|
|
32
|
+
},
|
|
33
|
+
{ slide: o, totalSlides: s, snap: t, totalSnaps: n }
|
|
34
|
+
) : e.formatMessage(
|
|
35
|
+
{
|
|
36
|
+
id: "unity:component:carousel:a11y:live-region:group-multiple",
|
|
37
|
+
defaultMessage: "Showing slides {slide}-{lastSlide} of {totalSlides} (group {snap} of {totalSnaps})"
|
|
38
|
+
},
|
|
39
|
+
{ slide: o, lastSlide: u, totalSlides: s, snap: t, totalSnaps: n }
|
|
40
|
+
) : e.formatMessage(
|
|
41
|
+
{
|
|
42
|
+
id: "unity:component:carousel:a11y:live-region:single",
|
|
43
|
+
defaultMessage: "Showing slide {slide} of {totalSlides}"
|
|
44
|
+
},
|
|
45
|
+
{ slide: o, totalSlides: s }
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function d(e) {
|
|
50
|
+
return {
|
|
51
|
+
announceChanges: !0,
|
|
52
|
+
carouselAriaLabel: e.formatMessage({
|
|
53
|
+
id: "unity:component:carousel:a11y:carousel:label",
|
|
54
|
+
defaultMessage: "Carousel"
|
|
55
|
+
}),
|
|
56
|
+
slideAriaLabel: g(e),
|
|
57
|
+
liveRegionContent: c(e)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export {
|
|
61
|
+
d as createCarouselAccessibilityOptions,
|
|
62
|
+
c as createLiveRegionContent,
|
|
63
|
+
g as createSlideAriaLabel
|
|
64
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { EmblaCarouselType } from 'embla-carousel';
|
|
2
|
+
import { KeyboardEvent, RefObject } from 'react';
|
|
3
|
+
export interface UseCarouselAccessibilityOptions {
|
|
4
|
+
api: EmblaCarouselType | undefined;
|
|
5
|
+
visibleSlideIndexes: ReadonlySet<number>;
|
|
6
|
+
focusedSlideIndex: number | null;
|
|
7
|
+
loop?: boolean;
|
|
8
|
+
goToSlide: (slideIndex: number) => void;
|
|
9
|
+
}
|
|
10
|
+
export interface UseCarouselAccessibilityReturn {
|
|
11
|
+
liveRegionRef: RefObject<HTMLDivElement>;
|
|
12
|
+
a11yIds: {
|
|
13
|
+
root: string;
|
|
14
|
+
track: string;
|
|
15
|
+
previousButton: string;
|
|
16
|
+
nextButton: string;
|
|
17
|
+
title: string;
|
|
18
|
+
};
|
|
19
|
+
onKeyDown: (e: KeyboardEvent<HTMLDivElement>) => void;
|
|
20
|
+
}
|
|
21
|
+
export declare function useCarouselAccessibility({ api, visibleSlideIndexes, focusedSlideIndex, loop, goToSlide, }: UseCarouselAccessibilityOptions): UseCarouselAccessibilityReturn;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { useRef as k, useEffect as _, useCallback as D } from "react";
|
|
2
|
+
import "embla-carousel-accessibility";
|
|
3
|
+
import { useId as x } from "react-aria";
|
|
4
|
+
function E({
|
|
5
|
+
api: e,
|
|
6
|
+
visibleSlideIndexes: d,
|
|
7
|
+
focusedSlideIndex: o,
|
|
8
|
+
loop: u = !1,
|
|
9
|
+
goToSlide: c
|
|
10
|
+
}) {
|
|
11
|
+
const v = k(null), n = k(!1), l = x(), h = {
|
|
12
|
+
root: `carousel-${l}__root`,
|
|
13
|
+
title: `carousel-${l}__title`,
|
|
14
|
+
track: `carousel-${l}__track`,
|
|
15
|
+
previousButton: `carousel-${l}__previous-button`,
|
|
16
|
+
nextButton: `carousel-${l}__next-button`
|
|
17
|
+
};
|
|
18
|
+
_(() => {
|
|
19
|
+
if (!e) return;
|
|
20
|
+
const r = e.plugins().accessibility;
|
|
21
|
+
!r || !v.current || r.setupLiveRegion(v.current);
|
|
22
|
+
}, [e]), _(() => {
|
|
23
|
+
if (!e) return;
|
|
24
|
+
const r = e.slideNodes();
|
|
25
|
+
r.forEach((a, i) => {
|
|
26
|
+
i === o ? (a.setAttribute("tabindex", "0"), a.removeAttribute("inert")) : (a.setAttribute("tabindex", "-1"), d.has(i) ? a.removeAttribute("inert") : a.setAttribute("inert", ""));
|
|
27
|
+
}), n.current && o !== null && (n.current = !1, r[o]?.focus({ preventScroll: !0 }));
|
|
28
|
+
}, [e, d, o]);
|
|
29
|
+
const m = D(
|
|
30
|
+
(r) => {
|
|
31
|
+
if (!e) return;
|
|
32
|
+
const i = e.slideNodes().length, s = o ?? 0;
|
|
33
|
+
switch (r.key) {
|
|
34
|
+
case "ArrowLeft": {
|
|
35
|
+
r.preventDefault();
|
|
36
|
+
const t = s > 0 ? s - 1 : u ? i - 1 : s;
|
|
37
|
+
t !== s && (n.current = !0, c(t));
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
case "ArrowRight": {
|
|
41
|
+
r.preventDefault();
|
|
42
|
+
const t = s < i - 1 ? s + 1 : u ? 0 : s;
|
|
43
|
+
t !== s && (n.current = !0, c(t));
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
case "Home": {
|
|
47
|
+
r.preventDefault(), u || (n.current = !0, c(0));
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
case "End": {
|
|
51
|
+
if (r.preventDefault(), !u) {
|
|
52
|
+
const t = i - 1;
|
|
53
|
+
n.current = !0, c(t);
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
case "PageUp": {
|
|
58
|
+
r.preventDefault();
|
|
59
|
+
const t = e.selectedSnap(), p = e.snapList().length, f = t > 0 ? t - 1 : u ? p - 1 : t;
|
|
60
|
+
if (f !== t) {
|
|
61
|
+
const b = e.internalEngine(), g = b.slidesToScroll.groupSlides(
|
|
62
|
+
b.slideIndexes
|
|
63
|
+
)[f]?.[0];
|
|
64
|
+
g !== void 0 && (n.current = !0, c(g));
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case "PageDown": {
|
|
69
|
+
r.preventDefault();
|
|
70
|
+
const t = e.selectedSnap(), p = e.snapList().length, f = t < p - 1 ? t + 1 : u ? 0 : t;
|
|
71
|
+
if (f !== t) {
|
|
72
|
+
const b = e.internalEngine(), g = b.slidesToScroll.groupSlides(
|
|
73
|
+
b.slideIndexes
|
|
74
|
+
)[f]?.[0];
|
|
75
|
+
g !== void 0 && (n.current = !0, c(g));
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
[e, u, o, c]
|
|
82
|
+
);
|
|
83
|
+
return { liveRegionRef: v, a11yIds: h, onKeyDown: m };
|
|
84
|
+
}
|
|
85
|
+
export {
|
|
86
|
+
E as useCarouselAccessibility
|
|
87
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { EmblaCarouselApi } from '../types.js';
|
|
2
|
+
export interface UseCarouselReturn {
|
|
3
|
+
selectedSnap: number;
|
|
4
|
+
snapCount: number;
|
|
5
|
+
visibleSlideIndexes: Set<number>;
|
|
6
|
+
focusedSlideIndex: number | null;
|
|
7
|
+
goToPrev: () => void;
|
|
8
|
+
goToNext: () => void;
|
|
9
|
+
goTo: (index: number) => void;
|
|
10
|
+
canGoToPrev: () => boolean | undefined;
|
|
11
|
+
canGoToNext: () => boolean | undefined;
|
|
12
|
+
goToSlide: (slideIndex: number) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare function useCarouselState(emblaApi: EmblaCarouselApi): UseCarouselReturn;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useState as d, useRef as h, useCallback as s, useEffect as L } from "react";
|
|
2
|
+
function y(e) {
|
|
3
|
+
const [I, l] = d(0), [G, x] = d(0), [w, T] = d(
|
|
4
|
+
() => /* @__PURE__ */ new Set()
|
|
5
|
+
), [N, u] = d(
|
|
6
|
+
null
|
|
7
|
+
), i = h(!1), c = s(() => {
|
|
8
|
+
if (!e) return { visible: /* @__PURE__ */ new Set(), focused: null };
|
|
9
|
+
const n = e.selectedSnap(), o = e.internalEngine(), t = (o.slidesToScroll.groupSlides(o.slideIndexes)[n] ?? [])[0] ?? null, g = e.slidesInView(), v = new Set(g);
|
|
10
|
+
return t !== null && v.add(t), {
|
|
11
|
+
visible: v,
|
|
12
|
+
focused: t
|
|
13
|
+
};
|
|
14
|
+
}, [e]), S = s(() => {
|
|
15
|
+
if (!e) return;
|
|
16
|
+
const n = e.selectedSnap();
|
|
17
|
+
l(n);
|
|
18
|
+
const o = c();
|
|
19
|
+
T(o.visible), i.current || u(o.focused);
|
|
20
|
+
}, [e, c]), f = s(() => {
|
|
21
|
+
if (!e) return;
|
|
22
|
+
x(e.snapList().length);
|
|
23
|
+
const n = e.selectedSnap();
|
|
24
|
+
l(n);
|
|
25
|
+
}, [e]);
|
|
26
|
+
L(() => {
|
|
27
|
+
if (!e) return;
|
|
28
|
+
x(e.snapList().length);
|
|
29
|
+
const n = c();
|
|
30
|
+
return T(n.visible), u(n.focused), e.on("select", S), e.on("resize", f), () => {
|
|
31
|
+
e.off("select", S), e.off("resize", f);
|
|
32
|
+
};
|
|
33
|
+
}, [e, S, f, c]);
|
|
34
|
+
const C = s(() => e?.goToPrev(), [e]), P = s(() => e?.goToNext(), [e]), a = s((n) => e?.goTo(n), [e]), z = s(() => e?.canGoToPrev(), [e]), E = s(() => e?.canGoToNext(), [e]), R = s(
|
|
35
|
+
(n) => {
|
|
36
|
+
if (!e) return;
|
|
37
|
+
i.current = !0, u(n);
|
|
38
|
+
const o = e.internalEngine(), r = o.slidesToScroll.groupSlides(o.slideIndexes).findIndex(
|
|
39
|
+
(g) => g.includes(n)
|
|
40
|
+
), t = e.selectedSnap();
|
|
41
|
+
r !== -1 && r !== t && e.goTo(r), setTimeout(() => {
|
|
42
|
+
i.current = !1;
|
|
43
|
+
}, 0);
|
|
44
|
+
},
|
|
45
|
+
[e]
|
|
46
|
+
);
|
|
47
|
+
return {
|
|
48
|
+
selectedSnap: I,
|
|
49
|
+
snapCount: G,
|
|
50
|
+
visibleSlideIndexes: w,
|
|
51
|
+
focusedSlideIndex: N,
|
|
52
|
+
goToPrev: C,
|
|
53
|
+
goToNext: P,
|
|
54
|
+
goTo: a,
|
|
55
|
+
canGoToPrev: z,
|
|
56
|
+
canGoToNext: E,
|
|
57
|
+
goToSlide: R
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export {
|
|
61
|
+
y as useCarouselState
|
|
62
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { PropsWithChildren } from 'react';
|
|
2
|
+
import { ResponsiveValue } from '../../../hooks/use-responsive-value.js';
|
|
3
|
+
import { SpacingToken } from '../../../utils/spacing.js';
|
|
4
|
+
export declare const carouselContent: import('tailwind-variants').TVReturnType<{
|
|
5
|
+
[key: string]: {
|
|
6
|
+
[key: string]: import('tailwind-merge').ClassNameValue | {
|
|
7
|
+
track?: import('tailwind-merge').ClassNameValue;
|
|
8
|
+
root?: import('tailwind-merge').ClassNameValue;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
} | {
|
|
12
|
+
[x: string]: {
|
|
13
|
+
[x: string]: import('tailwind-merge').ClassNameValue | {
|
|
14
|
+
track?: import('tailwind-merge').ClassNameValue;
|
|
15
|
+
root?: import('tailwind-merge').ClassNameValue;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
} | {}, {
|
|
19
|
+
root: string[];
|
|
20
|
+
track: string[];
|
|
21
|
+
}, undefined, {
|
|
22
|
+
[key: string]: {
|
|
23
|
+
[key: string]: import('tailwind-merge').ClassNameValue | {
|
|
24
|
+
track?: import('tailwind-merge').ClassNameValue;
|
|
25
|
+
root?: import('tailwind-merge').ClassNameValue;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
} | {}, {
|
|
29
|
+
root: string[];
|
|
30
|
+
track: string[];
|
|
31
|
+
}, import('tailwind-variants').TVReturnType<unknown, {
|
|
32
|
+
root: string[];
|
|
33
|
+
track: string[];
|
|
34
|
+
}, undefined, unknown, unknown, undefined>>;
|
|
35
|
+
export interface CarouselContentProps extends PropsWithChildren {
|
|
36
|
+
/**
|
|
37
|
+
* Number of slides visible at once, optionally configured per breakpoint.
|
|
38
|
+
* @example
|
|
39
|
+
* itemsPerPage={3}
|
|
40
|
+
* itemsPerPage={{ base: 1, md: 2, lg: 3 }}
|
|
41
|
+
*/
|
|
42
|
+
itemsPerPage?: number | ResponsiveValue<number>;
|
|
43
|
+
/** Gap between slides — Unity spacing token only (e.g. `'$200'`). */
|
|
44
|
+
gap?: SpacingToken | ResponsiveValue<SpacingToken>;
|
|
45
|
+
/** Additional padding to add to the first and last slides, for visual alignment purposes */
|
|
46
|
+
trackPadding?: {
|
|
47
|
+
start?: SpacingToken | ResponsiveValue<SpacingToken>;
|
|
48
|
+
end?: SpacingToken | ResponsiveValue<SpacingToken>;
|
|
49
|
+
};
|
|
50
|
+
/** Additional CSS classes for the viewport element. */
|
|
51
|
+
className?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Negative horizontal margin to apply to the carousel viewport, for cases where you place the carousel inside a padded container
|
|
54
|
+
*/
|
|
55
|
+
trackOffset?: number | SpacingToken | ResponsiveValue<number | SpacingToken>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The scrollable viewport that houses carousel slides.
|
|
59
|
+
* `CarouselContent` controls the visual layout of the carousel: how many slides are visible
|
|
60
|
+
* at once, spacing between slides, and optional padding to create a "peek" effect showing
|
|
61
|
+
* adjacent slides.
|
|
62
|
+
* ## Layout Control
|
|
63
|
+
* - **itemsPerPage**: Number of slides visible at once (supports fractional values like `2.5`)
|
|
64
|
+
* - **gap**: Spacing between slides (Unity spacing tokens only, e.g., `'$200'`)
|
|
65
|
+
* - **trackPadding**: Padding on start/end edges to align with the header, in cases where you use `trackOffset`
|
|
66
|
+
* - **trackOffset**: Horizontal offset to align slides with page content
|
|
67
|
+
* ## Responsive Layout
|
|
68
|
+
* All layout props support responsive values using the `{ base, md, lg }` syntax:
|
|
69
|
+
* @example
|
|
70
|
+
* ```tsx
|
|
71
|
+
* <CarouselContent
|
|
72
|
+
* itemsPerPage={{ base: 1, md: 2, lg: 3 }}
|
|
73
|
+
* gap={{ base: '$100', md: '$200' }}
|
|
74
|
+
* trackPadding={{ end: { base: '$100', md: '$200' } }}
|
|
75
|
+
* >
|
|
76
|
+
* <CarouselSlide>Slide 1</CarouselSlide>
|
|
77
|
+
* <CarouselSlide>Slide 2</CarouselSlide>
|
|
78
|
+
* </CarouselContent>
|
|
79
|
+
* ```
|
|
80
|
+
* ## Fractional Items Per Page
|
|
81
|
+
* Use fractional values to create a "bleeding" effect where the next slide is partially visible:
|
|
82
|
+
* @example
|
|
83
|
+
* ```tsx
|
|
84
|
+
* <CarouselContent itemsPerPage={2.3} gap="$200">
|
|
85
|
+
* <CarouselSlide>Slide 1</CarouselSlide>
|
|
86
|
+
* <CarouselSlide>Slide 2</CarouselSlide>
|
|
87
|
+
* <CarouselSlide>Slide 3</CarouselSlide>
|
|
88
|
+
* </CarouselContent>
|
|
89
|
+
* ```
|
|
90
|
+
* @param {CarouselContentProps} props - Component props
|
|
91
|
+
* @param {CarouselContentProps['itemsPerPage']} props.itemsPerPage - Number of slides visible at once
|
|
92
|
+
* @param {CarouselContentProps['gap']} props.gap - Spacing between slides (Unity spacing tokens)
|
|
93
|
+
* @param {CarouselContentProps['trackPadding']} props.trackPadding - Padding to reveal adjacent slides
|
|
94
|
+
* @param {CarouselContentProps['trackOffset']} props.trackOffset - Horizontal offset for alignment
|
|
95
|
+
* @param {CarouselContentProps['className']} props.className - Additional CSS classes
|
|
96
|
+
* @param {CarouselContentProps['children']} props.children - CarouselSlide components
|
|
97
|
+
* @see {@link CarouselContentProps} for all available props
|
|
98
|
+
*/
|
|
99
|
+
declare function CarouselContent({ className, children, itemsPerPage: responsiveItemsPerPage, gap: responsiveGap, trackPadding: responsiveTrackPadding, trackOffset: responsiveTrackOffset, }: CarouselContentProps): import("react/jsx-runtime").JSX.Element;
|
|
100
|
+
declare namespace CarouselContent {
|
|
101
|
+
var displayName: string;
|
|
102
|
+
}
|
|
103
|
+
export { CarouselContent };
|