UNPKG

11.9 kBJavaScriptView Raw
1"use client";
2
3import useEventCallback from '@restart/hooks/useEventCallback';
4import useUpdateEffect from '@restart/hooks/useUpdateEffect';
5import useCommittedRef from '@restart/hooks/useCommittedRef';
6import useTimeout from '@restart/hooks/useTimeout';
7import Anchor from '@restart/ui/Anchor';
8import classNames from 'classnames';
9import * as React from 'react';
10import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
11import { useUncontrolled } from 'uncontrollable';
12import CarouselCaption from './CarouselCaption';
13import CarouselItem from './CarouselItem';
14import { map, forEach } from './ElementChildren';
15import { useBootstrapPrefix, useIsRTL } from './ThemeProvider';
16import transitionEndListener from './transitionEndListener';
17import triggerBrowserReflow from './triggerBrowserReflow';
18import TransitionWrapper from './TransitionWrapper';
19import { jsx as _jsx } from "react/jsx-runtime";
20import { jsxs as _jsxs } from "react/jsx-runtime";
21import { Fragment as _Fragment } from "react/jsx-runtime";
22const SWIPE_THRESHOLD = 40;
23function isVisible(element) {
24 if (!element || !element.style || !element.parentNode || !element.parentNode.style) {
25 return false;
26 }
27 const elementStyle = getComputedStyle(element);
28 return elementStyle.display !== 'none' && elementStyle.visibility !== 'hidden' && getComputedStyle(element.parentNode).display !== 'none';
29}
30const Carousel = /*#__PURE__*/React.forwardRef(({
31 defaultActiveIndex = 0,
32 ...uncontrolledProps
33}, ref) => {
34 const {
35 // Need to define the default "as" during prop destructuring to be compatible with styled-components github.com/react-bootstrap/react-bootstrap/issues/3595
36 as: Component = 'div',
37 bsPrefix,
38 slide = true,
39 fade = false,
40 controls = true,
41 indicators = true,
42 indicatorLabels = [],
43 activeIndex,
44 onSelect,
45 onSlide,
46 onSlid,
47 interval = 5000,
48 keyboard = true,
49 onKeyDown,
50 pause = 'hover',
51 onMouseOver,
52 onMouseOut,
53 wrap = true,
54 touch = true,
55 onTouchStart,
56 onTouchMove,
57 onTouchEnd,
58 prevIcon = /*#__PURE__*/_jsx("span", {
59 "aria-hidden": "true",
60 className: "carousel-control-prev-icon"
61 }),
62 prevLabel = 'Previous',
63 nextIcon = /*#__PURE__*/_jsx("span", {
64 "aria-hidden": "true",
65 className: "carousel-control-next-icon"
66 }),
67 nextLabel = 'Next',
68 variant,
69 className,
70 children,
71 ...props
72 } = useUncontrolled({
73 defaultActiveIndex,
74 ...uncontrolledProps
75 }, {
76 activeIndex: 'onSelect'
77 });
78 const prefix = useBootstrapPrefix(bsPrefix, 'carousel');
79 const isRTL = useIsRTL();
80 const nextDirectionRef = useRef(null);
81 const [direction, setDirection] = useState('next');
82 const [paused, setPaused] = useState(false);
83 const [isSliding, setIsSliding] = useState(false);
84 const [renderedActiveIndex, setRenderedActiveIndex] = useState(activeIndex || 0);
85 useEffect(() => {
86 if (!isSliding && activeIndex !== renderedActiveIndex) {
87 if (nextDirectionRef.current) {
88 setDirection(nextDirectionRef.current);
89 } else {
90 setDirection((activeIndex || 0) > renderedActiveIndex ? 'next' : 'prev');
91 }
92 if (slide) {
93 setIsSliding(true);
94 }
95 setRenderedActiveIndex(activeIndex || 0);
96 }
97 }, [activeIndex, isSliding, renderedActiveIndex, slide]);
98 useEffect(() => {
99 if (nextDirectionRef.current) {
100 nextDirectionRef.current = null;
101 }
102 });
103 let numChildren = 0;
104 let activeChildInterval;
105
106 // Iterate to grab all of the children's interval values
107 // (and count them, too)
108 forEach(children, (child, index) => {
109 ++numChildren;
110 if (index === activeIndex) {
111 activeChildInterval = child.props.interval;
112 }
113 });
114 const activeChildIntervalRef = useCommittedRef(activeChildInterval);
115 const prev = useCallback(event => {
116 if (isSliding) {
117 return;
118 }
119 let nextActiveIndex = renderedActiveIndex - 1;
120 if (nextActiveIndex < 0) {
121 if (!wrap) {
122 return;
123 }
124 nextActiveIndex = numChildren - 1;
125 }
126 nextDirectionRef.current = 'prev';
127 onSelect == null ? void 0 : onSelect(nextActiveIndex, event);
128 }, [isSliding, renderedActiveIndex, onSelect, wrap, numChildren]);
129
130 // This is used in the setInterval, so it should not invalidate.
131 const next = useEventCallback(event => {
132 if (isSliding) {
133 return;
134 }
135 let nextActiveIndex = renderedActiveIndex + 1;
136 if (nextActiveIndex >= numChildren) {
137 if (!wrap) {
138 return;
139 }
140 nextActiveIndex = 0;
141 }
142 nextDirectionRef.current = 'next';
143 onSelect == null ? void 0 : onSelect(nextActiveIndex, event);
144 });
145 const elementRef = useRef();
146 useImperativeHandle(ref, () => ({
147 element: elementRef.current,
148 prev,
149 next
150 }));
151
152 // This is used in the setInterval, so it should not invalidate.
153 const nextWhenVisible = useEventCallback(() => {
154 if (!document.hidden && isVisible(elementRef.current)) {
155 if (isRTL) {
156 prev();
157 } else {
158 next();
159 }
160 }
161 });
162 const slideDirection = direction === 'next' ? 'start' : 'end';
163 useUpdateEffect(() => {
164 if (slide) {
165 // These callbacks will be handled by the <Transition> callbacks.
166 return;
167 }
168 onSlide == null ? void 0 : onSlide(renderedActiveIndex, slideDirection);
169 onSlid == null ? void 0 : onSlid(renderedActiveIndex, slideDirection);
170 }, [renderedActiveIndex]);
171 const orderClassName = `${prefix}-item-${direction}`;
172 const directionalClassName = `${prefix}-item-${slideDirection}`;
173 const handleEnter = useCallback(node => {
174 triggerBrowserReflow(node);
175 onSlide == null ? void 0 : onSlide(renderedActiveIndex, slideDirection);
176 }, [onSlide, renderedActiveIndex, slideDirection]);
177 const handleEntered = useCallback(() => {
178 setIsSliding(false);
179 onSlid == null ? void 0 : onSlid(renderedActiveIndex, slideDirection);
180 }, [onSlid, renderedActiveIndex, slideDirection]);
181 const handleKeyDown = useCallback(event => {
182 if (keyboard && !/input|textarea/i.test(event.target.tagName)) {
183 switch (event.key) {
184 case 'ArrowLeft':
185 event.preventDefault();
186 if (isRTL) {
187 next(event);
188 } else {
189 prev(event);
190 }
191 return;
192 case 'ArrowRight':
193 event.preventDefault();
194 if (isRTL) {
195 prev(event);
196 } else {
197 next(event);
198 }
199 return;
200 default:
201 }
202 }
203 onKeyDown == null ? void 0 : onKeyDown(event);
204 }, [keyboard, onKeyDown, prev, next, isRTL]);
205 const handleMouseOver = useCallback(event => {
206 if (pause === 'hover') {
207 setPaused(true);
208 }
209 onMouseOver == null ? void 0 : onMouseOver(event);
210 }, [pause, onMouseOver]);
211 const handleMouseOut = useCallback(event => {
212 setPaused(false);
213 onMouseOut == null ? void 0 : onMouseOut(event);
214 }, [onMouseOut]);
215 const touchStartXRef = useRef(0);
216 const touchDeltaXRef = useRef(0);
217 const touchUnpauseTimeout = useTimeout();
218 const handleTouchStart = useCallback(event => {
219 touchStartXRef.current = event.touches[0].clientX;
220 touchDeltaXRef.current = 0;
221 if (pause === 'hover') {
222 setPaused(true);
223 }
224 onTouchStart == null ? void 0 : onTouchStart(event);
225 }, [pause, onTouchStart]);
226 const handleTouchMove = useCallback(event => {
227 if (event.touches && event.touches.length > 1) {
228 touchDeltaXRef.current = 0;
229 } else {
230 touchDeltaXRef.current = event.touches[0].clientX - touchStartXRef.current;
231 }
232 onTouchMove == null ? void 0 : onTouchMove(event);
233 }, [onTouchMove]);
234 const handleTouchEnd = useCallback(event => {
235 if (touch) {
236 const touchDeltaX = touchDeltaXRef.current;
237 if (Math.abs(touchDeltaX) > SWIPE_THRESHOLD) {
238 if (touchDeltaX > 0) {
239 prev(event);
240 } else {
241 next(event);
242 }
243 }
244 }
245 if (pause === 'hover') {
246 touchUnpauseTimeout.set(() => {
247 setPaused(false);
248 }, interval || undefined);
249 }
250 onTouchEnd == null ? void 0 : onTouchEnd(event);
251 }, [touch, pause, prev, next, touchUnpauseTimeout, interval, onTouchEnd]);
252 const shouldPlay = interval != null && !paused && !isSliding;
253 const intervalHandleRef = useRef();
254 useEffect(() => {
255 var _ref, _activeChildIntervalR;
256 if (!shouldPlay) {
257 return undefined;
258 }
259 const nextFunc = isRTL ? prev : next;
260 intervalHandleRef.current = window.setInterval(document.visibilityState ? nextWhenVisible : nextFunc, (_ref = (_activeChildIntervalR = activeChildIntervalRef.current) != null ? _activeChildIntervalR : interval) != null ? _ref : undefined);
261 return () => {
262 if (intervalHandleRef.current !== null) {
263 clearInterval(intervalHandleRef.current);
264 }
265 };
266 }, [shouldPlay, prev, next, activeChildIntervalRef, interval, nextWhenVisible, isRTL]);
267 const indicatorOnClicks = useMemo(() => indicators && Array.from({
268 length: numChildren
269 }, (_, index) => event => {
270 onSelect == null ? void 0 : onSelect(index, event);
271 }), [indicators, numChildren, onSelect]);
272 return /*#__PURE__*/_jsxs(Component, {
273 ref: elementRef,
274 ...props,
275 onKeyDown: handleKeyDown,
276 onMouseOver: handleMouseOver,
277 onMouseOut: handleMouseOut,
278 onTouchStart: handleTouchStart,
279 onTouchMove: handleTouchMove,
280 onTouchEnd: handleTouchEnd,
281 className: classNames(className, prefix, slide && 'slide', fade && `${prefix}-fade`, variant && `${prefix}-${variant}`),
282 children: [indicators && /*#__PURE__*/_jsx("div", {
283 className: `${prefix}-indicators`,
284 children: map(children, (_, index) => /*#__PURE__*/_jsx("button", {
285 type: "button",
286 "data-bs-target": "" // Bootstrap requires this in their css.
287 ,
288 "aria-label": indicatorLabels != null && indicatorLabels.length ? indicatorLabels[index] : `Slide ${index + 1}`,
289 className: index === renderedActiveIndex ? 'active' : undefined,
290 onClick: indicatorOnClicks ? indicatorOnClicks[index] : undefined,
291 "aria-current": index === renderedActiveIndex
292 }, index))
293 }), /*#__PURE__*/_jsx("div", {
294 className: `${prefix}-inner`,
295 children: map(children, (child, index) => {
296 const isActive = index === renderedActiveIndex;
297 return slide ? /*#__PURE__*/_jsx(TransitionWrapper, {
298 in: isActive,
299 onEnter: isActive ? handleEnter : undefined,
300 onEntered: isActive ? handleEntered : undefined,
301 addEndListener: transitionEndListener,
302 children: (status, innerProps) => /*#__PURE__*/React.cloneElement(child, {
303 ...innerProps,
304 className: classNames(child.props.className, isActive && status !== 'entered' && orderClassName, (status === 'entered' || status === 'exiting') && 'active', (status === 'entering' || status === 'exiting') && directionalClassName)
305 })
306 }) : /*#__PURE__*/React.cloneElement(child, {
307 className: classNames(child.props.className, isActive && 'active')
308 });
309 })
310 }), controls && /*#__PURE__*/_jsxs(_Fragment, {
311 children: [(wrap || activeIndex !== 0) && /*#__PURE__*/_jsxs(Anchor, {
312 className: `${prefix}-control-prev`,
313 onClick: prev,
314 children: [prevIcon, prevLabel && /*#__PURE__*/_jsx("span", {
315 className: "visually-hidden",
316 children: prevLabel
317 })]
318 }), (wrap || activeIndex !== numChildren - 1) && /*#__PURE__*/_jsxs(Anchor, {
319 className: `${prefix}-control-next`,
320 onClick: next,
321 children: [nextIcon, nextLabel && /*#__PURE__*/_jsx("span", {
322 className: "visually-hidden",
323 children: nextLabel
324 })]
325 })]
326 })]
327 });
328});
329Carousel.displayName = 'Carousel';
330export default Object.assign(Carousel, {
331 Caption: CarouselCaption,
332 Item: CarouselItem
333});
\No newline at end of file