You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

370 lines
12 KiB

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 <Transition> 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
});