UNPKG

nuka-carousel

Version:
516 lines (497 loc) 18.2 kB
var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); // src/hooks/use-carousel.tsx import React, { useContext } from "react"; var CarouselContext = React.createContext( {} ); var CarouselProvider = CarouselContext.Provider; var useCarousel = () => { const context = useContext(CarouselContext); return context; }; // src/Carousel/Carousel.tsx import { forwardRef, useEffect as useEffect8, useImperativeHandle, useRef as useRef2, useState as useState6 } from "react"; // src/hooks/use-interval.tsx import { useEffect, useRef } from "react"; function useInterval(callback, delay, enabled = true) { const _callback = useRef(); useEffect(() => { _callback.current = callback; }, [callback]); useEffect(() => { if (enabled && delay !== null) { const id = setInterval(() => { if (_callback.current) _callback.current(); }, delay); return () => clearInterval(id); } }, [delay, enabled]); } // src/hooks/use-paging.tsx import { useEffect as useEffect2, useState } from "react"; function usePaging({ totalPages, wrapMode, initialPage }) { const [currentPage, setCurrentPage] = useState(0); useEffect2(() => { if (initialPage) { setCurrentPage(Math.max(0, Math.min(initialPage, totalPages))); } }, [initialPage, totalPages]); const goToPage = (idx) => { if (idx < 0 || idx >= totalPages) return; setCurrentPage(idx); }; const goForward = () => { if (wrapMode === "wrap") { setCurrentPage((prev) => (prev + 1) % totalPages); } else { setCurrentPage((prev) => Math.min(prev + 1, totalPages - 1)); } }; const goBack = () => { if (wrapMode === "wrap") { setCurrentPage((prev) => (prev - 1 + totalPages) % totalPages); } else { setCurrentPage((prev) => Math.max(prev - 1, 0)); } }; return { currentPage, goToPage, goForward, goBack }; } // src/hooks/use-measurement.tsx import { useEffect as useEffect4, useState as useState3 } from "react"; // src/utils/array.ts function arraySeq(length, initialValue) { return new Array(length).fill(0).map((_, i) => i * initialValue); } function arraySum(values) { let sum = 0; return values.map((value) => sum += value); } // src/utils/browser.ts var isBrowser = () => typeof window !== "undefined"; // src/utils/css.ts function cls(...classes) { return classes.filter(Boolean).join(" "); } // src/utils/mouse.ts function isMouseEvent(e) { return "clientY" in e; } // src/hooks/use-resize-observer.tsx import { useEffect as useEffect3, useState as useState2 } from "react"; function useResizeObserver(element) { const [dimensions, setDimensions] = useState2(); useEffect3(() => { if (!element.current) return; const target = element.current; const observer = new ResizeObserver( () => setDimensions(target.getBoundingClientRect()) ); observer.observe(target); return () => { observer.unobserve(target); }; }, [element]); return dimensions; } // src/hooks/use-measurement.tsx function useMeasurement({ element, scrollDistance }) { const [totalPages, setTotalPages] = useState3(0); const [scrollOffset, setScrollOffset] = useState3(arraySeq(totalPages, 0)); const dimensions = useResizeObserver(element); useEffect4(() => { var _a; const container = element.current; if (!(container && dimensions)) return; const scrollWidth = container.scrollWidth; const visibleWidth = container.offsetWidth; const remainder = scrollWidth - visibleWidth; if (visibleWidth === 0) return; switch (scrollDistance) { case "screen": { const pageCount = Math.round(scrollWidth / visibleWidth); setTotalPages(pageCount); setScrollOffset(arraySeq(pageCount, visibleWidth)); break; } case "slide": { const children = ((_a = container.querySelector("#nuka-wrapper")) == null ? void 0 : _a.children) || []; const offsets = Array.from(children).map( (child) => child.offsetWidth ); const scrollOffsets = arraySum([0, ...offsets.slice(0, -1)]); const pageCount = scrollOffsets.findIndex((offset) => offset >= remainder) + 1; setTotalPages(pageCount); setScrollOffset(scrollOffsets); break; } default: { if (typeof scrollDistance === "number" && scrollDistance > 0) { const pageCount = Math.ceil(remainder / scrollDistance) + 1; setTotalPages(pageCount); setScrollOffset(arraySeq(pageCount, scrollDistance)); } } } }, [element, scrollDistance, dimensions]); return { totalPages, scrollOffset }; } // src/hooks/use-hover.tsx import { useEffect as useEffect5, useState as useState4 } from "react"; function useHover({ element, enabled }) { const [hovered, setHovered] = useState4(false); const target = element == null ? void 0 : element.current; useEffect5(() => { if (!(target && target.addEventListener)) return; if (enabled) { const onMouseOver = () => setHovered(true); const onMouseOut = () => setHovered(false); target.addEventListener("mouseover", onMouseOver); target.addEventListener("mouseout", onMouseOut); return () => { target.removeEventListener("mouseover", onMouseOver); target.removeEventListener("mouseout", onMouseOut); }; } }, [target, enabled]); return hovered; } // src/hooks/use-keyboard.tsx import { useEffect as useEffect6 } from "react"; function useKeyboard({ element, enabled, goForward, goBack }) { const target = element == null ? void 0 : element.current; useEffect6(() => { if (target && enabled) { const onKeyDown = (e) => { if (e.key === "ArrowLeft") { goBack(); } else if (e.key === "ArrowRight") { goForward(); } }; target.addEventListener("keydown", onKeyDown); return () => target.removeEventListener("keydown", onKeyDown); } }, [enabled, goBack, goForward, target]); } // src/hooks/use-reduced-motion.tsx import { useEffect as useEffect7, useState as useState5 } from "react"; var QUERY = "(prefers-reduced-motion: no-preference)"; var getInitialState = () => isBrowser() ? !window.matchMedia(QUERY).matches : true; function useReducedMotion({ enabled }) { const [reduceMotion, setReducedMotion] = useState5(getInitialState); useEffect7(() => { if (!(isBrowser() && enabled)) return; const mediaQueryList = window.matchMedia(QUERY); const listener = (event) => { setReducedMotion(!event.matches); }; mediaQueryList.addEventListener("change", listener); return () => { mediaQueryList.removeEventListener("change", listener); }; }, [enabled]); return reduceMotion; } // src/Carousel/NavButtons.tsx import { Fragment, jsx, jsxs } from "react/jsx-runtime"; function NavButtons() { const { currentPage, totalPages, wrapMode, goBack, goForward } = useCarousel(); const allowWrap = wrapMode !== "nowrap"; const enablePrevNavButton = allowWrap || currentPage > 0; const enableNextNavButton = allowWrap || currentPage < totalPages - 1; const prevNavClassName = cls( "nuka-nav-button", "nuka-nav-button-prev", enablePrevNavButton && "nuka-nav-button-enabled" ); const nextNavClassName = cls( "nuka-nav-button", "nuka-nav-button-next", enableNextNavButton && "nuka-nav-button-enabled" ); return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx("div", { className: prevNavClassName, onClick: goBack, children: /* @__PURE__ */ jsx( "svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 20 20", fill: "currentcolor", children: /* @__PURE__ */ jsx( "path", { fillRule: "evenodd", d: "M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z", clipRule: "evenodd" } ) } ) }), /* @__PURE__ */ jsx("div", { className: nextNavClassName, onClick: goForward, children: /* @__PURE__ */ jsx( "svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 20 20", fill: "currentcolor", children: /* @__PURE__ */ jsx( "path", { fillRule: "evenodd", d: "M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z", clipRule: "evenodd" } ) } ) }) ] }); } // src/Carousel/PageIndicators.tsx import { jsx as jsx2 } from "react/jsx-runtime"; var PageIndicators = () => { const { totalPages, currentPage, goToPage } = useCarousel(); const className = (index) => cls( "nuka-page-indicator", currentPage === index ? "nuka-page-indicator-active" : "" ); return /* @__PURE__ */ jsx2("div", { className: "nuka-page-container", "data-testid": "pageIndicatorContainer", children: [...Array(totalPages)].map((_, index) => /* @__PURE__ */ jsx2( "button", { onClick: () => goToPage(index), className: className(index), children: /* @__PURE__ */ jsx2("span", { className: "nuka-hidden", children: index + 1 }) }, index )) }); }; // #style-inject:#style-inject function styleInject(css, { insertAt } = {}) { if (!css || typeof document === "undefined") return; const head = document.head || document.getElementsByTagName("head")[0]; const style = document.createElement("style"); style.type = "text/css"; if (insertAt === "top") { if (head.firstChild) { head.insertBefore(style, head.firstChild); } else { head.appendChild(style); } } else { head.appendChild(style); } if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } } // src/Carousel/Carousel.css styleInject(".nuka-hidden {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n}\n.nuka-container {\n position: relative;\n}\n.nuka-slide-container {\n position: relative;\n}\n.nuka-overflow {\n overflow: scroll;\n -ms-overflow-style: none;\n scrollbar-width: none;\n}\n.nuka-overflow.scroll-smooth {\n scroll-behavior: smooth;\n}\n.nuka-overflow.scroll-auto {\n scroll-behavior: auto;\n}\n.nuka-overflow::-webkit-scrollbar {\n display: none;\n}\n.nuka-wrapper {\n display: flex;\n}\n.nuka-nav-button {\n position: absolute;\n top: calc(50% - 2rem);\n margin: 1rem;\n display: none;\n height: 2rem;\n width: 2rem;\n cursor: pointer;\n background-color: rgba(146, 148, 151, 0.5);\n color: white;\n border-radius: 9999px;\n font-size: 1rem;\n user-select: none;\n}\n.nuka-nav-button.nuka-nav-button-prev {\n left: 0;\n margin-right: 1rem;\n}\n.nuka-nav-button.nuka-nav-button-next {\n right: 0;\n margin-left: 1rem;\n}\n.nuka-nav-button:hover {\n background-color: rgba(146, 148, 151, 0.65);\n}\n.nuka-nav-button-enabled {\n display: block;\n}\n.nuka-container-auto-hide .nuka-nav-button {\n display: none;\n}\n.nuka-container-auto-hide:hover .nuka-nav-button-enabled {\n display: block;\n}\n.nuka-page-container {\n padding-top: 1rem;\n padding-bottom: 1rem;\n display: flex;\n gap: 0.5rem;\n justify-content: center;\n align-items: center;\n}\n.nuka-page-indicator {\n width: 0.75rem;\n height: 0.75rem;\n cursor: pointer;\n border-radius: 9999px;\n border-style: none;\n background-color: rgba(146, 148, 151, 0.65);\n}\n.nuka-page-indicator.nuka-page-indicator-active,\n.nuka-page-indicator.nuka-page-indicator-active:hover {\n background-color: rgb(229 231 235 / 1);\n}\n.nuka-page-indicator:hover {\n background-color: rgb(229 231 235 / 1);\n}\n"); // src/Carousel/Carousel.tsx import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime"; var defaults = { arrows: /* @__PURE__ */ jsx3(NavButtons, {}), autoplay: false, autoplayInterval: 3e3, dots: /* @__PURE__ */ jsx3(PageIndicators, {}), id: "nuka-carousel", keyboard: true, minSwipeDistance: 50, scrollDistance: "screen", showArrows: false, showDots: false, swiping: true, wrapMode: "nowrap" }; var Carousel = forwardRef( (props, ref) => { const options = __spreadValues(__spreadValues({}, defaults), props); const { afterSlide, arrows, autoplay, autoplayInterval, beforeSlide, children, className, dots, id, keyboard, minSwipeDistance, scrollDistance, showArrows, showDots, swiping, title, wrapMode, initialPage } = options; const carouselRef = useRef2(null); const containerRef = useRef2(null); const previousPageRef = useRef2(-1); const arrowsContainerRef = useRef2(null); const { totalPages, scrollOffset } = useMeasurement({ element: containerRef, scrollDistance }); const { currentPage, goBack, goForward, goToPage } = usePaging({ totalPages, wrapMode, initialPage }); const [touchStart, setTouchStart] = useState6(null); const [touchEnd, setTouchEnd] = useState6(null); const onTouchStart = (e) => { if (!swiping) return; setTouchEnd(null); setTouchStart(isMouseEvent(e) ? e.clientX : e.targetTouches[0].clientX); }; const onTouchMove = (e) => { if (!swiping) return; setTouchEnd(isMouseEvent(e) ? e.clientX : e.targetTouches[0].clientX); }; const onTouchEnd = () => { if (!swiping) return; if (!containerRef.current) return; if (!touchStart || !touchEnd) return; const distance = touchStart - touchEnd; const isLeftSwipe = distance > minSwipeDistance; const isRightSwipe = distance < -minSwipeDistance; if (isLeftSwipe || isRightSwipe) { if (isLeftSwipe) { goForward(); } else { goBack(); } } }; useKeyboard({ element: carouselRef, enabled: keyboard, goForward, goBack }); useImperativeHandle(ref, () => ({ goForward, goBack, goToPage }), [ goForward, goBack, goToPage ]); const isHovered = useHover({ element: containerRef, enabled: autoplay }); const isArrowHovered = useHover({ element: arrowsContainerRef, enabled: autoplay && showArrows === true }); const prefersReducedMotion = useReducedMotion({ enabled: autoplay }); const autoplayEnabled = autoplay && !(isHovered || prefersReducedMotion || isArrowHovered); useInterval(goForward, autoplayInterval, autoplayEnabled); useEffect8(() => { if (containerRef.current) { const currentSlideIndex = previousPageRef.current; const endSlideIndex = currentPage; beforeSlide && beforeSlide(currentSlideIndex, endSlideIndex); containerRef.current.scrollLeft = scrollOffset[currentPage]; afterSlide && setTimeout(() => afterSlide(endSlideIndex), 0); previousPageRef.current = currentPage; if (initialPage === void 0 || currentPage === initialPage) { containerRef.current.classList.remove("scroll-auto"); containerRef.current.classList.add("scroll-smooth"); } } }, [currentPage, scrollOffset, beforeSlide, afterSlide, initialPage]); const containerClassName = cls( "nuka-container", showArrows === "hover" && "nuka-container-auto-hide", className ); const providerValues = __spreadProps(__spreadValues({}, options), { totalPages, currentPage, scrollOffset, goBack, goForward, goToPage }); return /* @__PURE__ */ jsxs2(CarouselProvider, { value: providerValues, children: [ /* @__PURE__ */ jsxs2( "div", { className: containerClassName, "aria-labelledby": "nuka-carousel-heading", tabIndex: keyboard ? 0 : void 0, ref: carouselRef, id, children: [ title && /* @__PURE__ */ jsx3("h3", { id: "nuka-carousel-heading", className: "nuka-hidden", children: title }), /* @__PURE__ */ jsxs2("div", { className: "nuka-slide-container", children: [ /* @__PURE__ */ jsx3( "div", { className: "nuka-overflow", ref: containerRef, onTouchEnd, onTouchMove, onTouchStart, id: "nuka-overflow", "data-testid": "nuka-overflow", style: { touchAction: "pan-y" }, children: /* @__PURE__ */ jsx3( "div", { className: "nuka-wrapper", id: "nuka-wrapper", "data-testid": "nuka-wrapper", children } ) } ), showArrows && /* @__PURE__ */ jsx3("div", { ref: arrowsContainerRef, children: arrows }) ] }) ] } ), showDots && dots ] }); } ); Carousel.displayName = "Carousel"; export { Carousel, CarouselProvider, useCarousel }; //# sourceMappingURL=index.mjs.map