@lobehub/ui
Version:
Lobe UI is an open-source UI component library for building AIGC web apps
157 lines (156 loc) • 5.33 kB
JavaScript
"use client";
import ActionIcon from "../ActionIcon/ActionIcon.mjs";
import { useEventCallback } from "../../hooks/useEventCallback.mjs";
import { styles } from "./style.mjs";
import { Children, isValidElement, memo, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
import { cx } from "antd-style";
import useMergeState from "use-merge-value";
import { ChevronLeft, ChevronRight } from "lucide-react";
//#region src/base-ui/Carousel/Carousel.tsx
const DEFAULT_INTERVAL = 5e3;
const SWIPE_THRESHOLD = 40;
const ARROW_SIZE = {
blockSize: 28,
borderRadius: "50%",
size: 16
};
const Carousel = memo(({ adaptiveHeight = false, arrows = false, autoplay = false, children, className, defaultIndex = 0, dots = true, index, loop = true, onIndexChange, onPointerEnter, onPointerLeave, pauseOnHover = true, ref, ...rest }) => {
const slides = Children.toArray(children);
const count = slides.length;
const keys = slides.map((slide, slideIndex) => isValidElement(slide) && slide.key !== null ? slide.key : slideIndex);
const [mergedIndex, setMergedIndex] = useMergeState(defaultIndex, {
onChange: onIndexChange,
value: index
});
const current = Math.min(Math.max(mergedIndex, 0), Math.max(count - 1, 0));
const [hovered, setHovered] = useState(false);
const [height, setHeight] = useState();
const slidesRef = useRef([]);
const swipeStartXRef = useRef(null);
const goTo = useEventCallback((target) => {
if (count === 0) return;
const next = loop ? (target % count + count) % count : Math.min(Math.max(target, 0), count - 1);
if (next !== current) setMergedIndex(next);
});
const next = useEventCallback(() => goTo(current + 1));
const prev = useEventCallback(() => goTo(current - 1));
useImperativeHandle(ref, () => ({
goTo,
next,
prev
}), [
goTo,
next,
prev
]);
const interval = autoplay === true ? DEFAULT_INTERVAL : autoplay || 0;
const paused = pauseOnHover && hovered;
const atEnd = !loop && current >= count - 1;
useEffect(() => {
if (!interval || paused || count < 2 || atEnd) return;
const timer = setTimeout(next, interval);
return () => clearTimeout(timer);
}, [
interval,
paused,
count,
atEnd,
current,
next
]);
useLayoutEffect(() => {
const slide = slidesRef.current[current];
if (!adaptiveHeight || !slide) return;
setHeight(slide.offsetHeight);
if (typeof ResizeObserver === "undefined") return;
const observer = new ResizeObserver(() => setHeight(slide.offsetHeight));
observer.observe(slide);
return () => observer.disconnect();
}, [
adaptiveHeight,
current,
count
]);
return /* @__PURE__ */ jsxs("div", {
"aria-roledescription": "carousel",
className: cx(styles.root, className),
role: "region",
onPointerEnter: (event) => {
onPointerEnter?.(event);
setHovered(true);
},
onPointerLeave: (event) => {
onPointerLeave?.(event);
setHovered(false);
},
...rest,
children: [/* @__PURE__ */ jsxs("div", {
className: styles.viewport,
style: adaptiveHeight && height !== void 0 ? { height } : void 0,
onPointerCancel: () => {
swipeStartXRef.current = null;
},
onPointerDown: (event) => {
if (event.pointerType !== "mouse") swipeStartXRef.current = event.clientX;
},
onPointerUp: (event) => {
if (swipeStartXRef.current === null) return;
const delta = event.clientX - swipeStartXRef.current;
swipeStartXRef.current = null;
if (Math.abs(delta) < SWIPE_THRESHOLD) return;
if (delta < 0) next();
else prev();
},
children: [/* @__PURE__ */ jsx("div", {
"aria-live": interval && !paused ? "off" : "polite",
className: cx(styles.track, adaptiveHeight && styles.trackAdaptive),
style: { transform: `translateX(-${current * 100}%)` },
children: slides.map((slide, slideIndex) => {
const active = slideIndex === current;
return /* @__PURE__ */ jsx("div", {
"aria-hidden": !active || void 0,
"aria-label": `${slideIndex + 1} of ${count}`,
"aria-roledescription": "slide",
className: styles.slide,
inert: !active,
role: "group",
ref: (node) => {
slidesRef.current[slideIndex] = node;
},
children: slide
}, keys[slideIndex]);
})
}), arrows && count > 1 && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ActionIcon, {
"aria-label": "Previous slide",
className: styles.arrow,
disabled: !loop && current === 0,
icon: ChevronLeft,
size: ARROW_SIZE,
style: { insetInlineStart: 8 },
onClick: prev
}), /* @__PURE__ */ jsx(ActionIcon, {
"aria-label": "Next slide",
className: styles.arrow,
disabled: atEnd,
icon: ChevronRight,
size: ARROW_SIZE,
style: { insetInlineEnd: 8 },
onClick: next
})] })]
}), dots && count > 1 && /* @__PURE__ */ jsx("div", {
className: styles.dots,
children: slides.map((_, dotIndex) => /* @__PURE__ */ jsx("button", {
"aria-current": dotIndex === current || void 0,
"aria-label": `Go to slide ${dotIndex + 1}`,
className: styles.dot,
type: "button",
onClick: () => goTo(dotIndex)
}, keys[dotIndex]))
})]
});
});
Carousel.displayName = "Carousel";
//#endregion
export { Carousel as default };
//# sourceMappingURL=Carousel.mjs.map