@telus-uds/components-base 3.25.0 → 3.27.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/lib/cjs/Card/Card.js +34 -13
  3. package/lib/cjs/Card/CardBase.js +78 -11
  4. package/lib/cjs/Card/PressableCardBase.js +147 -8
  5. package/lib/cjs/Carousel/Carousel.js +161 -77
  6. package/lib/cjs/Carousel/CarouselContext.js +10 -4
  7. package/lib/cjs/Carousel/CarouselItem/CarouselItem.js +11 -7
  8. package/lib/cjs/Carousel/Constants.js +22 -2
  9. package/lib/cjs/Checkbox/Checkbox.js +43 -13
  10. package/lib/cjs/InputSupports/InputSupports.js +2 -1
  11. package/lib/cjs/List/List.js +24 -9
  12. package/lib/cjs/List/ListItem.js +18 -1
  13. package/lib/cjs/List/ListItemBase.js +27 -8
  14. package/lib/cjs/List/ListItemMark.js +33 -62
  15. package/lib/cjs/List/PressableListItemBase.js +1 -0
  16. package/lib/cjs/PriceLockup/PriceLockup.js +1 -1
  17. package/lib/esm/Card/Card.js +34 -13
  18. package/lib/esm/Card/CardBase.js +78 -11
  19. package/lib/esm/Card/PressableCardBase.js +148 -9
  20. package/lib/esm/Carousel/Carousel.js +153 -69
  21. package/lib/esm/Carousel/CarouselContext.js +10 -4
  22. package/lib/esm/Carousel/CarouselItem/CarouselItem.js +11 -7
  23. package/lib/esm/Carousel/Constants.js +21 -1
  24. package/lib/esm/Checkbox/Checkbox.js +43 -13
  25. package/lib/esm/InputSupports/InputSupports.js +2 -1
  26. package/lib/esm/List/List.js +24 -9
  27. package/lib/esm/List/ListItem.js +19 -2
  28. package/lib/esm/List/ListItemBase.js +27 -8
  29. package/lib/esm/List/ListItemMark.js +33 -62
  30. package/lib/esm/List/PressableListItemBase.js +1 -0
  31. package/lib/esm/PriceLockup/PriceLockup.js +1 -1
  32. package/lib/package.json +2 -2
  33. package/package.json +2 -2
  34. package/src/Card/Card.jsx +29 -7
  35. package/src/Card/CardBase.jsx +88 -8
  36. package/src/Card/PressableCardBase.jsx +135 -9
  37. package/src/Carousel/Carousel.jsx +185 -88
  38. package/src/Carousel/CarouselContext.jsx +12 -4
  39. package/src/Carousel/CarouselItem/CarouselItem.jsx +10 -6
  40. package/src/Carousel/Constants.js +24 -0
  41. package/src/Checkbox/Checkbox.jsx +29 -7
  42. package/src/InputSupports/InputSupports.jsx +6 -1
  43. package/src/List/List.jsx +33 -9
  44. package/src/List/ListItem.jsx +33 -11
  45. package/src/List/ListItemBase.jsx +33 -9
  46. package/src/List/ListItemMark.jsx +32 -53
  47. package/src/List/PressableListItemBase.jsx +1 -0
  48. package/src/PriceLockup/PriceLockup.jsx +1 -1
@@ -1,5 +1,13 @@
1
1
  import React from 'react'
2
- import { View, Animated, PanResponder, StyleSheet, Platform, Dimensions } from 'react-native'
2
+ import {
3
+ View,
4
+ Animated,
5
+ PanResponder,
6
+ StyleSheet,
7
+ Platform,
8
+ Dimensions,
9
+ Easing
10
+ } from 'react-native'
3
11
  import PropTypes from 'prop-types'
4
12
  import { useThemeTokens, useTheme } from '../ThemeProvider'
5
13
  import { useViewport } from '../ViewportProvider'
@@ -37,16 +45,15 @@ import {
37
45
  LARGE_VIEWPORT_MARGIN,
38
46
  DEFAULT_VIEWPORT_MARGIN,
39
47
  PEEKING_MULTIPLIER,
40
- ACTIVE_INDEX_OFFSET_MULTIPLIER,
41
- NEGATIVE_MULTIPLIER
48
+ NEGATIVE_MULTIPLIER,
49
+ TRANSITION_MODES,
50
+ SWIPE_RELEASE_STYLES,
51
+ INSTANT_ANIMATION_DURATION,
52
+ DEFAULT_SWIPE_RELEASE_DURATION,
53
+ POSITION_VARIANTS,
54
+ POSITION_PROPERTIES
42
55
  } from './Constants'
