UNPKG

15.5 kBJavaScriptView Raw
1import _extends from "@babel/runtime/helpers/esm/extends";
2import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
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 classNames from 'classnames';
8import transitionEnd from 'dom-helpers/transitionEnd';
9import Transition from 'react-transition-group/Transition';
10import PropTypes from 'prop-types';
11import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
12import { useUncontrolled } from 'uncontrollable';
13import CarouselCaption from './CarouselCaption';
14import CarouselItem from './CarouselItem';
15import { map, forEach } from './ElementChildren';
16import SafeAnchor from './SafeAnchor';
17import { useBootstrapPrefix } from './ThemeProvider';
18import triggerBrowserReflow from './triggerBrowserReflow';
19var SWIPE_THRESHOLD = 40;
20var propTypes = {
21 /**
22 * @default 'carousel'
23 */
24 bsPrefix: PropTypes.string,
25 as: PropTypes.elementType,
26
27 /**
28 * Enables animation on the Carousel as it transitions between slides.
29 */
30 slide: PropTypes.bool,
31
32 /** Cross fade slides instead of the default slide animation */
33 fade: PropTypes.bool,
34
35 /**
36 * Show the Carousel previous and next arrows for changing the current slide
37 */
38 controls: PropTypes.bool,
39
40 /**
41 * Show a set of slide position indicators
42 */
43 indicators: PropTypes.bool,
44
45 /**
46 * Controls the current visible slide
47 *
48 * @controllable onSelect
49 */
50 activeIndex: PropTypes.number,
51
52 /**
53 * Callback fired when the active item changes.
54 *
55 * ```js
56 * (eventKey: number, event: Object | null) => void
57 * ```
58 *
59 * @controllable activeIndex
60 */
61 onSelect: PropTypes.func,
62
63 /**
64 * Callback fired when a slide transition starts.
65 *
66 * ```js
67 * (eventKey: number, direction: 'left' | 'right') => void
68 */
69 onSlide: PropTypes.func,
70
71 /**
72 * Callback fired when a slide transition ends.
73 *
74 * ```js
75 * (eventKey: number, direction: 'left' | 'right') => void
76 */
77 onSlid: PropTypes.func,
78
79 /**
80 * The amount of time to delay between automatically cycling an item. If `null`, carousel will not automatically cycle.
81 */
82 interval: PropTypes.number,
83
84 /** Whether the carousel should react to keyboard events. */
85 keyboard: PropTypes.bool,
86
87 /**
88 * If set to `"hover"`, pauses the cycling of the carousel on `mouseenter` and resumes the cycling of the carousel on `mouseleave`. If set to `false`, hovering over the carousel won't pause it.
89 *
90 * On touch-enabled devices, when set to `"hover"`, cycling will pause on `touchend` (once the user finished interacting with the carousel) for two intervals, before automatically resuming. Note that this is in addition to the above mouse behavior.
91 */
92 pause: PropTypes.oneOf(['hover', false]),
93
94 /** Whether the carousel should cycle continuously or have hard stops. */
95 wrap: PropTypes.bool,
96
97 /**
98 * Whether the carousel should support left/right swipe interactions on touchscreen devices.
99 */
100 touch: PropTypes.bool,
101
102 /** Override the default button icon for the "previous" control */
103 prevIcon: PropTypes.node,
104
105 /**
106 * Label shown to screen readers only, can be used to show the previous element
107 * in the carousel.
108 * Set to null to deactivate.
109 */
110 prevLabel: PropTypes.string,
111
112 /** Override the default button icon for the "next" control */
113 nextIcon: PropTypes.node,
114
115 /**
116 * Label shown to screen readers only, can be used to show the next element
117 * in the carousel.
118 * Set to null to deactivate.
119 */
120 nextLabel: PropTypes.string
121};
122var defaultProps = {
123 slide: true,
124 fade: false,
125 controls: true,
126 indicators: true,
127 defaultActiveIndex: 0,
128 interval: 5000,
129 keyboard: true,
130 pause: 'hover',
131 wrap: true,
132 touch: true,
133 prevIcon: /*#__PURE__*/React.createElement("span", {
134 "aria-hidden": "true",
135 className: "carousel-control-prev-icon"
136 }),
137 prevLabel: 'Previous',
138 nextIcon: /*#__PURE__*/React.createElement("span", {
139 "aria-hidden": "true",
140 className: "carousel-control-next-icon"
141 }),
142 nextLabel: 'Next'
143};
144
145function isVisible(element) {
146 if (!element || !element.style || !element.parentNode || !element.parentNode.style) {
147 return false;
148 }
149
150 var elementStyle = getComputedStyle(element);
151 return elementStyle.display !== 'none' && elementStyle.visibility !== 'hidden' && getComputedStyle(element.parentNode).display !== 'none';
152}
153
154function CarouselFunc(uncontrolledProps, ref) {
155 var _useUncontrolled = useUncontrolled(uncontrolledProps, {
156 activeIndex: 'onSelect'
157 }),
158 _useUncontrolled$as = _useUncontrolled.as,
159 Component = _useUncontrolled$as === void 0 ? 'div' : _useUncontrolled$as,
160 bsPrefix = _useUncontrolled.bsPrefix,
161 slide = _useUncontrolled.slide,
162 fade = _useUncontrolled.fade,
163 controls = _useUncontrolled.controls,
164 indicators = _useUncontrolled.indicators,
165 activeIndex = _useUncontrolled.activeIndex,
166 onSelect = _useUncontrolled.onSelect,
167 onSlide = _useUncontrolled.onSlide,
168 onSlid = _useUncontrolled.onSlid,
169 interval = _useUncontrolled.interval,
170 keyboard = _useUncontrolled.keyboard,
171 onKeyDown = _useUncontrolled.onKeyDown,
172 pause = _useUncontrolled.pause,
173 onMouseOver = _useUncontrolled.onMouseOver,
174 onMouseOut = _useUncontrolled.onMouseOut,
175 wrap = _useUncontrolled.wrap,
176 touch = _useUncontrolled.touch,
177 onTouchStart = _useUncontrolled.onTouchStart,
178 onTouchMove = _useUncontrolled.onTouchMove,
179 onTouchEnd = _useUncontrolled.onTouchEnd,
180 prevIcon = _useUncontrolled.prevIcon,
181 prevLabel = _useUncontrolled.prevLabel,
182 nextIcon = _useUncontrolled.nextIcon,
183 nextLabel = _useUncontrolled.nextLabel,
184 className = _useUncontrolled.className,
185 children = _useUncontrolled.children,
186 props = _objectWithoutPropertiesLoose(_useUncontrolled, ["as", "bsPrefix", "slide", "fade", "controls", "indicators", "activeIndex", "onSelect", "onSlide", "onSlid", "interval", "keyboard", "onKeyDown", "pause", "onMouseOver", "onMouseOut", "wrap", "touch", "onTouchStart", "onTouchMove", "onTouchEnd", "prevIcon", "prevLabel", "nextIcon", "nextLabel", "className", "children"]);
187
188 var prefix = useBootstrapPrefix(bsPrefix, 'carousel');
189 var nextDirectionRef = useRef(null);
190
191 var _useState = useState('next'),
192 direction = _useState[0],
193 setDirection = _useState[1];
194
195 var _useState2 = useState(false),
196 paused = _useState2[0],
197 setPaused = _useState2[1];
198
199 var _useState3 = useState(false),
200 isSliding = _useState3[0],
201 setIsSliding = _useState3[1];
202
203 var _useState4 = useState(activeIndex || 0),
204 renderedActiveIndex = _useState4[0],
205 setRenderedActiveIndex = _useState4[1];
206
207 if (!isSliding && activeIndex !== renderedActiveIndex) {
208 if (nextDirectionRef.current) {
209 setDirection(nextDirectionRef.current);
210 } else {
211 setDirection((activeIndex || 0) > renderedActiveIndex ? 'next' : 'prev');
212 }
213
214 if (slide) {
215 setIsSliding(true);
216 }
217
218 setRenderedActiveIndex(activeIndex || 0);
219 }
220
221 useEffect(function () {
222 if (nextDirectionRef.current) {
223 nextDirectionRef.current = null;
224 }
225 });
226 var numChildren = 0;
227 var activeChildInterval; // Iterate to grab all of the children's interval values
228 // (and count them, too)
229
230 forEach(children, function (child, index) {
231 ++numChildren;
232
233 if (index === activeIndex) {
234 activeChildInterval = child.props.interval;
235 }
236 });
237 var activeChildIntervalRef = useCommittedRef(activeChildInterval);
238 var prev = useCallback(function (event) {
239 if (isSliding) {
240 return;
241 }
242
243 var nextActiveIndex = renderedActiveIndex - 1;
244
245 if (nextActiveIndex < 0) {
246 if (!wrap) {
247 return;
248 }
249
250 nextActiveIndex = numChildren - 1;
251 }
252
253 nextDirectionRef.current = 'prev';
254
255 if (onSelect) {
256 onSelect(nextActiveIndex, event);
257 }
258 }, [isSliding, renderedActiveIndex, onSelect, wrap, numChildren]); // This is used in the setInterval, so it should not invalidate.
259
260 var next = useEventCallback(function (event) {
261 if (isSliding) {
262 return;
263 }
264
265 var nextActiveIndex = renderedActiveIndex + 1;
266
267 if (nextActiveIndex >= numChildren) {
268 if (!wrap) {
269 return;
270 }
271
272 nextActiveIndex = 0;
273 }
274
275 nextDirectionRef.current = 'next';
276
277 if (onSelect) {
278 onSelect(nextActiveIndex, event);
279 }
280 });
281 var elementRef = useRef();
282 useImperativeHandle(ref, function () {
283 return {
284 element: elementRef.current,
285 prev: prev,
286 next: next
287 };
288 }); // This is used in the setInterval, so it should not invalidate.
289
290 var nextWhenVisible = useEventCallback(function () {
291 if (!document.hidden && isVisible(elementRef.current)) {
292 next();
293 }
294 });
295 var slideDirection = direction === 'next' ? 'left' : 'right';
296 useUpdateEffect(function () {
297 if (slide) {
298 // These callbacks will be handled by the <Transition> callbacks.
299 return;
300 }
301
302 if (onSlide) {
303 onSlide(renderedActiveIndex, slideDirection);
304 }
305
306 if (onSlid) {
307 onSlid(renderedActiveIndex, slideDirection);
308 }
309 }, [renderedActiveIndex]);
310 var orderClassName = prefix + "-item-" + direction;
311 var directionalClassName = prefix + "-item-" + slideDirection;
312 var handleEnter = useCallback(function (node) {
313 triggerBrowserReflow(node);
314
315 if (onSlide) {
316 onSlide(renderedActiveIndex, slideDirection);
317 }
318 }, [onSlide, renderedActiveIndex, slideDirection]);
319 var handleEntered = useCallback(function () {
320 setIsSliding(false);
321
322 if (onSlid) {
323 onSlid(renderedActiveIndex, slideDirection);
324 }
325 }, [onSlid, renderedActiveIndex, slideDirection]);
326 var handleKeyDown = useCallback(function (event) {
327 if (keyboard && !/input|textarea/i.test(event.target.tagName)) {
328 switch (event.key) {
329 case 'ArrowLeft':
330 event.preventDefault();
331 prev(event);
332 return;
333
334 case 'ArrowRight':
335 event.preventDefault();
336 next(event);
337 return;
338
339 default:
340 }
341 }
342
343 if (onKeyDown) {
344 onKeyDown(event);
345 }
346 }, [keyboard, onKeyDown, prev, next]);
347 var handleMouseOver = useCallback(function (event) {
348 if (pause === 'hover') {
349 setPaused(true);
350 }
351
352 if (onMouseOver) {
353 onMouseOver(event);
354 }
355 }, [pause, onMouseOver]);
356 var handleMouseOut = useCallback(function (event) {
357 setPaused(false);
358
359 if (onMouseOut) {
360 onMouseOut(event);
361 }
362 }, [onMouseOut]);
363 var touchStartXRef = useRef(0);
364 var touchDeltaXRef = useRef(0);
365 var touchUnpauseTimeout = useTimeout();
366 var handleTouchStart = useCallback(function (event) {
367 touchStartXRef.current = event.touches[0].clientX;
368 touchDeltaXRef.current = 0;
369
370 if (pause === 'hover') {
371 setPaused(true);
372 }
373
374 if (onTouchStart) {
375 onTouchStart(event);
376 }
377 }, [pause, onTouchStart]);
378 var handleTouchMove = useCallback(function (event) {
379 if (event.touches && event.touches.length > 1) {
380 touchDeltaXRef.current = 0;
381 } else {
382 touchDeltaXRef.current = event.touches[0].clientX - touchStartXRef.current;
383 }
384
385 if (onTouchMove) {
386 onTouchMove(event);
387 }
388 }, [onTouchMove]);
389 var handleTouchEnd = useCallback(function (event) {
390 if (touch) {
391 var touchDeltaX = touchDeltaXRef.current;
392
393 if (Math.abs(touchDeltaX) > SWIPE_THRESHOLD) {
394 if (touchDeltaX > 0) {
395 prev(event);
396 } else {
397 next(event);
398 }
399 }
400 }
401
402 if (pause === 'hover') {
403 touchUnpauseTimeout.set(function () {
404 setPaused(false);
405 }, interval || undefined);
406 }
407
408 if (onTouchEnd) {
409 onTouchEnd(event);
410 }
411 }, [touch, pause, prev, next, touchUnpauseTimeout, interval, onTouchEnd]);
412 var shouldPlay = interval != null && !paused && !isSliding;
413 var intervalHandleRef = useRef();
414 useEffect(function () {
415 var _ref, _activeChildIntervalR;
416
417 if (!shouldPlay) {
418 return undefined;
419 }
420
421 intervalHandleRef.current = window.setInterval(document.visibilityState ? nextWhenVisible : next, (_ref = (_activeChildIntervalR = activeChildIntervalRef.current) != null ? _activeChildIntervalR : interval) != null ? _ref : undefined);
422 return function () {
423 if (intervalHandleRef.current !== null) {
424 clearInterval(intervalHandleRef.current);
425 }
426 };
427 }, [shouldPlay, next, activeChildIntervalRef, interval, nextWhenVisible]);
428 var indicatorOnClicks = useMemo(function () {
429 return indicators && Array.from({
430 length: numChildren
431 }, function (_, index) {
432 return function (event) {
433 if (onSelect) {
434 onSelect(index, event);
435 }
436 };
437 });
438 }, [indicators, numChildren, onSelect]);
439 return /*#__PURE__*/React.createElement(Component, _extends({
440 ref: elementRef
441 }, props, {
442 onKeyDown: handleKeyDown,
443 onMouseOver: handleMouseOver,
444 onMouseOut: handleMouseOut,
445 onTouchStart: handleTouchStart,
446 onTouchMove: handleTouchMove,
447 onTouchEnd: handleTouchEnd,
448 className: classNames(className, prefix, slide && 'slide', fade && prefix + "-fade")
449 }), indicators && /*#__PURE__*/React.createElement("ol", {
450 className: prefix + "-indicators"
451 }, map(children, function (_child, index) {
452 return /*#__PURE__*/React.createElement("li", {
453 key: index,
454 className: index === renderedActiveIndex ? 'active' : undefined,
455 onClick: indicatorOnClicks ? indicatorOnClicks[index] : undefined
456 });
457 })), /*#__PURE__*/React.createElement("div", {
458 className: prefix + "-inner"
459 }, map(children, function (child, index) {
460 var isActive = index === renderedActiveIndex;
461 return slide ? /*#__PURE__*/React.createElement(Transition, {
462 in: isActive,
463 onEnter: isActive ? handleEnter : undefined,
464 onEntered: isActive ? handleEntered : undefined,
465 addEndListener: transitionEnd
466 }, function (status) {
467 return /*#__PURE__*/React.cloneElement(child, {
468 className: classNames(child.props.className, isActive && status !== 'entered' && orderClassName, (status === 'entered' || status === 'exiting') && 'active', (status === 'entering' || status === 'exiting') && directionalClassName)
469 });
470 }) : /*#__PURE__*/React.cloneElement(child, {
471 className: classNames(child.props.className, isActive && 'active')
472 });
473 })), controls && /*#__PURE__*/React.createElement(React.Fragment, null, (wrap || activeIndex !== 0) && /*#__PURE__*/React.createElement(SafeAnchor, {
474 className: prefix + "-control-prev",
475 onClick: prev
476 }, prevIcon, prevLabel && /*#__PURE__*/React.createElement("span", {
477 className: "sr-only"
478 }, prevLabel)), (wrap || activeIndex !== numChildren - 1) && /*#__PURE__*/React.createElement(SafeAnchor, {
479 className: prefix + "-control-next",
480 onClick: next
481 }, nextIcon, nextLabel && /*#__PURE__*/React.createElement("span", {
482 className: "sr-only"
483 }, nextLabel))));
484}
485
486var Carousel = /*#__PURE__*/React.forwardRef(CarouselFunc);
487Carousel.displayName = 'Carousel';
488Carousel.propTypes = propTypes;
489Carousel.defaultProps = defaultProps;
490Carousel.Caption = CarouselCaption;
491Carousel.Item = CarouselItem;
492export default Carousel;
\No newline at end of file