nuka-carousel
Version:
Pure React Carousel
546 lines (526 loc) • 20.5 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __getProtoOf = Object.getPrototypeOf;
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));
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.tsx
var src_exports = {};
__export(src_exports, {
Carousel: () => Carousel,
CarouselProvider: () => CarouselProvider,
useCarousel: () => useCarousel
});
module.exports = __toCommonJS(src_exports);
// src/hooks/use-carousel.tsx
var import_react = __toESM(require("react"));
var CarouselContext = import_react.default.createContext(
{}
);
var CarouselProvider = CarouselContext.Provider;
var useCarousel = () => {
const context = (0, import_react.useContext)(CarouselContext);
return context;
};
// src/Carousel/Carousel.tsx
var import_react9 = require("react");
// src/hooks/use-interval.tsx
var import_react2 = require("react");
function useInterval(callback, delay, enabled = true) {
const _callback = (0, import_react2.useRef)();
(0, import_react2.useEffect)(() => {
_callback.current = callback;
}, [callback]);
(0, import_react2.useEffect)(() => {
if (enabled && delay !== null) {
const id = setInterval(() => {
if (_callback.current) _callback.current();
}, delay);
return () => clearInterval(id);
}
}, [delay, enabled]);
}
// src/hooks/use-paging.tsx
var import_react3 = require("react");
function usePaging({
totalPages,
wrapMode,
initialPage
}) {
const [currentPage, setCurrentPage] = (0, import_react3.useState)(0);
(0, import_react3.useEffect)(() => {
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
var import_react5 = require("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
var import_react4 = require("react");
function useResizeObserver(element) {
const [dimensions, setDimensions] = (0, import_react4.useState)();
(0, import_react4.useEffect)(() => {
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] = (0, import_react5.useState)(0);
const [scrollOffset, setScrollOffset] = (0, import_react5.useState)(arraySeq(totalPages, 0));
const dimensions = useResizeObserver(element);
(0, import_react5.useEffect)(() => {
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
var import_react6 = require("react");
function useHover({
element,
enabled
}) {
const [hovered, setHovered] = (0, import_react6.useState)(false);
const target = element == null ? void 0 : element.current;
(0, import_react6.useEffect)(() => {
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
var import_react7 = require("react");
function useKeyboard({
element,
enabled,
goForward,
goBack
}) {
const target = element == null ? void 0 : element.current;
(0, import_react7.useEffect)(() => {
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
var import_react8 = require("react");
var QUERY = "(prefers-reduced-motion: no-preference)";
var getInitialState = () => isBrowser() ? !window.matchMedia(QUERY).matches : true;
function useReducedMotion({ enabled }) {
const [reduceMotion, setReducedMotion] = (0, import_react8.useState)(getInitialState);
(0, import_react8.useEffect)(() => {
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
var import_jsx_runtime = require("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__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: prevNavClassName, onClick: goBack, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"svg",
{
xmlns: "http://www.w3.org/2000/svg",
viewBox: "0 0 20 20",
fill: "currentcolor",
children: /* @__PURE__ */ (0, import_jsx_runtime.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__ */ (0, import_jsx_runtime.jsx)("div", { className: nextNavClassName, onClick: goForward, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"svg",
{
xmlns: "http://www.w3.org/2000/svg",
viewBox: "0 0 20 20",
fill: "currentcolor",
children: /* @__PURE__ */ (0, import_jsx_runtime.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
var import_jsx_runtime2 = require("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__ */ (0, import_jsx_runtime2.jsx)("div", { className: "nuka-page-container", "data-testid": "pageIndicatorContainer", children: [...Array(totalPages)].map((_, index) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
"button",
{
onClick: () => goToPage(index),
className: className(index),
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("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
var import_jsx_runtime3 = require("react/jsx-runtime");
var defaults = {
arrows: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(NavButtons, {}),
autoplay: false,
autoplayInterval: 3e3,
dots: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PageIndicators, {}),
id: "nuka-carousel",
keyboard: true,
minSwipeDistance: 50,
scrollDistance: "screen",
showArrows: false,
showDots: false,
swiping: true,
wrapMode: "nowrap"
};
var Carousel = (0, import_react9.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 = (0, import_react9.useRef)(null);
const containerRef = (0, import_react9.useRef)(null);
const previousPageRef = (0, import_react9.useRef)(-1);
const arrowsContainerRef = (0, import_react9.useRef)(null);
const { totalPages, scrollOffset } = useMeasurement({
element: containerRef,
scrollDistance
});
const { currentPage, goBack, goForward, goToPage } = usePaging({
totalPages,
wrapMode,
initialPage
});
const [touchStart, setTouchStart] = (0, import_react9.useState)(null);
const [touchEnd, setTouchEnd] = (0, import_react9.useState)(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
});
(0, import_react9.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);
(0, import_react9.useEffect)(() => {
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__ */ (0, import_jsx_runtime3.jsxs)(CarouselProvider, { value: providerValues, children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
"div",
{
className: containerClassName,
"aria-labelledby": "nuka-carousel-heading",
tabIndex: keyboard ? 0 : void 0,
ref: carouselRef,
id,
children: [
title && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { id: "nuka-carousel-heading", className: "nuka-hidden", children: title }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "nuka-slide-container", children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
"div",
{
className: "nuka-overflow",
ref: containerRef,
onTouchEnd,
onTouchMove,
onTouchStart,
id: "nuka-overflow",
"data-testid": "nuka-overflow",
style: { touchAction: "pan-y" },
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
"div",
{
className: "nuka-wrapper",
id: "nuka-wrapper",
"data-testid": "nuka-wrapper",
children
}
)
}
),
showArrows && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref: arrowsContainerRef, children: arrows })
] })
]
}
),
showDots && dots
] });
}
);
Carousel.displayName = "Carousel";
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Carousel,
CarouselProvider,
useCarousel
});
//# sourceMappingURL=index.js.map