import useEventCallback from '@restart/hooks/useEventCallback'; import useUpdateEffect from '@restart/hooks/useUpdateEffect'; import useCommittedRef from '@restart/hooks/useCommittedRef'; import useTimeout from '@restart/hooks/useTimeout'; import Anchor from '@restart/ui/Anchor'; import classNames from 'classnames'; import * as React from 'react'; import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; import { useUncontrolled } from 'uncontrollable'; import CarouselCaption from './CarouselCaption'; import CarouselItem from './CarouselItem'; import { map, forEach } from './ElementChildren'; import { useBootstrapPrefix, useIsRTL } from './ThemeProvider'; import transitionEndListener from './transitionEndListener'; import triggerBrowserReflow from './triggerBrowserReflow'; import TransitionWrapper from './TransitionWrapper'; import { jsx as _jsx } from "react/jsx-runtime"; import { jsxs as _jsxs } from "react/jsx-runtime"; import { Fragment as _Fragment } from "react/jsx-runtime"; const SWIPE_THRESHOLD = 40; const defaultProps = { slide: true, fade: false, controls: true, indicators: true, indicatorLabels: [], defaultActiveIndex: 0, interval: 5000, keyboard: true, pause: 'hover', wrap: true, touch: true, prevIcon: /*#__PURE__*/_jsx("span", { "aria-hidden": "true", className: "carousel-control-prev-icon" }), prevLabel: 'Previous', nextIcon: /*#__PURE__*/_jsx("span", { "aria-hidden": "true", className: "carousel-control-next-icon" }), nextLabel: 'Next' }; function isVisible(element) { if (!element || !element.style || !element.parentNode || !element.parentNode.style) { return false; } const elementStyle = getComputedStyle(element); return elementStyle.display !== 'none' && elementStyle.visibility !== 'hidden' && getComputedStyle(element.parentNode).display !== 'none'; } const Carousel = /*#__PURE__*/React.forwardRef((uncontrolledProps, ref) => { const { // Need to define the default "as" during prop destructuring to be compatible with styled-components github.com/react-bootstrap/react-bootstrap/issues/3595 as: Component = 'div', bsPrefix, slide, fade, controls, indicators, indicatorLabels, activeIndex, onSelect, onSlide, onSlid, interval, keyboard, onKeyDown, pause, onMouseOver, onMouseOut, wrap, touch, onTouchStart, onTouchMove, onTouchEnd, prevIcon, prevLabel, nextIcon, nextLabel, variant, className, children, ...props } = useUncontrolled(uncontrolledProps, { activeIndex: 'onSelect' }); const prefix = useBootstrapPrefix(bsPrefix, 'carousel'); const isRTL = useIsRTL(); const nextDirectionRef = useRef(null); const [direction, setDirection] = useState('next'); const [paused, setPaused] = useState(false); const [isSliding, setIsSliding] = useState(false); const [renderedActiveIndex, setRenderedActiveIndex] = useState(activeIndex || 0); useEffect(() => { if (!isSliding && activeIndex !== renderedActiveIndex) { if (nextDirectionRef.current) { setDirection(nextDirectionRef.current); } else { setDirection((activeIndex || 0) > renderedActiveIndex ? 'next' : 'prev'); } if (slide) { setIsSliding(true); } setRenderedActiveIndex(activeIndex || 0); } }, [activeIndex, isSliding, renderedActiveIndex, slide]); useEffect(() => { if (nextDirectionRef.current) { nextDirectionRef.current = null; } }); let numChildren = 0; let activeChildInterval; // Iterate to grab all of the children's interval values // (and count them, too) forEach(children, (child, index) => { ++numChildren; if (index === activeIndex) { activeChildInterval = child.props.interval; } }); const activeChildIntervalRef = useCommittedRef(activeChildInterval); const prev = useCallback(event => { if (isSliding) { return; } let nextActiveIndex = renderedActiveIndex - 1; if (nextActiveIndex < 0) { if (!wrap) { return; } nextActiveIndex = numChildren - 1; } nextDirectionRef.current = 'prev'; onSelect == null ? void 0 : onSelect(nextActiveIndex, event); }, [isSliding, renderedActiveIndex, onSelect, wrap, numChildren]); // This is used in the setInterval, so it should not invalidate. const next = useEventCallback(event => { if (isSliding) { return; } let nextActiveIndex = renderedActiveIndex + 1; if (nextActiveIndex >= numChildren) { if (!wrap) { return; } nextActiveIndex = 0; } nextDirectionRef.current = 'next'; onSelect == null ? void 0 : onSelect(nextActiveIndex, event); }); const elementRef = useRef(); useImperativeHandle(ref, () => ({ element: elementRef.current, prev, next })); // This is used in the setInterval, so it should not invalidate. const nextWhenVisible = useEventCallback(() => { if (!document.hidden && isVisible(elementRef.current)) { if (isRTL) { prev(); } else { next(); } } }); const slideDirection = direction === 'next' ? 'start' : 'end'; useUpdateEffect(() => { if (slide) { // These callbacks will be handled by the callbacks. return; } onSlide == null ? void 0 : onSlide(renderedActiveIndex, slideDirection); onSlid == null ? void 0 : onSlid(renderedActiveIndex, slideDirection); }, [renderedActiveIndex]); const orderClassName = `${prefix}-item-${direction}`; const directionalClassName = `${prefix}-item-${slideDirection}`; const handleEnter = useCallback(node => { triggerBrowserReflow(node); onSlide == null ? void 0 : onSlide(renderedActiveIndex, slideDirection); }, [onSlide, renderedActiveIndex, slideDirection]); const handleEntered = useCallback(() => { setIsSliding(false); onSlid == null ? void 0 : onSlid(renderedActiveIndex, slideDirection); }, [onSlid, renderedActiveIndex, slideDirection]); const handleKeyDown = useCallback(event => { if (keyboard && !/input|textarea/i.test(event.target.tagName)) { switch (event.key) { case 'ArrowLeft': event.preventDefault(); if (isRTL) { next(event); } else { prev(event); } return; case 'ArrowRight': event.preventDefault(); if (isRTL) { prev(event); } else { next(event); } return; default: } } onKeyDown == null ? void 0 : onKeyDown(event); }, [keyboard, onKeyDown, prev, next, isRTL]); const handleMouseOver = useCallback(event => { if (pause === 'hover') { setPaused(true); } onMouseOver == null ? void 0 : onMouseOver(event); }, [pause, onMouseOver]); const handleMouseOut = useCallback(event => { setPaused(false); onMouseOut == null ? void 0 : onMouseOut(event); }, [onMouseOut]); const touchStartXRef = useRef(0); const touchDeltaXRef = useRef(0); const touchUnpauseTimeout = useTimeout(); const handleTouchStart = useCallback(event => { touchStartXRef.current = event.touches[0].clientX; touchDeltaXRef.current = 0; if (pause === 'hover') { setPaused(true); } onTouchStart == null ? void 0 : onTouchStart(event); }, [pause, onTouchStart]); const handleTouchMove = useCallback(event => { if (event.touches && event.touches.length > 1) { touchDeltaXRef.current = 0; } else { touchDeltaXRef.current = event.touches[0].clientX - touchStartXRef.current; } onTouchMove == null ? void 0 : onTouchMove(event); }, [onTouchMove]); const handleTouchEnd = useCallback(event => { if (touch) { const touchDeltaX = touchDeltaXRef.current; if (Math.abs(touchDeltaX) > SWIPE_THRESHOLD) { if (touchDeltaX > 0) { prev(event); } else { next(event); } } } if (pause === 'hover') { touchUnpauseTimeout.set(() => { setPaused(false); }, interval || undefined); } onTouchEnd == null ? void 0 : onTouchEnd(event); }, [touch, pause, prev, next, touchUnpauseTimeout, interval, onTouchEnd]); const shouldPlay = interval != null && !paused && !isSliding; const intervalHandleRef = useRef(); useEffect(() => { var _ref, _activeChildIntervalR; if (!shouldPlay) { return undefined; } const nextFunc = isRTL ? prev : next; intervalHandleRef.current = window.setInterval(document.visibilityState ? nextWhenVisible : nextFunc, (_ref = (_activeChildIntervalR = activeChildIntervalRef.current) != null ? _activeChildIntervalR : interval) != null ? _ref : undefined); return () => { if (intervalHandleRef.current !== null) { clearInterval(intervalHandleRef.current); } }; }, [shouldPlay, prev, next, activeChildIntervalRef, interval, nextWhenVisible, isRTL]); const indicatorOnClicks = useMemo(() => indicators && Array.from({ length: numChildren }, (_, index) => event => { onSelect == null ? void 0 : onSelect(index, event); }), [indicators, numChildren, onSelect]); return /*#__PURE__*/_jsxs(Component, { ref: elementRef, ...props, onKeyDown: handleKeyDown, onMouseOver: handleMouseOver, onMouseOut: handleMouseOut, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, className: classNames(className, prefix, slide && 'slide', fade && `${prefix}-fade`, variant && `${prefix}-${variant}`), children: [indicators && /*#__PURE__*/_jsx("div", { className: `${prefix}-indicators`, children: map(children, (_, index) => /*#__PURE__*/_jsx("button", { type: "button", "data-bs-target": "" // Bootstrap requires this in their css. , "aria-label": indicatorLabels != null && indicatorLabels.length ? indicatorLabels[index] : `Slide ${index + 1}`, className: index === renderedActiveIndex ? 'active' : undefined, onClick: indicatorOnClicks ? indicatorOnClicks[index] : undefined, "aria-current": index === renderedActiveIndex }, index)) }), /*#__PURE__*/_jsx("div", { className: `${prefix}-inner`, children: map(children, (child, index) => { const isActive = index === renderedActiveIndex; return slide ? /*#__PURE__*/_jsx(TransitionWrapper, { in: isActive, onEnter: isActive ? handleEnter : undefined, onEntered: isActive ? handleEntered : undefined, addEndListener: transitionEndListener, children: (status, innerProps) => /*#__PURE__*/React.cloneElement(child, { ...innerProps, className: classNames(child.props.className, isActive && status !== 'entered' && orderClassName, (status === 'entered' || status === 'exiting') && 'active', (status === 'entering' || status === 'exiting') && directionalClassName) }) }) : /*#__PURE__*/React.cloneElement(child, { className: classNames(child.props.className, isActive && 'active') }); }) }), controls && /*#__PURE__*/_jsxs(_Fragment, { children: [(wrap || activeIndex !== 0) && /*#__PURE__*/_jsxs(Anchor, { className: `${prefix}-control-prev`, onClick: prev, children: [prevIcon, prevLabel && /*#__PURE__*/_jsx("span", { className: "visually-hidden", children: prevLabel })] }), (wrap || activeIndex !== numChildren - 1) && /*#__PURE__*/_jsxs(Anchor, { className: `${prefix}-control-next`, onClick: next, children: [nextIcon, nextLabel && /*#__PURE__*/_jsx("span", { className: "visually-hidden", children: nextLabel })] })] })] }); }); Carousel.displayName = 'Carousel'; Carousel.defaultProps = defaultProps; export default Object.assign(Carousel, { Caption: CarouselCaption, Item: CarouselItem });