43
56
 
44
- const TRANSITION_MODES = {
45
- MANUAL: 'manual',
46
- AUTOMATIC: 'automatic',
47
- SWIPE: 'swipe'
48
- }
49
-
50
57
  const staticStyles = StyleSheet.create({
51
58
  root: {
52
59
  backgroundColor: 'transparent',
@@ -106,7 +113,7 @@ const selectHeroContainerStyles = (width, hidden) => ({
106
113
  })
107
114
 
108
115
  const getDynamicPositionProperty = (areStylesAppliedOnPreviousButton) =>
109
- areStylesAppliedOnPreviousButton ? 'left' : 'right'
116
+ areStylesAppliedOnPreviousButton ? POSITION_PROPERTIES.LEFT : POSITION_PROPERTIES.RIGHT
110
117
 
111
118
  const selectControlButtonPositionStyles = ({
112
119
  positionVariant,
@@ -116,24 +123,41 @@ const selectControlButtonPositionStyles = ({
116
123
  enablePeeking,
117
124
  enableDisplayMultipleItemsPerSlide,
118
125
  isAutoPlayEnabled,
119
- viewport
126
+ viewport,
127
+ maxWidth,
128
+ viewportWidth
120
129
  }) => {
121
130
  const styles = {}
122
- if (positionVariant === 'edge') {
123
- styles[positionProperty] = -1 * (buttonWidth / 2)
124
- } else if (positionVariant === 'inside') {
125
- styles[positionProperty] = DEFAULT_POSITION_OFFSET
126
- } else if (positionVariant === 'outside') {
131
+
132
+ let positionOffset = 0
133
+
134
+ if (positionVariant === POSITION_VARIANTS.EDGE) {
135
+ positionOffset = -1 * (buttonWidth / 2)
136
+ } else if (positionVariant === POSITION_VARIANTS.INSIDE) {
137
+ positionOffset = DEFAULT_POSITION_OFFSET
138
+ } else if (positionVariant === POSITION_VARIANTS.OUTSIDE) {
127
139
  if (
128
140
  enablePeeking ||
129
141
  enableDisplayMultipleItemsPerSlide ||
130
142
  (isAutoPlayEnabled && viewport === 'xs')
131
143
  ) {
132
- styles[positionProperty] = 0
144
+ positionOffset = 0
133
145
  } else {
134
- styles[positionProperty] = -1 * (spaceBetweenSlideAndButton + buttonWidth)
146
+ positionOffset = -1 * (spaceBetweenSlideAndButton + buttonWidth)
135
147
  }
136
148
  }
149
+
150
+ if (enablePeeking) {
151
+ if (positionProperty === POSITION_PROPERTIES.RIGHT) {
152
+ const rightMargin = (viewportWidth - maxWidth) / 2
153
+ positionOffset += rightMargin
154
+ } else if (positionProperty === POSITION_PROPERTIES.LEFT) {
155
+ const leftMargin = (viewportWidth - maxWidth) / 2
156
+ positionOffset += leftMargin
157
+ }
158
+ }
159
+
160
+ styles[positionProperty] = positionOffset
137
161
  return styles
138
162
  }
139
163
 
@@ -147,7 +171,9 @@ const selectPreviousNextNavigationButtonStyles = (
147
171
  enablePeeking,
148
172
  enableDisplayMultipleItemsPerSlide,
149
173
  isAutoPlayEnabled,
150
- viewport
174
+ viewport,
175
+ maxWidth,
176
+ viewportWidth
151
177
  ) => {
152
178
  const styles = {
153
179
  zIndex: 1,
@@ -171,7 +197,9 @@ const selectPreviousNextNavigationButtonStyles = (
171
197
  enablePeeking,
172
198
  enableDisplayMultipleItemsPerSlide,
173
199
  isAutoPlayEnabled,
174
- viewport
200
+ viewport,
201
+ maxWidth,
202
+ viewportWidth
175
203
  })
176
204
  }
177
205
  }
@@ -240,7 +268,7 @@ const getMaximumItemsForSlide = (enableDisplayMultipleItemsPerSlide, viewport) =
240
268
  return ITEMS_PER_VIEWPORT_XS_SM
241
269
  }
242
270
 
243
- const selectRootContainerStyles = (enableHero, viewport) => {
271
+ const selectRootContainerStyles = (enableHero, viewport, enablePeeking) => {
244
272
  if (enableHero && viewport === 'xl' && Platform.OS === 'web') {
245
273
  return {
246
274
  alignItems: 'center'
@@ -251,16 +279,26 @@ const selectRootContainerStyles = (enableHero, viewport) => {
251
279
  paddingHorizontal: 16
252
280
  }
253
281
  }
282
+ if (enablePeeking) {
283
+ return {
284
+ width: '100%'
285
+ }
286
+ }
254
287
  return {}
255
288
  }
256
289
 
257
- const selectMainContainerStyles = (enableHero, viewport, maxWidth) => {
258
- if (enableHero && viewport === 'xl' && Platform.OS === 'web') {
290
+ const selectMainContainerStyles = (enableHero, viewport, maxWidth, enablePeeking) => {
291
+ if (enableHero && viewport === 'xl' && Platform.OS === 'web' && !enablePeeking) {
259
292
  return {
260
293
  width: '100%',
261
294
  maxWidth: maxWidth || 1200
262
295
  }
263
296
  }
297
+ if (enablePeeking) {
298
+ return {
299
+ width: '100%'
300
+ }
301
+ }
264
302
  if (maxWidth !== null && maxWidth !== undefined) {
265
303
  return {
266
304
  maxWidth,
@@ -271,16 +309,24 @@ const selectMainContainerStyles = (enableHero, viewport, maxWidth) => {
271
309
  return {}
272
310
  }
273
311
 
274
- const selectNavigationStyles = (tabs, enableHero, viewport) => {
312
+ const selectNavigationStyles = (tabs, enableHero, viewport, enablePeeking, maxWidth) => {
275
313
  let marginHorizontal = 0
276
314
 
277
315
  if (enableHero && tabs) {
278
316
  marginHorizontal = viewport === 'xl' ? LARGE_VIEWPORT_MARGIN : DEFAULT_VIEWPORT_MARGIN
279
317
  }
280
318
 
281
- return {
319
+ const styles = {
282
320
  marginHorizontal
283
321
  }
322
+
323
+ if (enablePeeking && maxWidth) {
324
+ styles.maxWidth = maxWidth
325
+ styles.alignSelf = 'center'
326
+ styles.width = '100%'
327
+ }
328
+
329
+ return styles
284
330
  }
285
331
 
286
332
  /**
@@ -289,22 +335,16 @@ const selectNavigationStyles = (tabs, enableHero, viewport) => {
289
335
  * @param {number} containerWidth - The width of the carousel container.
290
336
  * @param {boolean} enablePeeking - Flag indicating whether peeking is enabled.
291
337
  * @param {Object} viewport - The viewport configuration object used to determine peeking properties.
292
- * @param {React.MutableRefObject<number>} activeIndexRef - A ref object holding the current active index of the carousel.
338
+ * @param {number} maxWidth - The maximum width constraint for the carousel content.
293
339
  * @returns {number} The calculated final width of the carousel container.
294
340
  */
295
- const calculateFinalWidth = (containerWidth, enablePeeking, viewport, activeIndexRef) => {
341
+ const calculateFinalWidth = (containerWidth, enablePeeking, viewport, maxWidth) => {
296
342
  let finalWidth = containerWidth
297
343
 
298
344
  if (enablePeeking) {
299
- const { peekingGap, peekingMiddleSpace, peekingMarginLeft } = getPeekingProps(viewport)
300
- const slideWide =
301
- containerWidth - (peekingMiddleSpace * PEEKING_MULTIPLIER + peekingGap * PEEKING_MULTIPLIER)
302
-
303
- if (activeIndexRef.current === 0) {
304
- finalWidth = slideWide + peekingMarginLeft - peekingMiddleSpace
305
- } else {
306
- finalWidth = slideWide + peekingGap
307
- }
345
+ const { peekingGap, peekingMiddleSpace } = getPeekingProps(viewport)
346
+ const baseWidth = maxWidth || containerWidth
347
+ finalWidth = baseWidth - peekingMiddleSpace * PEEKING_MULTIPLIER + peekingGap
308
348
  }
309
349
 
310
350
  return finalWidth
@@ -412,6 +452,8 @@ const Carousel = React.forwardRef(
412
452
  autoPlay = false,
413
453
  enablePeeking = false,
414
454
  contentMaxWidth,
455
+ swipeReleaseStyle = SWIPE_RELEASE_STYLES.INSTANT,
456
+ swipeReleaseDuration = DEFAULT_SWIPE_RELEASE_DURATION,
415
457
  ...rest
416
458
  },
417
459
  ref
@@ -423,7 +465,15 @@ const Carousel = React.forwardRef(
423
465
 
424
466
  const contentMaxWidthValue = useResponsiveProp(contentMaxWidth)
425
467
  const responsiveWidth = useResponsiveProp(themeOptions?.contentMaxWidth)
426
- const maxWidth = resolveContentMaxWidth(contentMaxWidthValue, responsiveWidth)
468
+
469
+ let maxWidth = null
470
+ if (enablePeeking || contentMaxWidth !== undefined) {
471
+ maxWidth =
472
+ contentMaxWidthValue === undefined
473
+ ? responsiveWidth
474
+ : resolveContentMaxWidth(contentMaxWidthValue, responsiveWidth)
475
+ }
476
+
427
477
  const totalItems = getTotalItems(enableDisplayMultipleItemsPerSlide, childrenArray, viewport)
428
478
  const autoPlayFeatureEnabled =
429
479
  autoPlay && slideDuration > 0 && transitionDuration > 0 && totalItems > 1
@@ -486,8 +536,17 @@ const Carousel = React.forwardRef(
486
536
  const isSwiping = React.useRef(false)
487
537
  const autoPlayRef = React.useRef(null)
488
538
 
539
+ const [rootContainerLayout, setRootContainerLayout] = React.useState({
540
+ x: 0,
541
+ y: 0,
542
+ width: 0,
543
+ height: 0
544
+ })
545
+ const rootContainerLayoutRef = React.useRef(rootContainerLayout)
546
+
489
547
  const isFirstSlide = !activeIndex
490
548
  const isLastSlide = activeIndex + 1 >= totalItems
549
+ const currentViewportWidth = rootContainerLayout.width
491
550
 
492
551
  const handleAnimationStart = React.useCallback(
493
552
  (...args) => {
@@ -510,21 +569,16 @@ const Carousel = React.forwardRef(
510
569
 
511
570
  const updateOffset = React.useCallback(() => {
512
571
  if (enablePeeking) {
513
- const { peekingGap, peekingMiddleSpace, peekingMarginLeft } = getPeekingProps(viewport)
572
+ const { peekingGap, peekingMiddleSpace } = getPeekingProps(viewport)
514
573
 
515
574
  let finalWidth
516
- const slideWide =
517
- containerLayoutRef.current.width -
518
- (peekingMiddleSpace * PEEKING_MULTIPLIER + peekingGap * PEEKING_MULTIPLIER)
575
+ const baseWidth = maxWidth || containerLayoutRef.current.width
576
+ const slideWide = baseWidth - peekingMiddleSpace * PEEKING_MULTIPLIER
519
577
 
520
578
  if (activeIndexRef.current === 0) {
521
579
  finalWidth = 0
522
580
  } else {
523
- finalWidth =
524
- slideWide +
525
- peekingMarginLeft -
526
- peekingMiddleSpace +
527
- (slideWide + peekingGap) * (activeIndexRef.current - ACTIVE_INDEX_OFFSET_MULTIPLIER)
581
+ finalWidth = (slideWide + peekingGap) * activeIndexRef.current
528
582
  }
529
583
 
530
584
  animatedX.current = finalWidth * NEGATIVE_MULTIPLIER
@@ -550,17 +604,32 @@ const Carousel = React.forwardRef(
550
604
  })
551
605
  heroPan.setValue({ x: 0, y: 0 })
552
606
  }
553
- }, [pan, animatedX, heroPan, heroAnimatedX, enableHero, viewport, enablePeeking])
607
+ }, [pan, animatedX, heroPan, heroAnimatedX, enableHero, viewport, enablePeeking, maxWidth])
554
608
 
555
609
  const animate = React.useCallback(
556
- (panToAnimate, toValue, toIndex) => {
610
+ (panToAnimate, toValue, toIndex, isSwipeRelease = false) => {
557
611
  const applicableTransitionDuration =
558
612
  isLastSlide && toIndex === 0 ? loopDuration : transitionDuration
559
613
  const handleAnimationEndToIndex = (...args) => handleAnimationEnd(toIndex, ...args)
560
- if (reduceMotionRef.current || isSwiping.current) {
561
- Animated.timing(panToAnimate, { toValue, duration: 1, useNativeDriver: false }).start(
562
- handleAnimationEndToIndex
563
- )
614
+ if (reduceMotionRef.current) {
615
+ Animated.timing(panToAnimate, {
616
+ toValue,
617
+ duration: INSTANT_ANIMATION_DURATION,
618
+ useNativeDriver: false
619
+ }).start(handleAnimationEndToIndex)
620
+ } else if (isSwipeRelease && swipeReleaseStyle === SWIPE_RELEASE_STYLES.EASE_OUT) {
621
+ Animated.timing(panToAnimate, {
622
+ toValue,
623
+ duration: swipeReleaseDuration,
624
+ easing: Easing.out(Easing.cubic),
625
+ useNativeDriver: false
626
+ }).start(handleAnimationEndToIndex)
627
+ } else if (isSwiping.current || isSwipeRelease) {
628
+ Animated.timing(panToAnimate, {
629
+ toValue,
630
+ duration: INSTANT_ANIMATION_DURATION,
631
+ useNativeDriver: false
632
+ }).start(handleAnimationEndToIndex)
564
633
  } else if (isAutoPlayEnabled) {
565
634
  Animated.timing(panToAnimate, {
566
635
  ...springConfig,
@@ -591,7 +660,9 @@ const Carousel = React.forwardRef(
591
660
  isLastSlide,
592
661
  isAutoPlayEnabled,
593
662
  enablePeeking,
594
- enableDisplayMultipleItemsPerSlide
663
+ enableDisplayMultipleItemsPerSlide,
664
+ swipeReleaseStyle,
665
+ swipeReleaseDuration
595
666
  ]
596
667
  )
597
668
 
@@ -612,6 +683,7 @@ const Carousel = React.forwardRef(
612
683
  const toValue = { x: 0, y: 0 }
613
684
  let skipChanges = !delta
614
685
  let calcDelta = delta
686
+ const isSwipeRelease = transitionMode === TRANSITION_MODES.SWIPE
615
687
  if (activeIndexRef.current <= 0 && delta < 0) {
616
688
  skipChanges = transitionMode !== TRANSITION_MODES.AUTOMATIC
617
689
  calcDelta = totalItems + delta
@@ -623,10 +695,10 @@ const Carousel = React.forwardRef(
623
695
  const index = activeIndexRef.current + calcDelta
624
696
  if (skipChanges) {
625
697
  isTransitioningRef.current = true
626
- animate(pan, toValue, index)
698
+ animate(pan, toValue, index, isSwipeRelease)
627
699
 
628
700
  if (enableHero) {
629
- animate(heroPan, toValue, index)
701
+ animate(heroPan, toValue, index, isSwipeRelease)
630
702
  }
631
703
  return calcDelta
632
704
  }
@@ -638,16 +710,16 @@ const Carousel = React.forwardRef(
638
710
  containerLayoutRef.current.width,
639
711
  enablePeeking,
640
712
  viewport,
641
- activeIndexRef
713
+ maxWidth
642
714
  )
643
715
 
644
716
  toValue.x = finalWidth * -1 * calcDelta
645
717
  const heroToValue = { x: 0, y: 0 }
646
718
  heroToValue.x = heroContainerLayoutRef.current.width * -1 * calcDelta
647
719
  isTransitioningRef.current = true
648
- animate(pan, toValue, index)
720
+ animate(pan, toValue, index, isSwipeRelease)
649
721
  if (enableHero) {
650
- animate(heroPan, heroToValue, index)
722
+ animate(heroPan, heroToValue, index, isSwipeRelease)
651
723
  }
652
724
  if (isCarouselPlaying) {
653
725
  stopAutoplay()
@@ -684,7 +756,8 @@ const Carousel = React.forwardRef(
684
756
  enablePeeking,
685
757
  pan,
686
758
  heroPan,
687
- enableHero
759
+ enableHero,
760
+ maxWidth
688
761
  ]
689
762
  )
690
763
 
@@ -741,6 +814,10 @@ const Carousel = React.forwardRef(
741
814
  heroContainerLayoutRef.current = heroContainerLayout
742
815
  }, [heroContainerLayout])
743
816
 
817
+ React.useEffect(() => {
818
+ rootContainerLayoutRef.current = rootContainerLayout
819
+ }, [rootContainerLayout])
820
+
744
821
  React.useEffect(() => {
745
822
  pan.x.addListener(({ value }) => {
746
823
  animatedX.current = value
@@ -823,6 +900,12 @@ const Carousel = React.forwardRef(
823
900
  }
824
901
  }) => setPreviousNextNavigationButtonWidth(width)
825
902
 
903
+ const onRootContainerLayout = ({
904
+ nativeEvent: {
905
+ layout: { x, y, width, height }
906
+ }
907
+ }) => setRootContainerLayout((prevState) => ({ ...prevState, x, y, width, height }))
908
+
826
909
  const isSwipeAllowed = React.useCallback(() => {
827
910
  if (totalItems === 1) {
828
911
  return false
@@ -866,14 +949,14 @@ const Carousel = React.forwardRef(
866
949
  }
867
950
  const correction = gesture.moveX - gesture.x0
868
951
 
952
+ isSwiping.current = false
953
+
869
954
  if (Math.abs(correction) < containerLayoutRef.current.width * minDistanceForAction) {
870
- animate(pan, { x: 0, y: 0 }, 0)
955
+ animate(pan, { x: 0, y: 0 }, activeIndexRef.current, true)
871
956
  } else {
872
957
  const delta = correction > 0 ? -1 : 1
873
958
  updateIndex(delta, TRANSITION_MODES.SWIPE)
874
959
  }
875
-
876
- isSwiping.current = false
877
960
  }
878
961
  }),
879
962
  [
@@ -924,14 +1007,14 @@ const Carousel = React.forwardRef(
924
1007
  }
925
1008
  const correction = gesture.moveX - gesture.x0
926
1009
 
1010
+ isSwiping.current = false
1011
+
927
1012
  if (Math.abs(correction) < containerLayoutRef.current.width * minDistanceForAction) {
928
- animate(heroPan, { x: 0, y: 0 }, 0)
1013
+ animate(heroPan, { x: 0, y: 0 }, activeIndexRef.current, true)
929
1014
  } else {
930
1015
  const delta = correction > 0 ? -1 : 1
931
1016
  updateIndex(delta, TRANSITION_MODES.SWIPE)
932
1017
  }
933
-
934
- isSwiping.current = false
935
1018
  }
936
1019
  }),
937
1020
  [
@@ -1045,8 +1128,11 @@ const Carousel = React.forwardRef(
1045
1128
  )
1046
1129
 
1047
1130
  return (
1048
- <View style={selectRootContainerStyles(enableHero, viewport)}>
1049
- <View style={selectMainContainerStyles(enableHero, viewport, maxWidth)}>
1131
+ <View
1132
+ style={selectRootContainerStyles(enableHero, viewport, enablePeeking)}
1133
+ onLayout={onRootContainerLayout}
1134
+ >
1135
+ <View style={selectMainContainerStyles(enableHero, viewport, maxWidth, enablePeeking)}>
1050
1136
  <CarouselProvider
1051
1137
  activeIndex={activeIndex}
1052
1138
  goTo={goTo}
@@ -1061,6 +1147,8 @@ const Carousel = React.forwardRef(
1061
1147
  enableDisplayMultipleItemsPerSlide,
1062
1148
  viewport
1063
1149
  )}
1150
+ maxWidth={maxWidth}
1151
+ viewportWidth={currentViewportWidth}
1064
1152
  >
1065
1153
  <View
1066
1154
  style={[
@@ -1087,7 +1175,9 @@ const Carousel = React.forwardRef(
1087
1175
  enablePeeking,
1088
1176
  enableDisplayMultipleItemsPerSlide,
1089
1177
  isAutoPlayEnabled,
1090
- viewport
1178
+ viewport,
1179
+ maxWidth,
1180
+ viewportWidth: currentViewportWidth
1091
1181
  })
1092
1182
  ]}
1093
1183
  >
@@ -1113,7 +1203,9 @@ const Carousel = React.forwardRef(
1113
1203
  enablePeeking,
1114
1204
  enableDisplayMultipleItemsPerSlide,
1115
1205
  isAutoPlayEnabled,
1116
- viewport
1206
+ viewport,
1207
+ maxWidth,
1208
+ currentViewportWidth
1117
1209
  )}
1118
1210
  testID="previous-button-container"
1119
1211
  >
@@ -1166,22 +1258,7 @@ const Carousel = React.forwardRef(
1166
1258
  let hidden = !isAnimating && index !== activeIndex
1167
1259
 
1168
1260
  if (enablePeeking && !isAnimating) {
1169
- if (enableDisplayMultipleItemsPerSlide) {
1170
- const maxItemsForSlide = getMaximumItemsForSlide(
1171
- enableDisplayMultipleItemsPerSlide,
1172
- viewport
1173
- )
1174
- if (
1175
- index >= activeIndex * maxItemsForSlide - 1 &&
1176
- index < activeIndex * maxItemsForSlide + maxItemsForSlide + 1
1177
- ) {
1178
- hidden = false
1179
- } else {
1180
- hidden = true
1181
- }
1182
- } else if (index >= activeIndex - 1 && index <= activeIndex + 1) {
1183
- hidden = false
1184
- }
1261
+ hidden = false
1185
1262
  } else if (
1186
1263
  !enablePeeking &&
1187
1264
  enableDisplayMultipleItemsPerSlide &&
@@ -1224,7 +1301,9 @@ const Carousel = React.forwardRef(
1224
1301
  enablePeeking,
1225
1302
  enableDisplayMultipleItemsPerSlide,
1226
1303
  isAutoPlayEnabled,
1227
- viewport
1304
+ viewport,
1305
+ maxWidth,
1306
+ currentViewportWidth
1228
1307
  )}
1229
1308
  testID="next-button-container"
1230
1309
  >
@@ -1242,7 +1321,9 @@ const Carousel = React.forwardRef(
1242
1321
  </View>
1243
1322
  ) : null}
1244
1323
  </View>
1245
- <View style={selectNavigationStyles(tabs, enableHero, viewport)}>
1324
+ <View
1325
+ style={selectNavigationStyles(tabs, enableHero, viewport, enablePeeking, maxWidth)}
1326
+ >
1246
1327
  {showPanelNavigation ? activePanelNavigation : null}
1247
1328
  </View>
1248
1329
  </CarouselProvider>
@@ -1495,7 +1576,23 @@ Carousel.propTypes = {
1495
1576
  md: PropTypes.oneOfType([PropTypes.oneOf(['max', 'full']), PropTypes.number]),
1496
1577
  sm: PropTypes.oneOfType([PropTypes.oneOf(['max', 'full']), PropTypes.number]),
1497
1578
  xs: PropTypes.oneOfType([PropTypes.oneOf(['max', 'full']), PropTypes.number])
1498
- })
1579
+ }),
1580
+ /**
1581
+ * Animation style for swipe release transitions.
1582
+ * - `'instant'`: Immediate snap to position (current default)
1583
+ * - `'ease-out'`: Smooth deceleration animation
1584
+ * - Default value is `'instant'`
1585
+ * - Use `swipeReleaseDuration` to customize the animation duration when using `'ease-out'`
1586
+ *
1587
+ * @deprecated The default will change to `'ease-out'` in Q2 2026 (introduced Jan 2026).
1588
+ */
1589
+ swipeReleaseStyle: PropTypes.oneOf(['instant', 'ease-out']),
1590
+ /**
1591
+ * Duration in milliseconds of the ease-out animation when releasing a swipe gesture.
1592
+ * Only applies when `swipeReleaseStyle` is set to `'ease-out'`.
1593
+ * - Default value is `500` (500ms)
1594
+ */
1595
+ swipeReleaseDuration: PropTypes.number
1499
1596
  }
1500
1597
 
1501
1598
  Carousel.Item = CarouselItem
@@ -14,7 +14,9 @@ const CarouselProvider = ({
14
14
  themeTokens,
15
15
  totalItems,
16
16
  width,
17
- maximumItemsForSlide
17
+ maximumItemsForSlide,
18
+ maxWidth,
19
+ viewportWidth
18
20
  }) => {
19
21
  const value = React.useMemo(
20
22
  () => ({
@@ -26,7 +28,9 @@ const CarouselProvider = ({
26
28
  themeTokens,
27
29
  totalItems,
28
30
  width,
29
- maximumItemsForSlide
31
+ maximumItemsForSlide,
32
+ maxWidth,
33
+ viewportWidth
30
34
  }),
31
35
  [
32
36
  activeIndex,
@@ -37,7 +41,9 @@ const CarouselProvider = ({
37
41
  totalItems,
38
42
  themeTokens,
39
43
  width,
40
- maximumItemsForSlide
44
+ maximumItemsForSlide,
45
+ maxWidth,
46
+ viewportWidth
41
47
  ]
42
48
  )
43
49
  return <CarouselContext.Provider value={value}>{children}</CarouselContext.Provider>
@@ -61,7 +67,9 @@ CarouselProvider.propTypes = {
61
67
  themeTokens: getTokensPropType('Carousel'),
62
68
  totalItems: PropTypes.number.isRequired,
63
69
  width: PropTypes.number.isRequired,
64
- maximumItemsForSlide: PropTypes.number
70
+ maximumItemsForSlide: PropTypes.number,
71
+ maxWidth: PropTypes.number,
72
+ viewportWidth: PropTypes.number
65
73
  }
66
74
 
67
75
  export { CarouselProvider, useCarousel }
@@ -18,27 +18,28 @@ const selectContainerStyle = ({
18
18
  width,
19
19
  elementIndex,
20
20
  enablePeeking,
21
- peekingMarginLeft,
22
21
  peekingGap,
23
22
  hidden,
24
23
  enableDisplayMultipleItemsPerSlide,
25
24
  viewport,
26
- peekingMiddleSpace
25
+ peekingMiddleSpace,
26
+ maxWidth,
27
+ viewportWidth
27
28
  }) => {
28
29
  let adjustedWidth = width
29
30
  let marginLeft = 0
30
31
 
31
32
  if (enablePeeking) {
32
33
  const isFirst = elementIndex === 0
33
- adjustedWidth = width - (peekingMiddleSpace * 2 + peekingGap * 2)
34
+ const baseWidth = maxWidth || width
35
+ adjustedWidth = baseWidth - peekingMiddleSpace * 2
34
36
  if (isFirst) {
35
- marginLeft = peekingMarginLeft
37
+ marginLeft = peekingMiddleSpace + (viewportWidth - maxWidth) / 2
36
38
  } else {
37
39
  marginLeft = peekingGap
38
40
  }
39
41
  }
40
42
 
41
- // Adjust width and margins for multiple items per slide.
42
43
  if (enableDisplayMultipleItemsPerSlide) {
43
44
  switch (viewport) {
44
45
  case 'xs':
@@ -102,7 +103,8 @@ const CarouselItem = React.forwardRef(
102
103
  },
103
104
  ref
104
105
  ) => {
105
- const { width, activeIndex, goTo, maximumItemsForSlide } = useCarousel()
106
+ const { width, activeIndex, goTo, maximumItemsForSlide, maxWidth, viewportWidth } =
107
+ useCarousel()
106
108
 
107
109
  const selectedProps = selectProps({
108
110
  ...rest,
@@ -162,6 +164,8 @@ const CarouselItem = React.forwardRef(
162
164
  enablePeeking,
163
165
  enableDisplayMultipleItemsPerSlide,
164
166
  viewport,
167
+ maxWidth,
168
+ viewportWidth,
165
169
  ...peekingProps
166
170
  })}
167
171
  {...selectedProps}
@@ -9,3 +9,27 @@ export const DEFAULT_VIEWPORT_MARGIN = 10
9
9
  export const PEEKING_MULTIPLIER = 2
10
10
  export const ACTIVE_INDEX_OFFSET_MULTIPLIER = 1
11
11
  export const NEGATIVE_MULTIPLIER = -1
12
+
13
+ export const TRANSITION_MODES = {
14
+ MANUAL: 'manual',
15
+ AUTOMATIC: 'automatic',
16
+ SWIPE: 'swipe'
17
+ }
18
+
19
+ export const SWIPE_RELEASE_STYLES = {
20
+ INSTANT: 'instant',
21
+ EASE_OUT: 'ease-out'
22
+ }
23
+
24
+ export const INSTANT_ANIMATION_DURATION = 1
25
+ export const DEFAULT_SWIPE_RELEASE_DURATION = 500
26
+ export const POSITION_VARIANTS = {
27
+ EDGE: 'edge',
28
+ INSIDE: 'inside',
29
+ OUTSIDE: 'outside'
30
+ }
31
+
32
+ export const POSITION_PROPERTIES = {
33
+ LEFT: 'left',
34
+ RIGHT: 'right'
35
+ }