Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

react-aria-carousel

Package Overview
Dependencies
Maintainers
0
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

react-aria-carousel - npm Package Compare versions

Comparing version 0.1.0 to 0.2.0

src/utils/useCallbackRef.ts

52

dist/index.d.ts

@@ -0,4 +1,7 @@

import * as react from 'react';
import { ElementType, ComponentPropsWithoutRef, Dispatch, SetStateAction, ReactNode, ReactElement } from 'react';
import * as react_jsx_runtime from 'react/jsx-runtime';
/**
* Options for useCarouselState
*/
interface CarouselStateProps {

@@ -36,5 +39,22 @@ /**

initialPages?: number[][];
/** Whether the carousel should scroll when the user drags with their mouse */
mouseDragging?: boolean;
/**
* Ref object that reflects whether the user is actively dragging
* the carousel with their mouse
*/
isDraggingRef?: {
current: boolean;
};
/**
* Handler called when the activePageIndex changes
*/
onActivePageIndexChange?: ({ index }: {
index: number;
}) => void;
}
/**
* API returned by useCarouselState
*/
interface CarouselState extends Required<Pick<CarouselStateProps, "itemsPerPage" | "scrollBy">> {
/** The collection of items in the carousel. */
/** The index of the page in view. */

@@ -56,2 +76,5 @@ readonly activePageIndex: number;

/**
* Options for useCarousel
*/
interface CarouselOptions extends CarouselStateProps {

@@ -84,2 +107,5 @@ /**

}
/**
* API returned by useCarousel
*/
interface CarouselAria extends CarouselState {

@@ -97,5 +123,9 @@ autoplayUserPreference: boolean;

readonly scrollerProps: Attributes<"div">;
/** Props for the autoplay toggle element */
readonly autoplayControlProps: Attributes<"button">;
}
declare function useCarousel(props?: CarouselOptions): [Dispatch<SetStateAction<HTMLElement | null>>, CarouselAria];
declare function useCarousel({ itemsPerPage, loop, orientation, spaceBetweenItems, mouseDragging, autoplay: propAutoplay, autoplayInterval, scrollPadding, onActivePageIndexChange, }?: CarouselOptions): [
Dispatch<SetStateAction<HTMLElement | null>>,
CarouselAria
];

@@ -126,3 +156,3 @@ interface CarouselTabOptions {

}
declare function Carousel({ children, spaceBetweenItems, scrollPadding, mouseDragging, autoplay, autoplayInterval, itemsPerPage, loop, orientation, scrollBy, initialPages, ...props }: CarouselProps): react_jsx_runtime.JSX.Element;
declare const Carousel: react.ForwardRefExoticComponent<CarouselProps & react.RefAttributes<HTMLDivElement>>;

@@ -136,6 +166,6 @@ interface CarouselTabListProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {

}
declare function CarouselTabs({ children, ...props }: CarouselTabListProps): react_jsx_runtime.JSX.Element;
declare const CarouselTabs: react.ForwardRefExoticComponent<CarouselTabListProps & react.RefAttributes<HTMLDivElement>>;
interface CarouselTabProps extends CarouselTabOptions, ComponentPropsWithoutRef<"button"> {
}
declare function CarouselTab(props: CarouselTabProps): react_jsx_runtime.JSX.Element;
declare const CarouselTab: react.ForwardRefExoticComponent<CarouselTabProps & react.RefAttributes<HTMLButtonElement>>;

@@ -146,3 +176,3 @@ interface CarouselButtonProps extends Omit<ComponentPropsWithoutRef<"button">, "dir"> {

}
declare function CarouselButton({ dir, ...props }: CarouselButtonProps): react_jsx_runtime.JSX.Element;
declare const CarouselButton: react.ForwardRefExoticComponent<CarouselButtonProps & react.RefAttributes<HTMLButtonElement>>;

@@ -159,3 +189,5 @@ interface CarouselScrollerProps<T> extends Omit<ComponentPropsWithoutRef<"div">, "children"> {

}
declare function CarouselScroller<T>({ children, items, ...props }: CarouselScrollerProps<T>): react_jsx_runtime.JSX.Element;
declare const CarouselScroller: <T>(props: CarouselScrollerProps<T> & {
ref?: react.ForwardedRef<HTMLElement>;
}) => JSX.Element;

@@ -167,3 +199,3 @@ interface CarouselAutoplayControlProps extends Omit<ComponentPropsWithoutRef<"button">, "children"> {

}
declare function CarouselAutoplayControl({ children, ...props }: CarouselAutoplayControlProps): react_jsx_runtime.JSX.Element;
declare const CarouselAutoplayControl: react.ForwardRefExoticComponent<CarouselAutoplayControlProps & react.RefAttributes<HTMLButtonElement>>;

@@ -174,4 +206,4 @@ interface CarouselItemProps extends ComponentPropsWithoutRef<"div"> {

}
declare function CarouselItem({ index, ...props }: CarouselItemProps): react_jsx_runtime.JSX.Element;
declare const CarouselItem: react.ForwardRefExoticComponent<CarouselItemProps & react.RefAttributes<HTMLDivElement>>;
export { Carousel, type CarouselAria, CarouselAutoplayControl, type CarouselAutoplayControlProps, CarouselButton, type CarouselButtonProps, CarouselItem, type CarouselItemAria, type CarouselItemOptions, type CarouselOptions, type CarouselProps, CarouselScroller, type CarouselScrollerProps, CarouselTab, type CarouselTabListProps, type CarouselTabOptions, type CarouselTabProps, CarouselTabs, useCarousel, useCarouselItem, useCarouselTab };

535

dist/index.js
// src/useCarousel.ts
import {
useCallback as useCallback4,
useCallback as useCallback5,
useId,

@@ -9,3 +9,3 @@ useState as useState4

// src/useCarouselState.ts
import { useCallback as useCallback2, useEffect as useEffect4, useState as useState2 } from "react";
import { useCallback as useCallback3, useEffect as useEffect5, useMemo as useMemo2, useState as useState2 } from "react";

@@ -117,2 +117,3 @@ // src/utils/useAriaBusyScroll.ts

return {
isDraggingRef: dragging,
scrollerProps: {

@@ -135,8 +136,18 @@ onMouseDown: handleDragStart

// src/utils/useCallbackRef.ts
import { useEffect as useEffect3, useMemo, useRef as useRef2 } from "react";
function useCallbackRef(callback) {
const callbackRef = useRef2(callback);
useEffect3(() => {
callbackRef.current = callback;
});
return useMemo(() => (...args) => callbackRef.current?.(...args), []);
}
// src/utils/usePrefersReducedMotion.ts
import { useEffect as useEffect3, useState } from "react";
import { useEffect as useEffect4, useState } from "react";
var QUERY = "(prefers-reduced-motion: no-preference)";
function usePrefersReducedMotion() {
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
useEffect3(() => {
useEffect4(() => {
const mediaQueryList = window.matchMedia(QUERY);

@@ -155,2 +166,20 @@ setPrefersReducedMotion(!window.matchMedia(QUERY).matches);

// src/utils/useMergedRef.ts
import { useCallback as useCallback2 } from "react";
function assignRef(ref, value) {
if (typeof ref === "function") {
ref(value);
} else if (typeof ref === "object" && ref !== null && "current" in ref) {
ref.current = value;
}
}
function mergeRefs(...refs) {
return (node) => {
refs.forEach((ref) => assignRef(ref, node));
};
}
function useMergedRef(...refs) {
return useCallback2(mergeRefs(...refs), refs);
}
// src/utils/mergeProps.ts

@@ -202,18 +231,20 @@ function mergeProps(...args) {

// src/useCarouselState.ts
function useCarouselState(props, ref) {
const {
itemsPerPage = 1,
scrollBy = "page",
loop = false,
initialPages = []
} = props;
function useCarouselState({
itemsPerPage = 1,
scrollBy = "page",
loop = false,
initialPages = [],
isDraggingRef,
mouseDragging,
onActivePageIndexChange: propChangeHandler
}, host) {
const onActivePageIndexChange = useCallbackRef(propChangeHandler);
const [activePageIndex, setActivePageIndex] = useState2(0);
const [pages, setPages] = useState2(initialPages);
const prefersReducedMotion = usePrefersReducedMotion();
const scroller = ref;
const getItems = useCallback2(
const getItems = useCallback3(
({ includeClones } = { includeClones: false }) => {
if (!scroller)
if (!host)
return [];
let allChildren = Array.from(scroller.children);
let allChildren = Array.from(host.children);
if (includeClones)

@@ -223,21 +254,21 @@ return allChildren;

},
[scroller]
[host]
);
const scrollToItem = useCallback2(
const scrollToItem = useCallback3(
(slide, behavior = "smooth") => {
if (!scroller)
if (!host)
return;
const scrollContainerRect = scroller.getBoundingClientRect();
const scrollContainerRect = host.getBoundingClientRect();
const nextSlideRect = slide.getBoundingClientRect();
const nextLeft = nextSlideRect.left - scrollContainerRect.left;
const nextTop = nextSlideRect.top - scrollContainerRect.top;
scroller.scrollTo({
left: nextLeft + scroller.scrollLeft,
top: nextTop + scroller.scrollTop,
host.scrollTo({
left: nextLeft + host.scrollLeft,
top: nextTop + host.scrollTop,
behavior: prefersReducedMotion ? "instant" : behavior
});
},
[prefersReducedMotion, scroller]
[prefersReducedMotion, host]
);
const scrollToPage = useCallback2(
const scrollToPage = useCallback3(
(index, behavior) => {

@@ -253,3 +284,3 @@ const items = getItems();

);
const updateSnaps = useCallback2(() => {
const updateSnaps = useCallback3(() => {
const actualItemsPerPage = Math.floor(itemsPerPage);

@@ -265,3 +296,3 @@ getItems({ includeClones: true }).forEach((item, index) => {

}, [getItems, itemsPerPage, scrollBy]);
const calculatePages = useCallback2(() => {
const calculatePages = useCallback3(() => {
const items = getItems();

@@ -287,6 +318,10 @@ const actualItemsPerPage = Math.floor(itemsPerPage);

setActivePageIndex((prev2) => {
return clamp(0, prev2, newPages.length - 1);
const index = clamp(0, prev2, newPages.length - 1);
if (index !== prev2) {
onActivePageIndexChange?.({ index });
}
return index;
});
}, [getItems, itemsPerPage]);
const scrollToPageIndex = useCallback2(
}, [getItems, itemsPerPage, onActivePageIndexChange]);
const scrollToPageIndex = useCallback3(
(index) => {

@@ -330,10 +365,10 @@ const items = getItems();

);
const next = useCallback2(() => {
const next = useCallback3(() => {
return scrollToPageIndex(activePageIndex + 1);
}, [activePageIndex, scrollToPageIndex]);
const prev = useCallback2(() => {
const prev = useCallback3(() => {
return scrollToPageIndex(activePageIndex - 1);
}, [activePageIndex, scrollToPageIndex]);
useEffect4(() => {
if (!scroller || pages.length === 0)
useEffect5(() => {
if (!host || pages.length === 0)
return;

@@ -356,3 +391,3 @@ getItems({ includeClones: true }).forEach((item) => {

clone.setAttribute("aria-hidden", "true");
scroller.prepend(clone);
host.prepend(clone);
});

@@ -364,3 +399,3 @@ firstPage.forEach((slide) => {

clone.setAttribute("aria-hidden", "true");
scroller.append(clone);
host.append(clone);
});

@@ -376,7 +411,7 @@ }

scrollToPage,
scroller,
host,
updateSnaps
]);
useEffect4(() => {
if (!scroller)
useEffect5(() => {
if (!host)
return;

@@ -399,68 +434,108 @@ calculatePages();

});
mutationObserver.observe(scroller, { childList: true, subtree: true });
mutationObserver.observe(host, { childList: true, subtree: true });
return () => {
mutationObserver.disconnect();
};
}, [getItems, scroller, calculatePages, updateSnaps]);
useEffect4(() => {
if (!scroller)
}, [getItems, host, calculatePages, updateSnaps]);
useEffect5(() => {
if (!host)
return;
function handle() {
const intersectionObserver = new IntersectionObserver(
(entries) => {
intersectionObserver.disconnect();
const firstIntersecting = entries.find(
(entry) => entry.isIntersecting
);
if (firstIntersecting) {
if (loop === "infinite" && firstIntersecting.target.hasAttribute("data-clone")) {
const cloneIndex = firstIntersecting.target.getAttribute("data-carousel-item");
const actualItem = getItems().find(
(el) => el.getAttribute("data-carousel-item") === cloneIndex
);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
scrollToItem(actualItem, "instant");
});
});
} else {
const indexString = firstIntersecting.target.dataset.carouselItem;
if (process.env.NODE_ENV !== "production") {
if (!indexString) {
throw new Error(
"Failed to find data-carousel-item HTML attribute on an item."
);
}
}
const slideIndex = parseInt(indexString, 10);
const activePage = pages.findIndex(
(page) => page.includes(slideIndex)
);
setActivePageIndex(clamp(0, activePage, getItems().length));
}
const hasIntersected = /* @__PURE__ */ new Set();
const intersectionObserver = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting && !hasIntersected.has(entry.target)) {
hasIntersected.add(entry.target);
}
},
{
root: scroller,
threshold: 0.6
if (!entry.isIntersecting) {
hasIntersected.delete(entry.target);
}
}
);
for (let child of getItems({ includeClones: true })) {
intersectionObserver.observe(child);
},
{
root: host,
threshold: 0.6
}
);
const children = getItems({ includeClones: true });
for (let child of children) {
intersectionObserver.observe(child);
}
scroller.addEventListener("scrollend", handle);
function handleScrollEnd() {
if (hasIntersected.size === 0)
return;
const sorted = [...hasIntersected].sort((a, b) => {
return a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
});
const firstIntersecting = sorted[0];
if (loop === "infinite" && firstIntersecting.hasAttribute("data-clone") && !(mouseDragging && isDraggingRef?.current)) {
const cloneIndex = firstIntersecting.getAttribute("data-carousel-item");
const actualItem = getItems().find(
(el) => el.getAttribute("data-carousel-item") === cloneIndex
);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
scrollToItem(actualItem, "instant");
});
});
} else {
const indexString = firstIntersecting.dataset.carouselItem;
if (process.env.NODE_ENV !== "production") {
if (!indexString) {
throw new Error(
"Failed to find data-carousel-item HTML attribute on an item."
);
}
}
setActivePageIndex((prev2) => {
const slideIndex = parseInt(indexString, 10);
const activePage = pages.findIndex((page) => page[0] === slideIndex);
const newIndex = clamp(0, activePage, getItems().length);
if (prev2 !== newIndex) {
onActivePageIndexChange?.({ index: newIndex });
}
return newIndex;
});
}
}
let timeout;
function handleScroll() {
clearTimeout(timeout);
timeout = setTimeout(() => {
if (mouseDragging && isDraggingRef?.current)
return;
handleScrollEnd();
}, 150);
}
host.addEventListener("scroll", handleScroll, { passive: true });
return () => {
scroller.removeEventListener("scrollend", handle);
clearTimeout(timeout);
for (let child of children) {
intersectionObserver.unobserve(child);
}
intersectionObserver.disconnect();
host.removeEventListener("scroll", handleScroll);
};
}, [getItems, loop, pages, scrollToItem, scroller]);
return {
itemsPerPage,
activePageIndex,
scrollBy,
}, [
getItems,
isDraggingRef,
loop,
mouseDragging,
onActivePageIndexChange,
pages,
next,
prev,
scrollToPage
};
scrollToItem,
host
]);
return useMemo2(
() => ({
itemsPerPage,
activePageIndex,
scrollBy,
pages,
next,
prev,
scrollToPage
}),
[activePageIndex, itemsPerPage, next, pages, prev, scrollBy, scrollToPage]
);
}

@@ -470,9 +545,9 @@

import {
useCallback as useCallback3,
useEffect as useEffect5,
useCallback as useCallback4,
useEffect as useEffect6,
useLayoutEffect,
useRef as useRef2,
useRef as useRef3,
useState as useState3
} from "react";
var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect5;
var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect6;
function useAutoplay({

@@ -489,3 +564,3 @@ enabled,

);
const pause = useCallback3(() => {
const pause = useCallback4(() => {
if (!enabled)

@@ -495,3 +570,3 @@ return;

}, [enabled]);
const play = useCallback3(() => {
const play = useCallback4(() => {
if (!enabled)

@@ -501,3 +576,3 @@ return;

}, [enabled]);
useEffect5(() => {
useEffect6(() => {
function listener() {

@@ -530,7 +605,7 @@ if (document.visibilityState === "hidden") {

function useInterval(callback, delay) {
const savedCallback = useRef2(callback);
const savedCallback = useRef3(callback);
useIsomorphicLayoutEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect5(() => {
useEffect6(() => {
if (delay === null) {

@@ -549,23 +624,28 @@ return;

// src/useCarousel.ts
function useCarousel(props = {
itemsPerPage: 1,
loop: false,
orientation: "horizontal",
spaceBetweenItems: "0px",
mouseDragging: false,
autoplay: false,
autoplayInterval: 5e3
}) {
function useCarousel({
itemsPerPage = 1,
loop = false,
orientation = "horizontal",
spaceBetweenItems = "0px",
mouseDragging = false,
autoplay: propAutoplay = false,
autoplayInterval = 5e3,
scrollPadding,
onActivePageIndexChange
} = {}) {
const [host, setHost] = useState4(null);
const {
itemsPerPage = 1,
loop = false,
orientation = "horizontal",
spaceBetweenItems: spaceBetweenSlides = "0px",
scrollPadding,
mouseDragging = false,
autoplay: propAutoplay = false,
autoplayInterval = 5e3
} = props;
const [host, setHost] = useState4(null);
const state = useCarouselState(props, host);
isDraggingRef,
scrollerProps: { onMouseDown }
} = useMouseDrag(host);
const state = useCarouselState(
{
itemsPerPage,
loop,
mouseDragging,
isDraggingRef,
onActivePageIndexChange
},
host
);
const { pages, activePageIndex, next, prev, scrollToPage } = state;

@@ -583,3 +663,3 @@ const scrollerId = useId();

});
const handleKeyDown = useCallback4(
const handleKeyDown = useCallback5(
(e) => {

@@ -637,3 +717,3 @@ if (![

);
const handleTablistKeydown = useCallback4(
const handleTablistKeydown = useCallback5(
(e) => {

@@ -654,5 +734,2 @@ const nextIndex = handleKeyDown(e);

useAriaBusyScroll(host);
const {
scrollerProps: { onMouseDown }
} = useMouseDrag(host);
return [

@@ -702,6 +779,6 @@ setHost,

style: {
[orientation === "horizontal" ? "gridAutoColumns" : "gridAutoRows"]: `calc(100% / ${itemsPerPage} - ${spaceBetweenSlides} * ${itemsPerPage - 1} / ${itemsPerPage})`,
gap: spaceBetweenSlides,
[orientation === "horizontal" ? "scrollPaddingInline" : "scrollPaddingBlock"]: scrollPadding,
[orientation === "horizontal" ? "paddingInline" : "paddingBlock"]: scrollPadding
[`gridAuto${orientation === "horizontal" ? "Columns" : "Rows"}`]: `calc(100% / ${itemsPerPage} - ${spaceBetweenItems} * ${itemsPerPage - 1} / ${itemsPerPage})`,
[`scrollPadding${orientation === "horizontal" ? "Inline" : "Block"}`]: scrollPadding,
[`padding${orientation === "horizontal" ? "Inline" : "Block"}`]: scrollPadding,
gap: spaceBetweenItems
}

@@ -729,3 +806,3 @@ },

role: "tab",
"aria-label": `Go to item ${current} of ${setSize}`,
"aria-label": `Go to page ${current} of ${setSize}`,
"aria-posinset": current,

@@ -752,3 +829,3 @@ "aria-setsize": setSize,

const shouldSnap = scrollBy === "item" || (index + actualItemsPerPage) % actualItemsPerPage === 0;
const itemCount = pages?.flat().length;
const itemCount = new Set(pages?.flat()).size;
const label = itemCount ? `${index + 1} of ${itemCount}` : void 0;

@@ -771,2 +848,5 @@ const isInert = pages?.[activePageIndex]?.includes(index);

// src/Carousel.tsx
import { forwardRef } from "react";
// src/context.ts

@@ -786,18 +866,6 @@ import { createContext, useContext } from "react";

import { jsx } from "react/jsx-runtime";
function Carousel({
children,
spaceBetweenItems = "16px",
scrollPadding,
mouseDragging,
autoplay,
autoplayInterval,
itemsPerPage = 1,
loop,
orientation = "horizontal",
scrollBy = "page",
initialPages = [],
...props
}) {
const carouselProps = {
spaceBetweenItems,
var Carousel = forwardRef(
function Carousel2({
children,
spaceBetweenItems = "16px",
scrollPadding,

@@ -807,24 +875,43 @@ mouseDragging,

autoplayInterval,
itemsPerPage,
itemsPerPage = 1,
loop,
orientation,
scrollBy,
initialPages
};
const [assignRef, carouselState] = useCarousel(carouselProps);
return /* @__PURE__ */ jsx(
Context.Provider,
{
value: {
assignRef,
carouselState,
carouselProps
},
children: /* @__PURE__ */ jsx("div", { ...mergeProps(carouselState.rootProps, props), children })
}
);
}
orientation = "horizontal",
scrollBy = "page",
initialPages = [],
onActivePageIndexChange,
...props
}, ref) {
const carouselProps = {
spaceBetweenItems,
scrollPadding,
mouseDragging,
autoplay,
autoplayInterval,
itemsPerPage,
loop,
orientation,
scrollBy,
initialPages,
onActivePageIndexChange
};
const [assignRef2, carouselState] = useCarousel(carouselProps);
return /* @__PURE__ */ jsx(
Context.Provider,
{
value: {
assignRef: assignRef2,
carouselState,
carouselProps
},
children: /* @__PURE__ */ jsx("div", { ...mergeProps(carouselState.rootProps, props), ref, children })
}
);
}
);
// src/CarouselTabs.tsx
import { Fragment } from "react";
import {
forwardRef as forwardRef2,
Fragment
} from "react";

@@ -910,23 +997,49 @@ // ../node_modules/@react-aria/utils/dist/useId.mjs

import { jsx as jsx2 } from "react/jsx-runtime";
function CarouselTabs({ children, ...props }) {
const { carouselState } = useCarouselContext();
return /* @__PURE__ */ jsx2("div", { ...$3ef42575df84b30b$export$9d1611c77c2fe928(carouselState?.tablistProps, props), children: carouselState?.pages.map((_, index) => /* @__PURE__ */ jsx2(Fragment, { children: children({
isSelected: index === carouselState?.activePageIndex,
index
}) }, index)) });
}
function CarouselTab(props) {
const { carouselState } = useCarouselContext();
const { index } = props;
const { tabProps } = useCarouselTab({ index }, carouselState);
return /* @__PURE__ */ jsx2("button", { type: "button", ...$3ef42575df84b30b$export$9d1611c77c2fe928(tabProps, props) });
}
var CarouselTabs = forwardRef2(
function CarouselTabs2({ children, ...props }, forwardedRef) {
const { carouselState } = useCarouselContext();
return /* @__PURE__ */ jsx2(
"div",
{
ref: forwardedRef,
...$3ef42575df84b30b$export$9d1611c77c2fe928(carouselState?.tablistProps, props),
children: carouselState?.pages.map((_, index) => /* @__PURE__ */ jsx2(Fragment, { children: children({
isSelected: index === carouselState?.activePageIndex,
index
}) }, index))
}
);
}
);
var CarouselTab = forwardRef2(
function CarouselTab2(props, forwardedRef) {
const { carouselState } = useCarouselContext();
const { index } = props;
const { tabProps } = useCarouselTab({ index }, carouselState);
return /* @__PURE__ */ jsx2(
"button",
{
type: "button",
...$3ef42575df84b30b$export$9d1611c77c2fe928(tabProps, props),
ref: forwardedRef
}
);
}
);
// src/CarouselButton.tsx
import { forwardRef as forwardRef3 } from "react";
import { jsx as jsx3 } from "react/jsx-runtime";
function CarouselButton({ dir, ...props }) {
var CarouselButton = forwardRef3(function CarouselButton2({ dir, ...props }, forwardedRef) {
const { carouselState } = useCarouselContext();
const buttonProps = dir === "prev" ? carouselState?.prevButtonProps : carouselState?.nextButtonProps;
return /* @__PURE__ */ jsx3("button", { type: "button", ...$3ef42575df84b30b$export$9d1611c77c2fe928(buttonProps, props) });
}
return /* @__PURE__ */ jsx3(
"button",
{
ref: forwardedRef,
type: "button",
...$3ef42575df84b30b$export$9d1611c77c2fe928(buttonProps, props)
}
);
});

@@ -936,27 +1049,32 @@ // src/CarouselScroller.tsx

import { jsx as jsx4 } from "react/jsx-runtime";
function CarouselScroller({
children,
items,
...props
}) {
function _CarouselScroller({ children, items, ...props }, forwardedRef) {
const context = useCarouselContext();
const { assignRef, carouselState } = context;
const { assignRef: assignRef2, carouselState } = context;
const ref = useMergedRef(assignRef2, forwardedRef);
const kids = typeof children === "function" ? items.map(children) : children;
function getChildren() {
let kidsArr = React.Children.toArray(kids);
if (kidsArr.length === 1 && kidsArr[0].type === React.Fragment) {
kidsArr = React.Children.toArray(kidsArr[0].props.children);
}
return kidsArr;
}
return /* @__PURE__ */ jsx4(
"div",
{
ref: assignRef,
ref,
...mergeProps(carouselState?.scrollerProps, props),
style: { ...carouselState?.scrollerProps.style, ...props?.style },
children: React.Children.map(kids, (child, index) => /* @__PURE__ */ jsx4(IndexContext.Provider, { value: index, children: child }))
children: getChildren().map((child, index) => {
return /* @__PURE__ */ jsx4(IndexContext.Provider, { value: index, children: child }, index);
})
}
);
}
var CarouselScroller = React.forwardRef(_CarouselScroller);
// src/CarouselAutoplayControl.tsx
import { forwardRef as forwardRef5 } from "react";
import { jsx as jsx5 } from "react/jsx-runtime";
function CarouselAutoplayControl({
children,
...props
}) {
var CarouselAutoplayControl = forwardRef5(function CarouselAutoplayControl2({ children, ...props }, forwardedRef) {
const { carouselState } = useCarouselContext();

@@ -967,2 +1085,3 @@ return /* @__PURE__ */ jsx5(

type: "button",
ref: forwardedRef,
...$3ef42575df84b30b$export$9d1611c77c2fe928(carouselState?.autoplayControlProps, props),

@@ -974,16 +1093,18 @@ children: typeof children === "function" ? children({

);
}
});
// src/CarouselItem.tsx
import { useContext as useContext2 } from "react";
import { forwardRef as forwardRef6, useContext as useContext2 } from "react";
import { jsx as jsx6 } from "react/jsx-runtime";
function CarouselItem({ index, ...props }) {
const ctx = useCarouselContext();
const itemIndex = useContext2(IndexContext);
const { itemProps } = useCarouselItem(
{ index: index ?? itemIndex },
ctx.carouselState
);
return /* @__PURE__ */ jsx6("div", { ...mergeProps(itemProps, props) });
}
var CarouselItem = forwardRef6(
function CarouselItem2({ index, ...props }, forwardedRef) {
const ctx = useCarouselContext();
const itemIndex = useContext2(IndexContext);
const { itemProps } = useCarouselItem(
{ index: index ?? itemIndex },
ctx.carouselState
);
return /* @__PURE__ */ jsx6("div", { ref: forwardedRef, ...mergeProps(itemProps, props) });
}
);
export {

@@ -990,0 +1111,0 @@ Carousel,

{
"name": "react-aria-carousel",
"version": "0.1.0",
"version": "0.2.0",
"exports": {

@@ -20,2 +20,3 @@ ".": {

"build": "tsup",
"dev": "tsup --watch",
"test": "vitest",

@@ -22,0 +23,0 @@ "test-storybook": "test-storybook",

@@ -27,2 +27,5 @@ "use client";

/**
* Options for useCarousel
*/
export interface CarouselOptions extends CarouselStateProps {

@@ -56,2 +59,5 @@ /**

/**
* API returned by useCarousel
*/
export interface CarouselAria extends CarouselState {

@@ -69,28 +75,35 @@ autoplayUserPreference: boolean;

readonly scrollerProps: Attributes<"div">;
/** Props for the autoplay toggle element */
readonly autoplayControlProps: Attributes<"button">;
}
export function useCarousel(
props: CarouselOptions = {
itemsPerPage: 1,
loop: false,
orientation: "horizontal",
spaceBetweenItems: "0px",
mouseDragging: false,
autoplay: false,
autoplayInterval: 5000,
},
): [Dispatch<SetStateAction<HTMLElement | null>>, CarouselAria] {
export function useCarousel({
itemsPerPage = 1,
loop = false,
orientation = "horizontal",
spaceBetweenItems = "0px",
mouseDragging = false,
autoplay: propAutoplay = false,
autoplayInterval = 5000,
scrollPadding,
onActivePageIndexChange,
}: CarouselOptions = {}): [
Dispatch<SetStateAction<HTMLElement | null>>,
CarouselAria,
] {
const [host, setHost] = useState<HTMLElement | null>(null);
const {
itemsPerPage = 1,
loop = false,
orientation = "horizontal",
spaceBetweenItems: spaceBetweenSlides = "0px",
scrollPadding,
mouseDragging = false,
autoplay: propAutoplay = false,
autoplayInterval = 5000,
} = props;
const [host, setHost] = useState<HTMLElement | null>(null);
const state = useCarouselState(props, host);
isDraggingRef,
scrollerProps: { onMouseDown },
} = useMouseDrag(host);
const state = useCarouselState(
{
itemsPerPage,
loop,
mouseDragging,
isDraggingRef,
onActivePageIndexChange,
},
host,
);
const { pages, activePageIndex, next, prev, scrollToPage } = state;

@@ -187,5 +200,2 @@ const scrollerId = useId();

useAriaBusyScroll(host);
const {
scrollerProps: { onMouseDown },
} = useMouseDrag(host);

@@ -244,10 +254,8 @@ return [

style: {
[orientation === "horizontal" ? "gridAutoColumns" : "gridAutoRows"]:
`calc(100% / ${itemsPerPage} - ${spaceBetweenSlides} * ${itemsPerPage - 1} / ${itemsPerPage})`,
gap: spaceBetweenSlides,
[orientation === "horizontal"
? "scrollPaddingInline"
: "scrollPaddingBlock"]: scrollPadding,
[orientation === "horizontal" ? "paddingInline" : "paddingBlock"]:
[`gridAuto${orientation === "horizontal" ? "Columns" : "Rows"}`]: `calc(100% / ${itemsPerPage} - ${spaceBetweenItems} * ${itemsPerPage - 1} / ${itemsPerPage})`,
[`scrollPadding${orientation === "horizontal" ? "Inline" : "Block"}`]:
scrollPadding,
[`padding${orientation === "horizontal" ? "Inline" : "Block"}`]:
scrollPadding,
gap: spaceBetweenItems,
},

@@ -254,0 +262,0 @@ },

@@ -31,3 +31,3 @@ "use client";

(index! + actualItemsPerPage) % actualItemsPerPage === 0;
const itemCount = pages?.flat().length;
const itemCount = new Set(pages?.flat()).size;
const label = itemCount ? `${index! + 1} of ${itemCount}` : undefined;

@@ -34,0 +34,0 @@ const isInert = pages?.[activePageIndex]?.includes(index!);

@@ -1,7 +0,8 @@

"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { clamp, useCallbackRef, usePrefersReducedMotion } from "./utils";
import { clamp, usePrefersReducedMotion } from "./utils";
/**
* Options for useCarouselState
*/
export interface CarouselStateProps {

@@ -39,8 +40,20 @@ /**

initialPages?: number[][];
/** Whether the carousel should scroll when the user drags with their mouse */
mouseDragging?: boolean;
/**
* Ref object that reflects whether the user is actively dragging
* the carousel with their mouse
*/
isDraggingRef?: { current: boolean };
/**
* Handler called when the activePageIndex changes
*/
onActivePageIndexChange?: ({ index }: { index: number }) => void;
}
/**
* API returned by useCarouselState
*/
export interface CarouselState
extends Required<Pick<CarouselStateProps, "itemsPerPage" | "scrollBy">> {
/** The collection of items in the carousel. */
// readonly collection: Collection<Node<T>>;
/** The index of the page in view. */

@@ -59,6 +72,3 @@ readonly activePageIndex: number;

export function useCarouselState(
props: CarouselStateProps,
ref: HTMLElement | null,
): CarouselState {
const {
{
itemsPerPage = 1,

@@ -68,7 +78,12 @@ scrollBy = "page",

initialPages = [],
} = props;
isDraggingRef,
mouseDragging,
onActivePageIndexChange: propChangeHandler,
}: CarouselStateProps,
host: HTMLElement | null,
): CarouselState {
const onActivePageIndexChange = useCallbackRef(propChangeHandler);
const [activePageIndex, setActivePageIndex] = useState(0);
const [pages, setPages] = useState<number[][]>(initialPages);
const prefersReducedMotion = usePrefersReducedMotion();
const scroller = ref;

@@ -79,8 +94,8 @@ const getItems = useCallback(

): HTMLElement[] => {
if (!scroller) return [];
let allChildren = Array.from(scroller.children) as HTMLElement[];
if (!host) return [];
let allChildren = Array.from(host.children) as HTMLElement[];
if (includeClones) return allChildren;
return allChildren.filter((child) => !child.hasAttribute("data-clone"));
},
[scroller],
[host],
);

@@ -90,4 +105,4 @@

(slide: HTMLElement, behavior: ScrollBehavior = "smooth"): void => {
if (!scroller) return;
const scrollContainerRect = scroller.getBoundingClientRect();
if (!host) return;
const scrollContainerRect = host.getBoundingClientRect();
const nextSlideRect = slide.getBoundingClientRect();

@@ -98,9 +113,9 @@

scroller.scrollTo({
left: nextLeft + scroller.scrollLeft,
top: nextTop + scroller.scrollTop,
host.scrollTo({
left: nextLeft + host.scrollLeft,
top: nextTop + host.scrollTop,
behavior: prefersReducedMotion ? "instant" : behavior,
});
},
[prefersReducedMotion, scroller],
[prefersReducedMotion, host],
);

@@ -159,7 +174,10 @@

setActivePageIndex((prev) => {
return clamp(0, prev, newPages.length - 1);
const index = clamp(0, prev, newPages.length - 1);
if (index !== prev) {
onActivePageIndexChange?.({ index });
}
return index;
});
}, [getItems, itemsPerPage]);
}, [getItems, itemsPerPage, onActivePageIndexChange]);
// @TODO: Rewrite this better
const scrollToPageIndex = useCallback(

@@ -175,2 +193,3 @@ (index: number): number => {

// @TODO: This should be rewritten for clarity and brevity
if (loop === "infinite") {

@@ -223,3 +242,3 @@ // The index allowing to be inclusive of cloned pages

useEffect(() => {
if (!scroller || pages.length === 0) return;
if (!host || pages.length === 0) return;

@@ -235,3 +254,3 @@ getItems({ includeClones: true }).forEach((item) => {

const firstPage = pages[0];
// We're gonna modify this in a second, so make sure not to mutate state
// We're gonna modify this in a second with .reverse, so make sure not to mutate state
const lastPage = [...pages.at(-1)!];

@@ -246,3 +265,3 @@

clone.setAttribute("aria-hidden", "true");
scroller.prepend(clone);
host.prepend(clone);
});

@@ -255,3 +274,3 @@

clone.setAttribute("aria-hidden", "true");
scroller.append(clone);
host.append(clone);
});

@@ -272,3 +291,3 @@ }

scrollToPage,
scroller,
host,
updateSnaps,

@@ -278,3 +297,3 @@ ]);

useEffect(() => {
if (!scroller) return;
if (!host) return;

@@ -299,77 +318,130 @@ calculatePages();

mutationObserver.observe(scroller, { childList: true, subtree: true });
mutationObserver.observe(host, { childList: true, subtree: true });
return () => {
mutationObserver.disconnect();
};
}, [getItems, scroller, calculatePages, updateSnaps]);
}, [getItems, host, calculatePages, updateSnaps]);
useEffect(() => {
if (!scroller) return;
function handle() {
const intersectionObserver = new IntersectionObserver(
(entries) => {
intersectionObserver.disconnect();
const firstIntersecting = entries.find(
(entry) => entry.isIntersecting,
);
if (!host) return;
if (firstIntersecting) {
if (
loop === "infinite" &&
firstIntersecting.target.hasAttribute("data-clone")
) {
const cloneIndex =
firstIntersecting.target.getAttribute("data-carousel-item");
const actualItem = getItems().find(
(el) => el.getAttribute("data-carousel-item") === cloneIndex,
);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
scrollToItem(actualItem!, "instant");
});
});
} else {
const indexString = (firstIntersecting.target as HTMLElement)
.dataset.carouselItem;
const hasIntersected = new Set<Element>();
if (process.env.NODE_ENV !== "production") {
if (!indexString) {
throw new Error(
"Failed to find data-carousel-item HTML attribute on an item.",
);
}
}
const intersectionObserver = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting && !hasIntersected.has(entry.target)) {
hasIntersected.add(entry.target);
}
if (!entry.isIntersecting) {
hasIntersected.delete(entry.target);
}
}
},
{
root: host,
threshold: 0.6,
},
);
const children = getItems({ includeClones: true });
for (let child of children) {
intersectionObserver.observe(child);
}
const slideIndex = parseInt(indexString!, 10);
const activePage = pages.findIndex((page) =>
page.includes(slideIndex),
);
setActivePageIndex(clamp(0, activePage, getItems().length));
}
function handleScrollEnd() {
if (hasIntersected.size === 0) return;
const sorted = [...hasIntersected].sort((a, b) => {
return a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING
? -1
: 1;
});
const firstIntersecting = sorted[0];
if (
loop === "infinite" &&
firstIntersecting.hasAttribute("data-clone") &&
!(mouseDragging && isDraggingRef?.current)
) {
const cloneIndex = firstIntersecting.getAttribute("data-carousel-item");
const actualItem = getItems().find(
(el) => el.getAttribute("data-carousel-item") === cloneIndex,
);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
scrollToItem(actualItem!, "instant");
});
});
} else {
const indexString = (firstIntersecting as HTMLElement).dataset
.carouselItem;
if (process.env.NODE_ENV !== "production") {
if (!indexString) {
throw new Error(
"Failed to find data-carousel-item HTML attribute on an item.",
);
}
},
{
root: scroller,
threshold: 0.6,
},
);
for (let child of getItems({ includeClones: true })) {
intersectionObserver.observe(child);
}
setActivePageIndex((prev) => {
const slideIndex = parseInt(indexString!, 10);
const activePage = pages.findIndex((page) => page[0] === slideIndex);
const newIndex = clamp(0, activePage, getItems().length);
if (prev !== newIndex) {
onActivePageIndexChange?.({ index: newIndex });
}
return newIndex;
});
}
}
scroller.addEventListener("scrollend", handle);
// Ideally we'd use the 'scrollend' event here.
// However, some browsers will call the 'scrollend' handler *before*
// snapping has settled. So in effect, the user will release the scroll,
// then 'scrollend' event is called, and the element continues to scroll
// to the closest snap position
//
// This will let us check whether scrolling has actually stopped and
// whether the user is still dragging
let timeout: any;
function handleScroll() {
clearTimeout(timeout);
timeout = setTimeout(() => {
if (mouseDragging && isDraggingRef?.current) return;
handleScrollEnd();
}, 150);
}
host.addEventListener("scroll", handleScroll, { passive: true });
return () => {
scroller.removeEventListener("scrollend", handle);
clearTimeout(timeout);
for (let child of children) {
intersectionObserver.unobserve(child);
}
intersectionObserver.disconnect();
host.removeEventListener("scroll", handleScroll);
};
}, [getItems, loop, pages, scrollToItem, scroller]);
}, [
getItems,
isDraggingRef,
loop,
mouseDragging,
onActivePageIndexChange,
pages,
scrollToItem,
host,
]);
return {
itemsPerPage,
activePageIndex,
scrollBy,
pages,
next,
prev,
scrollToPage,
};
return useMemo(
() => ({
itemsPerPage,
activePageIndex,
scrollBy,
pages,
next,
prev,
scrollToPage,
}),
[activePageIndex, itemsPerPage, next, pages, prev, scrollBy, scrollToPage],
);
}

@@ -30,3 +30,3 @@ "use client";

role: "tab",
"aria-label": `Go to item ${current} of ${setSize}`,
"aria-label": `Go to page ${current} of ${setSize}`,
"aria-posinset": current,

@@ -33,0 +33,0 @@ "aria-setsize": setSize,

@@ -5,3 +5,5 @@ import { ComponentPropsWithoutRef, ElementType } from "react";

export * from "./useMouseDrag";
export * from "./useCallbackRef";
export * from "./usePrefersReducedMotion";
export * from "./useMergedRef";
export * from "./mergeProps";

@@ -8,0 +10,0 @@

@@ -80,2 +80,3 @@ /**

if (!host) return;
// Primary click (usually left-click)
let canDrag = event.button === 0;

@@ -110,2 +111,3 @@ if (canDrag) {

return {
isDraggingRef: dragging,
scrollerProps: {

@@ -112,0 +114,0 @@ onMouseDown: handleDragStart,

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc