@vertisanpro/flowbite-react
Version:
Non-Official React components built for Flowbite and Tailwind CSS
71 lines (70 loc) • 5.33 kB
JavaScript
'use client';
import { HiOutlineChevronLeft, HiOutlineChevronRight } from '@vertisanpro/react-icons/hi';
import { twMerge } from '@vertisanpro/tailwind-merge';
import React, { Children, cloneElement, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import ScrollContainer from 'react-indiana-drag-scroll';
import { isClient } from '../../helpers/is-client';
import { mergeDeep } from '../../helpers/merge-deep';
import { getTheme } from '../../theme-store';
export const Carousel = ({ children, indicators = true, leftControl, rightControl, slide = true, draggable = true, slideInterval, className, theme: customTheme = {}, onSlideChange = null, pauseOnHover = false, ...props }) => {
const theme = mergeDeep(getTheme().carousel, customTheme);
const isDeviceMobile = isClient() && navigator.userAgent.indexOf('IEMobile') !== -1;
const carouselContainer = useRef(null);
const [activeItem, setActiveItem] = useState(0);
const [isDragging, setIsDragging] = useState(false);
const [isHovering, setIsHovering] = useState(false);
const didMountRef = useRef(false);
const items = useMemo(() => Children.map(children, (child) => cloneElement(child, {
className: twMerge(theme.item.base, child.props.className),
})), [children, theme.item.base]);
const navigateTo = useCallback((item) => () => {
if (!items)
return;
item = (item + items.length) % items.length;
if (carouselContainer.current) {
carouselContainer.current.scrollLeft = carouselContainer.current.clientWidth * item;
}
setActiveItem(item);
}, [items]);
useEffect(() => {
if (carouselContainer.current && !isDragging && carouselContainer.current.scrollLeft !== 0) {
setActiveItem(Math.round(carouselContainer.current.scrollLeft / carouselContainer.current.clientWidth));
}
}, [isDragging]);
useEffect(() => {
if (slide && !(pauseOnHover && isHovering)) {
const intervalId = setInterval(() => !isDragging && navigateTo(activeItem + 1)(), slideInterval ?? 3000);
return () => clearInterval(intervalId);
}
}, [activeItem, isDragging, navigateTo, slide, slideInterval, pauseOnHover, isHovering]);
useEffect(() => {
if (didMountRef.current) {
onSlideChange && onSlideChange(activeItem);
}
else {
didMountRef.current = true;
}
}, [onSlideChange, activeItem]);
const handleDragging = (dragging) => () => setIsDragging(dragging);
const setHoveringTrue = useCallback(() => setIsHovering(true), [setIsHovering]);
const setHoveringFalse = useCallback(() => setIsHovering(false), [setIsHovering]);
return (React.createElement("div", { className: twMerge(theme.root.base, className), "data-testid": "carousel", onMouseEnter: setHoveringTrue, onMouseLeave: setHoveringFalse, onTouchStart: setHoveringTrue, onTouchEnd: setHoveringFalse, ...props },
React.createElement(ScrollContainer, { className: twMerge(theme.scrollContainer.base, (isDeviceMobile || !isDragging) && theme.scrollContainer.snap), draggingClassName: "cursor-grab", innerRef: carouselContainer, onEndScroll: handleDragging(false), onStartScroll: handleDragging(draggable), vertical: false, horizontal: draggable }, items?.map((item, index) => (React.createElement("div", { key: index, className: theme.item.wrapper[draggable ? 'on' : 'off'], "data-active": activeItem === index, "data-testid": "carousel-item" }, item)))),
indicators && (React.createElement("div", { className: theme.indicators.wrapper }, items?.map((_, index) => (React.createElement("button", { key: index, className: twMerge(theme.indicators.base, theme.indicators.active[index === activeItem ? 'on' : 'off']), onClick: navigateTo(index), "data-testid": "carousel-indicator", "aria-label": `Slide ${index + 1}` }))))),
items && (React.createElement(React.Fragment, null,
React.createElement("div", { className: theme.root.leftControl },
React.createElement("button", { className: "group", "data-testid": "carousel-left-control", onClick: navigateTo(activeItem - 1), type: "button", "aria-label": "Previous slide" }, leftControl ? leftControl : React.createElement(DefaultLeftControl, { theme: customTheme }))),
React.createElement("div", { className: theme.root.rightControl },
React.createElement("button", { className: "group", "data-testid": "carousel-right-control", onClick: navigateTo(activeItem + 1), type: "button", "aria-label": "Next slide" }, rightControl ? rightControl : React.createElement(DefaultRightControl, { theme: customTheme })))))));
};
const DefaultLeftControl = ({ theme: customTheme = {} }) => {
const theme = mergeDeep(getTheme().carousel, customTheme);
return (React.createElement("span", { className: theme.control.base },
React.createElement(HiOutlineChevronLeft, { className: theme.control.icon })));
};
const DefaultRightControl = ({ theme: customTheme = {} }) => {
const theme = mergeDeep(getTheme().carousel, customTheme);
return (React.createElement("span", { className: theme.control.base },
React.createElement(HiOutlineChevronRight, { className: theme.control.icon })));
};
Carousel.displayName = 'Carousel';