@telus-uds/components-base 1.82.0 → 1.84.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import React from 'react'
1
+ import React, { useState, useRef, useEffect, useCallback, useMemo, forwardRef } from 'react'
2
2
  import { View, Animated, PanResponder, StyleSheet, Platform, Dimensions } from 'react-native'
3
3
  import PropTypes from 'prop-types'
4
4
  import { useThemeTokens } from '../ThemeProvider'
@@ -26,6 +26,12 @@ import CarouselTabsPanel from './CarouselTabs/CarouselTabsPanel'
26
26
  import CarouselTabsPanelItem from './CarouselTabs/CarouselTabsPanelItem'
27
27
  import dictionary from './dictionary'
28
28
 
29
+ const TRANSITION_MODES = {
30
+ MANUAL: 'manual',
31
+ AUTOMATIC: 'automatic',
32
+ SWIPE: 'swipe'
33
+ }
34
+
29
35
  const staticStyles = StyleSheet.create({
30
36
  root: {
31
37
  backgroundColor: 'transparent',
@@ -34,6 +40,12 @@ const staticStyles = StyleSheet.create({
34
40
  position: 'relative',
35
41
  top: 0,
36
42
  left: 0
43
+ },
44
+ animationControlButton: {
45
+ position: 'absolute',
46
+ zIndex: 1,
47
+ right: Platform.OS === 'web' ? undefined : 40,
48
+ top: 40
37
49
  }
38
50
  })
39
51
 
@@ -49,6 +61,26 @@ const selectSwipeAreaStyles = (count, width) => ({
49
61
  flexDirection: 'row'
50
62
  })
51
63
 
64
+ const getDynamicPositionProperty = (areStylesAppliedOnPreviousButton) =>
65
+ areStylesAppliedOnPreviousButton ? 'left' : 'right'
66
+
67
+ const selectControlButtonPositionStyles = ({
68
+ positionVariant,
69
+ buttonWidth,
70
+ positionProperty = getDynamicPositionProperty(),
71
+ spaceBetweenSlideAndButton
72
+ }) => {
73
+ const styles = {}
74
+ if (positionVariant === 'edge') {
75
+ styles[positionProperty] = -1 * (buttonWidth / 2)
76
+ } else if (positionVariant === 'inside') {
77
+ styles[positionProperty] = 0
78
+ } else if (positionVariant === 'outside') {
79
+ styles[positionProperty] = -1 * (spaceBetweenSlideAndButton + buttonWidth)
80
+ }
81
+ return styles
82
+ }
83
+
52
84
  const selectPreviousNextNavigationButtonStyles = (
53
85
  previousNextNavigationButtonWidth,
54
86
  previousNextNavigationPosition,
@@ -61,7 +93,6 @@ const selectPreviousNextNavigationButtonStyles = (
61
93
  zIndex: 1,
62
94
  position: 'absolute'
63
95
  }
64
- const dynamicPositionProperty = areStylesAppliedOnPreviousButton ? 'left' : 'right'
65
96
  if (isFirstSlide) {
66
97
  styles.visibility = areStylesAppliedOnPreviousButton ? 'hidden' : 'visible'
67
98
  } else if (isLastSlide) {
@@ -70,15 +101,15 @@ const selectPreviousNextNavigationButtonStyles = (
70
101
  styles.visibility = 'visible'
71
102
  }
72
103
 
73
- if (previousNextNavigationPosition === 'edge') {
74
- styles[dynamicPositionProperty] = -1 * (previousNextNavigationButtonWidth / 2)
75
- } else if (previousNextNavigationPosition === 'inside') {
76
- styles[dynamicPositionProperty] = 0
77
- } else if (previousNextNavigationPosition === 'outside') {
78
- styles[dynamicPositionProperty] =
79
- -1 * (spaceBetweenSlideAndPreviousNextNavigation + previousNextNavigationButtonWidth)
104
+ return {
105
+ ...styles,
106
+ ...selectControlButtonPositionStyles({
107
+ positionVariant: previousNextNavigationPosition,
108
+ buttonWidth: previousNextNavigationButtonWidth,
109
+ positionProperty: getDynamicPositionProperty(areStylesAppliedOnPreviousButton),
110
+ spaceBetweenSlideAndButton: spaceBetweenSlideAndPreviousNextNavigation
111
+ })
80
112
  }
81
- return styles
82
113
  }
83
114
 
84
115
  const selectIconStyles = ({ iconBackgroundColor }) => ({ backgroundColor: iconBackgroundColor })
@@ -139,7 +170,7 @@ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, vie
139
170
  - `spaceBetweenSlideAndPreviousNextNavigation` - Horizontal space between slide and previous/next navigational buttons
140
171
  - `spaceBetweenSlideAndPanelNavigation` - Vertical space between slide area and panel navigation area
141
172
  */
142
- const Carousel = React.forwardRef(
173
+ const Carousel = forwardRef(
143
174
  (
144
175
  {
145
176
  tokens,
@@ -170,10 +201,21 @@ const Carousel = React.forwardRef(
170
201
  accessibilityLabel,
171
202
  accessibilityLiveRegion = 'polite',
172
203
  copy,
204
+ slideDuration = 0,
205
+ transitionDuration = 0,
206
+ autoPlay = false,
173
207
  ...rest
174
208
  },
175
209
  ref
176
210
  ) => {
211
+ let childrenArray = unpackFragment(children)
212
+ const autoPlayFeatureEnabled =
213
+ autoPlay && slideDuration > 0 && transitionDuration > 0 && childrenArray.length > 1
214
+ // if `Carousel` only has one `Carousel.Item`, convert this to a single-item array
215
+ if (!Array.isArray(childrenArray)) {
216
+ childrenArray = [childrenArray]
217
+ }
218
+ const getCopy = useCopy({ dictionary, copy })
177
219
  const viewport = useViewport()
178
220
  const themeTokens = useThemeTokens('Carousel', tokens, variant, {
179
221
  viewport
@@ -181,22 +223,52 @@ const Carousel = React.forwardRef(
181
223
  const {
182
224
  previousIcon,
183
225
  nextIcon,
226
+ playIcon,
227
+ pauseIcon,
184
228
  showPreviousNextNavigation,
185
229
  showPanelNavigation,
186
230
  showPanelTabs,
187
231
  spaceBetweenSlideAndPreviousNextNavigation
188
232
  } = themeTokens
189
- const [activeIndex, setActiveIndex] = React.useState(0)
233
+ const [activeIndex, setActiveIndex] = useState(0)
234
+ const activeIndexRef = useRef(activeIndex)
235
+ const { reduceMotionEnabled } = useA11yInfo()
236
+ const reduceMotionRef = useRef(reduceMotionEnabled)
237
+ const [containerLayout, setContainerLayout] = React.useState({
238
+ x: 0,
239
+ y: 0,
240
+ width: 0
241
+ })
242
+ const containerLayoutRef = useRef(containerLayout)
243
+
244
+ const [previousNextNavigationButtonWidth, setPreviousNextNavigationButtonWidth] = useState(0)
245
+ const firstFocusRef = useRef(null)
246
+ const pan = useRef(new Animated.ValueXY()).current
247
+ const animatedX = useRef(0)
248
+ const animatedY = useRef(0)
249
+ const [isAnimating, setIsAnimating] = useState(false)
250
+ /**
251
+ * While having the same starting point, `isAutoPlayEnabled` and `isCarouselPlaying` are different states
252
+ *
253
+ * `isAutoPlayEnabled` is a state to determine if the autoplay feature is enabled or disabled
254
+ * `isCarouselPlaying` is a state to determine if the carousel is currently playing or paused
255
+ */
256
+ const [isAutoPlayEnabled, setIsAutoPlayEnabled] = useState(autoPlayFeatureEnabled)
257
+ const [isCarouselPlaying, setisCarouselPlaying] = useState(autoPlayFeatureEnabled)
258
+ const isSwiping = useRef(false)
259
+ const autoPlayRef = useRef(null)
190
260
 
191
- const [isAnimating, setIsAnimating] = React.useState(false)
192
- const handleAnimationStart = React.useCallback(
261
+ const isFirstSlide = !activeIndex
262
+ const isLastSlide = activeIndex + 1 >= childrenArray.length
263
+
264
+ const handleAnimationStart = useCallback(
193
265
  (...args) => {
194
266
  if (typeof onAnimationStart === 'function') onAnimationStart(...args)
195
267
  setIsAnimating(true)
196
268
  },
197
269
  [onAnimationStart]
198
270
  )
199
- const handleAnimationEnd = React.useCallback(
271
+ const handleAnimationEnd = useCallback(
200
272
  (...args) => {
201
273
  if (typeof onAnimationEnd === 'function') onAnimationEnd(...args)
202
274
  setIsAnimating(false)
@@ -204,68 +276,30 @@ const Carousel = React.forwardRef(
204
276
  [onAnimationEnd]
205
277
  )
206
278
 
207
- const getCopy = useCopy({ dictionary, copy })
208
-
209
- let childrenArray = unpackFragment(children)
210
- // if `Carousel` only has one `Carousel.Item`, convert this to a single-item array
211
- if (!Array.isArray(childrenArray)) {
212
- childrenArray = [childrenArray]
213
- }
214
- const systemProps = selectProps({
215
- ...rest,
216
- accessibilityRole,
217
- accessibilityLabel,
218
- accessibilityValue: {
219
- min: 1,
220
- max: childrenArray.length,
221
- now: activeIndex + 1
222
- }
223
- })
224
- const { reduceMotionEnabled } = useA11yInfo()
225
- const [containerLayout, setContainerLayout] = React.useState({
226
- x: 0,
227
- y: 0,
228
- width: 0
229
- })
230
-
231
- const [previousNextNavigationButtonWidth, setPreviousNextNavigationButtonWidth] =
232
- React.useState(0)
233
- const firstFocusRef = React.useRef(null)
234
- const pan = React.useRef(new Animated.ValueXY()).current
235
- const animatedX = React.useRef(0)
236
- const animatedY = React.useRef(0)
237
- const isFirstSlide = !activeIndex
238
- const isLastSlide = activeIndex + 1 >= children.length
239
-
240
- const onContainerLayout = ({
241
- nativeEvent: {
242
- layout: { x, y, width }
243
- }
244
- }) => setContainerLayout((prevState) => ({ ...prevState, x, y, width }))
245
-
246
- const onPreviousNextNavigationButtonLayout = ({
247
- nativeEvent: {
248
- layout: { width }
249
- }
250
- }) => setPreviousNextNavigationButtonWidth(width)
251
-
252
- const updateOffset = React.useCallback(() => {
253
- animatedX.current = containerLayout.width * activeIndex * -1
279
+ const updateOffset = useCallback(() => {
280
+ animatedX.current = containerLayoutRef.current.width * activeIndexRef.current * -1
254
281
  animatedY.current = 0
255
282
  pan.setOffset({
256
283
  x: animatedX.current,
257
284
  y: animatedY.current
258
285
  })
259
286
  pan.setValue({ x: 0, y: 0 })
260
- }, [activeIndex, containerLayout.width, pan, animatedX])
287
+ }, [pan, animatedX])
261
288
 
262
- const animate = React.useCallback(
289
+ const animate = useCallback(
263
290
  (toValue, toIndex) => {
264
291
  const handleAnimationEndToIndex = (...args) => handleAnimationEnd(toIndex, ...args)
265
- if (reduceMotionEnabled) {
292
+ if (reduceMotionRef.current || isSwiping.current) {
266
293
  Animated.timing(pan, { toValue, duration: 1, useNativeDriver: false }).start(
267
294
  handleAnimationEndToIndex
268
295
  )
296
+ } else if (isAutoPlayEnabled) {
297
+ Animated.timing(pan, {
298
+ ...springConfig,
299
+ toValue,
300
+ useNativeDriver: false,
301
+ duration: transitionDuration * 1000
302
+ }).start(handleAnimationEndToIndex)
269
303
  } else {
270
304
  Animated.spring(pan, {
271
305
  ...springConfig,
@@ -274,60 +308,178 @@ const Carousel = React.forwardRef(
274
308
  }).start(handleAnimationEndToIndex)
275
309
  }
276
310
  },
277
- [pan, springConfig, reduceMotionEnabled, handleAnimationEnd]
311
+ [pan, springConfig, handleAnimationEnd, transitionDuration, isAutoPlayEnabled]
278
312
  )
279
313
 
280
- const updateIndex = React.useCallback(
281
- (delta = 1) => {
314
+ const stopAutoplay = useCallback(() => {
315
+ if (autoPlayRef?.current) {
316
+ clearTimeout(autoPlayRef?.current)
317
+ }
318
+ }, [])
319
+
320
+ const updateIndex = useCallback(
321
+ (delta = 1, transitionMode) => {
282
322
  const toValue = { x: 0, y: 0 }
283
323
  let skipChanges = !delta
284
324
  let calcDelta = delta
285
-
286
- if (activeIndex <= 0 && delta < 0) {
287
- skipChanges = true
288
- calcDelta = children.length + delta
289
- } else if (activeIndex + 1 >= children.length && delta > 0) {
290
- skipChanges = true
291
- calcDelta = -1 * activeIndex + delta - 1
325
+ if (activeIndexRef.current <= 0 && delta < 0) {
326
+ skipChanges = transitionMode !== TRANSITION_MODES.AUTOMATIC
327
+ calcDelta = childrenArray.length + delta
328
+ } else if (activeIndexRef.current + 1 >= childrenArray.length && delta > 0) {
329
+ skipChanges = transitionMode !== TRANSITION_MODES.AUTOMATIC
330
+ calcDelta = -1 * activeIndexRef.current + delta - 1
292
331
  }
293
332
 
294
- const index = activeIndex + calcDelta
295
-
333
+ const index = activeIndexRef.current + calcDelta
296
334
  if (skipChanges) {
297
335
  animate(toValue, index)
298
336
  return calcDelta
299
337
  }
300
338
 
339
+ stopAutoplay()
301
340
  setActiveIndex(index)
302
341
 
303
- toValue.x = containerLayout.width * -1 * calcDelta
304
-
342
+ toValue.x = containerLayoutRef.current.width * -1 * calcDelta
305
343
  animate(toValue, index)
306
-
344
+ if (isCarouselPlaying) {
345
+ stopAutoplay()
346
+ if (
347
+ index === 0 &&
348
+ activeIndexRef.current + 1 === childrenArray.length &&
349
+ transitionMode === TRANSITION_MODES.AUTOMATIC
350
+ ) {
351
+ setisCarouselPlaying(false)
352
+ } else if (isAutoPlayEnabled) {
353
+ autoPlayRef.current = setTimeout(() => {
354
+ updateOffset()
355
+ handleAnimationStart(activeIndexRef.current)
356
+ updateIndex(slideDuration < 0 ? -1 : 1, TRANSITION_MODES.AUTOMATIC)
357
+ if (refocus) firstFocusRef.current?.focus()
358
+ }, Math.abs(slideDuration) * 1000)
359
+ }
360
+ }
307
361
  if (onIndexChanged) onIndexChanged(calcDelta, index)
308
362
  return calcDelta
309
363
  },
310
- [containerLayout.width, activeIndex, animate, children.length, onIndexChanged]
364
+ [
365
+ handleAnimationStart,
366
+ refocus,
367
+ slideDuration,
368
+ updateOffset,
369
+ animate,
370
+ childrenArray.length,
371
+ onIndexChanged,
372
+ isCarouselPlaying,
373
+ stopAutoplay,
374
+ isAutoPlayEnabled
375
+ ]
311
376
  )
312
377
 
313
- const fixOffsetAndGo = React.useCallback(
314
- (delta) => {
378
+ const startAutoplay = useCallback(() => {
379
+ stopAutoplay()
380
+ if (isAutoPlayEnabled) {
381
+ autoPlayRef.current = setTimeout(() => {
382
+ updateOffset()
383
+ handleAnimationStart(activeIndexRef.current)
384
+ updateIndex(slideDuration < 0 ? -1 : 1, TRANSITION_MODES.AUTOMATIC)
385
+ if (refocus && Platform.OS === 'web') firstFocusRef.current?.focus()
386
+ }, Math.abs(slideDuration) * 1000)
387
+ }
388
+ }, [
389
+ handleAnimationStart,
390
+ refocus,
391
+ updateIndex,
392
+ updateOffset,
393
+ slideDuration,
394
+ stopAutoplay,
395
+ isAutoPlayEnabled
396
+ ])
397
+
398
+ const fixOffsetAndGo = useCallback(
399
+ (delta, transitionMode) => {
315
400
  updateOffset()
316
- handleAnimationStart(activeIndex)
317
- updateIndex(delta)
318
- if (refocus) firstFocusRef.current?.focus()
401
+ handleAnimationStart(activeIndexRef.current)
402
+ updateIndex(delta, transitionMode)
403
+ if (refocus && Platform.OS === 'web') firstFocusRef.current?.focus()
319
404
  },
320
- [updateIndex, updateOffset, activeIndex, handleAnimationStart, refocus]
405
+ [updateIndex, updateOffset, handleAnimationStart, refocus]
321
406
  )
322
407
 
323
- const goToNeighboring = React.useCallback(
324
- (toPrev = false) => {
325
- fixOffsetAndGo(toPrev ? -1 : 1)
408
+ const goToNeighboring = useCallback(
409
+ (toPrev = false, transitionMode = TRANSITION_MODES.MANUAL) => {
410
+ fixOffsetAndGo(toPrev ? -1 : 1, transitionMode)
326
411
  },
327
412
  [fixOffsetAndGo]
328
413
  )
329
414
 
330
- const isSwipeAllowed = React.useCallback(() => {
415
+ useEffect(() => {
416
+ activeIndexRef.current = activeIndex
417
+ }, [activeIndex])
418
+
419
+ useEffect(() => {
420
+ reduceMotionRef.current = reduceMotionEnabled
421
+ }, [reduceMotionEnabled])
422
+
423
+ useEffect(() => {
424
+ containerLayoutRef.current = containerLayout
425
+ }, [containerLayout])
426
+
427
+ useEffect(() => {
428
+ pan.x.addListener(({ value }) => {
429
+ animatedX.current = value
430
+ })
431
+ pan.y.addListener(({ value }) => {
432
+ animatedY.current = value
433
+ })
434
+ if (isCarouselPlaying) {
435
+ startAutoplay()
436
+ }
437
+ return () => {
438
+ stopAutoplay()
439
+ pan.x.removeAllListeners()
440
+ pan.y.removeAllListeners()
441
+ }
442
+ }, [pan.x, pan.y, startAutoplay, stopAutoplay, isCarouselPlaying])
443
+
444
+ useEffect(() => {
445
+ const subscription = Dimensions.addEventListener('change', () => {
446
+ updateOffset()
447
+ })
448
+
449
+ return () => {
450
+ if (subscription.remove) {
451
+ subscription.remove()
452
+ } else {
453
+ Dimensions.removeEventListener('change', updateOffset)
454
+ }
455
+ }
456
+ }, [updateOffset])
457
+
458
+ useEffect(() => {
459
+ setIsAutoPlayEnabled(
460
+ autoPlay && slideDuration > 0 && transitionDuration > 0 && childrenArray.length > 1
461
+ )
462
+ }, [autoPlay, slideDuration, transitionDuration, childrenArray.length])
463
+
464
+ useEffect(() => {
465
+ return () => {
466
+ stopAutoplay()
467
+ }
468
+ }, [stopAutoplay])
469
+
470
+ const onContainerLayout = ({
471
+ nativeEvent: {
472
+ layout: { x, y, width }
473
+ }
474
+ }) => setContainerLayout((prevState) => ({ ...prevState, x, y, width }))
475
+
476
+ const onPreviousNextNavigationButtonLayout = ({
477
+ nativeEvent: {
478
+ layout: { width }
479
+ }
480
+ }) => setPreviousNextNavigationButtonWidth(width)
481
+
482
+ const isSwipeAllowed = useCallback(() => {
331
483
  if (childrenArray.length === 1) {
332
484
  return false
333
485
  }
@@ -337,7 +489,7 @@ const Carousel = React.forwardRef(
337
489
  return true
338
490
  }, [viewport, childrenArray.length])
339
491
 
340
- const panResponder = React.useMemo(
492
+ const panResponder = useMemo(
341
493
  () =>
342
494
  PanResponder.create({
343
495
  onPanResponderTerminationRequest: () => false,
@@ -347,76 +499,70 @@ const Carousel = React.forwardRef(
347
499
  return false
348
500
  }
349
501
 
350
- handleAnimationStart(activeIndex)
502
+ handleAnimationStart(activeIndexRef.current)
351
503
 
352
- return Math.abs(gestureState.dx) > minDistanceToCapture
504
+ const allow = Math.abs(gestureState.dx) > minDistanceToCapture
505
+
506
+ if (allow) {
507
+ isSwiping.current = true
508
+ stopAutoplay()
509
+ }
510
+
511
+ return allow
512
+ },
513
+ onPanResponderGrant: () => {
514
+ updateOffset()
353
515
  },
354
- onPanResponderGrant: () => updateOffset(),
355
516
  onPanResponderMove: Animated.event([null, { dx: pan.x }], {
356
517
  useNativeDriver: false
357
518
  }),
358
519
  onPanResponderRelease: (_, gesture) => {
520
+ if (isCarouselPlaying) {
521
+ startAutoplay()
522
+ }
359
523
  const correction = gesture.moveX - gesture.x0
360
524
 
361
- if (Math.abs(correction) < containerLayout.width * minDistanceForAction) {
525
+ if (Math.abs(correction) < containerLayoutRef.current.width * minDistanceForAction) {
362
526
  animate({ x: 0, y: 0 }, 0)
363
527
  } else {
364
528
  const delta = correction > 0 ? -1 : 1
365
- updateIndex(delta)
529
+ updateIndex(delta, TRANSITION_MODES.SWIPE)
366
530
  }
531
+
532
+ isSwiping.current = false
367
533
  }
368
534
  }),
369
535
  [
370
- containerLayout.width,
371
536
  updateIndex,
372
537
  updateOffset,
373
538
  animate,
374
539
  isSwipeAllowed,
375
- activeIndex,
376
540
  minDistanceForAction,
377
541
  handleAnimationStart,
378
542
  minDistanceToCapture,
379
- pan.x
543
+ pan.x,
544
+ startAutoplay,
545
+ stopAutoplay,
546
+ isCarouselPlaying
380
547
  ]
381
548
  )
382
549
 
383
- React.useEffect(() => {
384
- pan.x.addListener(({ value }) => {
385
- animatedX.current = value
386
- })
387
- pan.y.addListener(({ value }) => {
388
- animatedY.current = value
389
- })
390
- return () => {
391
- pan.x.removeAllListeners()
392
- pan.y.removeAllListeners()
393
- }
394
- }, [pan.x, pan.y])
395
-
396
- React.useEffect(() => {
397
- const subscription = Dimensions.addEventListener('change', () => {
398
- updateOffset()
399
- })
400
-
401
- return () => subscription?.remove()
402
- })
403
-
404
- const goToNext = React.useCallback(() => {
550
+ const goToNext = useCallback(() => {
405
551
  goToNeighboring()
406
552
  }, [goToNeighboring])
407
553
 
408
- const goToPrev = React.useCallback(() => {
554
+ const goToPrev = useCallback(() => {
409
555
  goToNeighboring(true)
410
556
  }, [goToNeighboring])
411
557
 
412
- const goTo = React.useCallback(
558
+ const goTo = useCallback(
413
559
  (index = 0) => {
414
- const delta = index - activeIndex
560
+ const delta = index - activeIndexRef.current
415
561
  if (delta) {
416
- fixOffsetAndGo(delta)
562
+ fixOffsetAndGo(delta, TRANSITION_MODES.MANUAL)
417
563
  }
418
564
  },
419
- [fixOffsetAndGo, activeIndex]
565
+ [fixOffsetAndGo]
420
566
  )
421
567
 
422
568
  // @TODO: - these are Allium-theme variants and won't have any effect in themes that don't implement them.
@@ -428,7 +574,7 @@ const Carousel = React.forwardRef(
428
574
  inverse: variant?.inverse
429
575
  }
430
576
 
431
- const getCopyWithPlaceholders = React.useCallback(
577
+ const getCopyWithPlaceholders = useCallback(
432
578
  (copyKey) => {
433
579
  const copyText = getCopy(copyKey)
434
580
  .replace(/%\{title\}/g, title)
@@ -459,6 +605,18 @@ const Carousel = React.forwardRef(
459
605
  }
460
606
  }
461
607
  }
608
+
609
+ const systemProps = selectProps({
610
+ ...rest,
611
+ accessibilityRole,
612
+ accessibilityLabel,
613
+ accessibilityValue: {
614
+ min: 1,
615
+ max: childrenArray.length,
616
+ now: activeIndex + 1
617
+ }
618
+ })
619
+
462
620
  // If container isn't used for focus, give it a label of title if none is passed in,
463
621
  // otherwise read the current position on focus
464
622
  const containerAccessibilityLabel =
@@ -471,6 +629,15 @@ const Carousel = React.forwardRef(
471
629
  ...(isFirstFocusContainer && { ref: containerRef, focusable: true })
472
630
  }
473
631
 
632
+ const onAnimationControlButtonPress = useCallback(() => {
633
+ if (isCarouselPlaying) {
634
+ stopAutoplay()
635
+ } else {
636
+ startAutoplay()
637
+ }
638
+ setisCarouselPlaying((prevState) => !prevState)
639
+ }, [isCarouselPlaying, stopAutoplay, startAutoplay])
640
+
474
641
  return (
475
642
  <CarouselProvider
476
643
  activeIndex={activeIndex}
@@ -490,6 +657,25 @@ const Carousel = React.forwardRef(
490
657
  {...systemProps}
491
658
  {...containerProps}
492
659
  >
660
+ {isAutoPlayEnabled ? (
661
+ <View
662
+ style={[
663
+ staticStyles.animationControlButton,
664
+ selectControlButtonPositionStyles({
665
+ positionVariant: previousNextNavigationPosition,
666
+ buttonWidth: previousNextNavigationButtonWidth,
667
+ positionProperty: getDynamicPositionProperty(),
668
+ spaceBetweenSlideAndButton: spaceBetweenSlideAndPreviousNextNavigation
669
+ })
670
+ ]}
671
+ >
672
+ <IconButton
673
+ icon={isCarouselPlaying ? pauseIcon : playIcon}
674
+ variant={previousNextIconButtonVariants}
675
+ onPress={onAnimationControlButtonPress}
676
+ />
677
+ </View>
678
+ ) : null}
493
679
  {showPreviousNextNavigation && childrenArray.length > 1 ? (
494
680
  <View
495
681
  style={selectPreviousNextNavigationButtonStyles(
@@ -531,7 +717,7 @@ const Carousel = React.forwardRef(
531
717
  <View style={selectContainerStyles(containerLayout.width)}>
532
718
  <Animated.View
533
719
  style={StyleSheet.flatten([
534
- selectSwipeAreaStyles(children.length, containerLayout.width),
720
+ selectSwipeAreaStyles(childrenArray.length, containerLayout.width),
535
721
  {
536
722
  transform: [{ translateX: pan.x }, { translateY: pan.y }]
537
723
  }
@@ -641,7 +827,7 @@ Carousel.propTypes = {
641
827
  * This function is also provided with a parameter indicating changed index (either 1, or -1)
642
828
  * Use it as follows:
643
829
  * ```js
644
- * const onIndexChangedCallback = React.useCallback((changedIndex, currentActiveIndex) => {
830
+ * const onIndexChangedCallback = useCallback((changedIndex, currentActiveIndex) => {
645
831
  * console.log(changedIndex)
646
832
  * }, []) // pass local dependencies as per your component
647
833
  * <Carousel
@@ -692,7 +878,7 @@ Carousel.propTypes = {
692
878
  * This function is also provided with a parameter indicating the current slide index before animation starts
693
879
  * Use it as follows:
694
880
  * ```js
695
- * const onAnimationStartCallback = React.useCallback((currentIndex) => {
881
+ * const onAnimationStartCallback = useCallback((currentIndex) => {
696
882
  * console.log(currentIndex)
697
883
  * }, []) // pass local dependencies as per your component
698
884
  * <Carousel
@@ -709,7 +895,7 @@ Carousel.propTypes = {
709
895
  * This function is also provided with a parameter indicating the updated slide index after animation ends
710
896
  * Use it as follows:
711
897
  * ```js
712
- * const onAnimationEndCallback = React.useCallback((changedIndex) => {
898
+ * const onAnimationEndCallback = useCallback((changedIndex) => {
713
899
  * console.log(changedIndex)
714
900
  * }, []) // pass local dependencies as per your component
715
901
  * <Carousel
@@ -743,7 +929,26 @@ Carousel.propTypes = {
743
929
  * Note that if the immediate Carousel children do not all render as `'li'` elements,
744
930
  * this should be changed (e.g. pass tag="div") because only 'li' is a valid child of 'ul'.
745
931
  */
746
- tag: PropTypes.oneOf(layoutTags)
932
+ tag: PropTypes.oneOf(layoutTags),
933
+ /**
934
+ * If set to `true`, the Carousel will automatically transition between slides
935
+ * and show the play/pause button
936
+ * - Default value is `false`
937
+ * - `slideDuration` and `transitionDuration` are required to be set for this to work
938
+ */
939
+ autoPlay: PropTypes.bool,
940
+ /**
941
+ * Duration of the time in seconds spent on each slide
942
+ * - Default value is `0`
943
+ * - `autoPlay` and `transitionDuration` are required to be set for this to work
944
+ */
945
+ slideDuration: PropTypes.number,
946
+ /**
947
+ * Duration of the time in seconds between each slide transition
948
+ * - Default value is `0`
949
+ * - `autoPlay` and `slideDuration` are required to be set for this to work
950
+ */
951
+ transitionDuration: PropTypes.number
747
952
  }
748
953
 
749
954
  Carousel.Item = CarouselItem