@telus-uds/components-base 3.21.0 → 3.23.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 (64) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/lib/cjs/Button/Button.js +12 -3
  3. package/lib/cjs/Button/ButtonBase.js +63 -10
  4. package/lib/cjs/Button/ButtonDropdown.js +2 -0
  5. package/lib/cjs/Button/ButtonGroup.js +45 -38
  6. package/lib/cjs/Button/propTypes.js +6 -0
  7. package/lib/cjs/Card/PressableCardBase.js +3 -1
  8. package/lib/cjs/Carousel/Carousel.js +63 -22
  9. package/lib/cjs/Carousel/CarouselItem/CarouselItem.js +23 -3
  10. package/lib/cjs/Icon/Icon.js +8 -11
  11. package/lib/cjs/Icon/IconText.js +0 -1
  12. package/lib/cjs/Listbox/GroupControl.js +33 -39
  13. package/lib/cjs/Listbox/Listbox.js +22 -13
  14. package/lib/cjs/Listbox/ListboxGroup.js +2 -1
  15. package/lib/cjs/Listbox/ListboxOverlay.js +5 -2
  16. package/lib/cjs/Listbox/PressableItem.js +8 -4
  17. package/lib/cjs/TextInput/TextInputBase.js +5 -1
  18. package/lib/cjs/ThemeProvider/index.js +9 -1
  19. package/lib/cjs/ThemeProvider/useResponsiveThemeTokensCallback.js +124 -0
  20. package/lib/cjs/Validator/Validator.js +171 -135
  21. package/lib/cjs/index.js +7 -0
  22. package/lib/esm/Button/Button.js +13 -4
  23. package/lib/esm/Button/ButtonBase.js +64 -11
  24. package/lib/esm/Button/ButtonDropdown.js +2 -0
  25. package/lib/esm/Button/ButtonGroup.js +44 -39
  26. package/lib/esm/Button/propTypes.js +6 -0
  27. package/lib/esm/Card/PressableCardBase.js +3 -1
  28. package/lib/esm/Carousel/Carousel.js +63 -22
  29. package/lib/esm/Carousel/CarouselItem/CarouselItem.js +23 -3
  30. package/lib/esm/Icon/Icon.js +8 -11
  31. package/lib/esm/Icon/IconText.js +0 -1
  32. package/lib/esm/Listbox/GroupControl.js +33 -39
  33. package/lib/esm/Listbox/Listbox.js +23 -14
  34. package/lib/esm/Listbox/ListboxGroup.js +2 -1
  35. package/lib/esm/Listbox/ListboxOverlay.js +5 -2
  36. package/lib/esm/Listbox/PressableItem.js +8 -4
  37. package/lib/esm/TextInput/TextInputBase.js +5 -1
  38. package/lib/esm/ThemeProvider/index.js +1 -0
  39. package/lib/esm/ThemeProvider/useResponsiveThemeTokensCallback.js +117 -0
  40. package/lib/esm/Validator/Validator.js +171 -135
  41. package/lib/esm/index.js +1 -1
  42. package/lib/package.json +2 -2
  43. package/package.json +2 -2
  44. package/src/Button/Button.jsx +26 -5
  45. package/src/Button/ButtonBase.jsx +79 -16
  46. package/src/Button/ButtonDropdown.jsx +2 -0
  47. package/src/Button/ButtonGroup.jsx +62 -45
  48. package/src/Button/propTypes.js +6 -0
  49. package/src/Card/PressableCardBase.jsx +3 -1
  50. package/src/Carousel/Carousel.jsx +71 -7
  51. package/src/Carousel/CarouselItem/CarouselItem.jsx +31 -3
  52. package/src/Icon/Icon.jsx +11 -14
  53. package/src/Icon/IconText.jsx +0 -1
  54. package/src/Listbox/GroupControl.jsx +41 -47
  55. package/src/Listbox/Listbox.jsx +26 -9
  56. package/src/Listbox/ListboxGroup.jsx +2 -1
  57. package/src/Listbox/ListboxOverlay.jsx +7 -2
  58. package/src/Listbox/PressableItem.jsx +8 -4
  59. package/src/PriceLockup/utils/renderPrice.jsx +15 -17
  60. package/src/TextInput/TextInputBase.jsx +5 -1
  61. package/src/ThemeProvider/index.js +1 -0
  62. package/src/ThemeProvider/useResponsiveThemeTokensCallback.js +129 -0
  63. package/src/Validator/Validator.jsx +180 -159
  64. package/src/index.js +2 -1
package/CHANGELOG.md CHANGED
@@ -1,9 +1,39 @@
1
1
  # Change Log - @telus-uds/components-base
2
2
 
3
- This log was last generated on Tue, 21 Oct 2025 14:46:26 GMT and should not be manually modified.
3
+ This log was last generated on Wed, 19 Nov 2025 05:51:39 GMT and should not be manually modified.
4
4
 
5
5
  <!-- Start content -->
6
6
 
7
+ ## 3.23.0
8
+
9
+ Wed, 19 Nov 2025 05:51:39 GMT
10
+
11
+ ### Minor changes
12
+
13
+ - `Listbox`: Added test and snapshot for Listbox (david.melara1@telus.com)
14
+ - `Listbox`: Updated style for koodo, allium and public mobile; added new style variant (david.melara1@telus.com)
15
+ - `ButtonGroup`: enhacements for iOS and android implemented and fix for the mobile version (35577399+JoshHC@users.noreply.github.com)
16
+ - `Button`: Implement heightFull prop (oscar.palencia@telus.com)
17
+ - Bump @telus-uds/system-theme-tokens to v4.16.0
18
+
19
+ ### Patches
20
+
21
+ - `Validator`: code refactored to resolve several bugs (35577399+JoshHC@users.noreply.github.com)
22
+ - `Carousel`: fix tab navigation (guillermo.peitzner@telus.com)
23
+
24
+ ## 3.22.0
25
+
26
+ Wed, 29 Oct 2025 07:40:46 GMT
27
+
28
+ ### Minor changes
29
+
30
+ - `Carousel`: Add `loopDuration` prop (oscar.palencia@telus.com)
31
+ - `Button`: add RNMQ support (guillermo.peitzner@telus.com)
32
+
33
+ ### Patches
34
+
35
+ - `Interactive Card`: extra spacing fixed in interactive Cards when padding tokens are set to none (35577399+JoshHC@users.noreply.github.com)
36
+
7
37
  ## 3.21.0
8
38
 
9
39
  Tue, 21 Oct 2025 14:46:26 GMT
@@ -19,19 +19,28 @@ const Button = /*#__PURE__*/_react.default.forwardRef((_ref, ref) => {
19
19
  accessibilityRole = 'button',
20
20
  tokens,
21
21
  variant,
22
+ heightFull = true,
22
23
  ...props
23
24
  } = _ref;
24
25
  const viewport = (0, _ViewportProvider.useViewport)();
25
- const buttonVariant = {
26
+ const {
27
+ themeOptions: {
28
+ enableMediaQueryStyleSheet
29
+ }
30
+ } = (0, _ThemeProvider.useTheme)();
31
+ const buttonVariant = enableMediaQueryStyleSheet ? variant : {
26
32
  viewport,
27
33
  ...variant
28
34
  };
29
- const getTokens = (0, _ThemeProvider.useThemeTokensCallback)('Button', tokens, buttonVariant);
35
+ const useTokens = enableMediaQueryStyleSheet ? _ThemeProvider.useResponsiveThemeTokensCallback : _ThemeProvider.useThemeTokensCallback;
36
+ const getTokens = useTokens('Button', tokens, buttonVariant);
30
37
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_ButtonBase.default, {
31
38
  ...props,
32
39
  tokens: getTokens,
40
+ heightFull: heightFull,
33
41
  accessibilityRole: accessibilityRole,
34
- ref: ref
42
+ ref: ref,
43
+ viewport: viewport
35
44
  });
36
45
  });
37
46
  Button.displayName = 'Button';
@@ -40,23 +40,27 @@ const selectFlexAndWidthStyles = _ref2 => {
40
40
  }
41
41
  return styles;
42
42
  };
43
- const selectOuterContainerStyles = _ref3 => {
43
+ const selectOuterContainerStyles = (_ref3, heightFull) => {
44
44
  let {
45
45
  opacity,
46
46
  outerBorderColor,
47
47
  outerBorderWidth,
48
48
  outerBorderGap,
49
49
  borderRadius,
50
- outerBackgroundColor
50
+ outerBackgroundColor,
51
+ alignSelf
51
52
  } = _ref3;
52
53
  return {
54
+ backgroundColor: outerBackgroundColor,
55
+ opacity,
53
56
  ..._Platform.default.select({
54
57
  native: {
58
+ alignSelf: alignSelf !== undefined ? alignSelf : 'flex-start'
59
+ },
60
+ web: heightFull ? {} : {
55
61
  alignSelf: 'flex-start'
56
62
  }
57
63
  }),
58
- backgroundColor: outerBackgroundColor,
59
- opacity,
60
64
  ...(0, _ThemeProvider.applyOuterBorder)({
61
65
  outerBorderGap,
62
66
  outerBorderWidth,
@@ -255,8 +259,16 @@ const ButtonBase = /*#__PURE__*/_react.default.forwardRef((_ref12, ref) => {
255
259
  icon,
256
260
  iconPosition = icon ? 'left' : undefined,
257
261
  iconProps,
262
+ heightFull = true,
258
263
  ...rawRest
259
264
  } = _ref12;
265
+ const {
266
+ themeOptions
267
+ } = (0, _ThemeProvider.useTheme)();
268
+ const {
269
+ viewport
270
+ } = rawRest;
271
+ const enableMediaQueryStyleSheet = themeOptions.enableMediaQueryStyleSheet && viewport;
260
272
  const {
261
273
  onPress,
262
274
  ...rest
@@ -268,15 +280,55 @@ const ButtonBase = /*#__PURE__*/_react.default.forwardRef((_ref12, ref) => {
268
280
  };
269
281
  const resolveButtonTokens = pressableState => (0, _utils.resolvePressableTokens)(tokens, pressableState, extraButtonState);
270
282
  const systemProps = selectProps(rest);
283
+ let layoutMediaQueryStyles;
284
+ let flexAndWidthStylesIds;
285
+ if (enableMediaQueryStyleSheet) {
286
+ const defaultPressableState = {
287
+ pressed: false,
288
+ hovered: false,
289
+ focused: false
290
+ };
291
+ const defaultTokensByViewport = resolveButtonTokens(defaultPressableState);
292
+ const layoutTokensByViewport = Object.entries(defaultTokensByViewport).reduce((acc, _ref13) => {
293
+ let [vp, viewportTokens] = _ref13;
294
+ const flexAndWidthStyles = viewportTokens.width === '100%' && viewportTokens.flex === 1 ? selectFlexAndWidthStyles(viewportTokens) : {};
295
+ acc[vp] = {
296
+ ...staticStyles.row,
297
+ ...selectWebOnlyStyles(inactive, viewportTokens, systemProps),
298
+ ...(Object.keys(flexAndWidthStyles).length > 0 ? flexAndWidthStyles : {}),
299
+ ...selectOuterSizeStyles(viewportTokens)
300
+ };
301
+ return acc;
302
+ }, {});
303
+ const mediaQueryStyles = (0, _utils.createMediaQueryStyles)(layoutTokensByViewport);
304
+ const {
305
+ ids,
306
+ styles
307
+ } = _utils.StyleSheet.create({
308
+ layout: {
309
+ ...mediaQueryStyles
310
+ }
311
+ });
312
+ layoutMediaQueryStyles = styles.layout;
313
+ flexAndWidthStylesIds = ids.layout;
314
+ }
271
315
  const getPressableStyle = pressableState => {
316
+ if (enableMediaQueryStyleSheet) {
317
+ const themeTokens = resolveButtonTokens(pressableState)[viewport];
318
+ return [layoutMediaQueryStyles, selectOuterContainerStyles(themeTokens)];
319
+ }
272
320
  const themeTokens = resolveButtonTokens(pressableState);
273
- // Only apply flex and width styles when they are explicitly set (e.g., from ButtonGroup with width: 'equal') to not to affect other use cases
274
321
  const flexAndWidthStyles = themeTokens.width === '100%' && themeTokens.flex === 1 ? selectFlexAndWidthStyles(themeTokens) : {};
275
- return [staticStyles.row, selectWebOnlyStyles(inactive, themeTokens, systemProps), selectOuterContainerStyles(themeTokens), ...(Object.keys(flexAndWidthStyles).length > 0 ? [flexAndWidthStyles] : []), selectOuterSizeStyles(themeTokens)];
322
+ return [staticStyles.row, selectWebOnlyStyles(inactive, themeTokens, systemProps), selectOuterContainerStyles(themeTokens, heightFull), ...(Object.keys(flexAndWidthStyles).length > 0 ? [flexAndWidthStyles] : []), selectOuterSizeStyles(themeTokens)];
276
323
  };
277
- const {
278
- themeOptions
279
- } = (0, _ThemeProvider.useTheme)();
324
+ const dataSetProp = flexAndWidthStylesIds || rawRest.dataSet ? {
325
+ dataSet: {
326
+ ...(flexAndWidthStylesIds ? {
327
+ media: flexAndWidthStylesIds
328
+ } : {}),
329
+ ...rawRest.dataSet
330
+ }
331
+ } : {};
280
332
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_Pressable.default, {
281
333
  ref: ref,
282
334
  href: href,
@@ -288,8 +340,9 @@ const ButtonBase = /*#__PURE__*/_react.default.forwardRef((_ref12, ref) => {
288
340
  disabled: inactive,
289
341
  hrefAttrs: hrefAttrs,
290
342
  ...systemProps,
343
+ ...dataSetProp,
291
344
  children: pressableState => {
292
- const themeTokens = resolveButtonTokens(pressableState);
345
+ const themeTokens = enableMediaQueryStyleSheet ? resolveButtonTokens(pressableState)[viewport] : resolveButtonTokens(pressableState);
293
346
  const containerStyles = selectInnerContainerStyles(themeTokens);
294
347
  const borderStyles = selectBorderStyles(themeTokens);
295
348
  const textStyles = [selectTextStyles(themeTokens, themeOptions), staticStyles.text, _Platform.default.select({
@@ -112,6 +112,7 @@ const ButtonDropdown = /*#__PURE__*/_react.default.forwardRef((_ref2, ref) => {
112
112
  accessibilityRole = 'radio',
113
113
  description,
114
114
  singleOption,
115
+ heightFull = true,
115
116
  ...props
116
117
  } = _ref2;
117
118
  const isFullWidth = variant?.width === FULL_WIDTH_STYLE;
@@ -154,6 +155,7 @@ const ButtonDropdown = /*#__PURE__*/_react.default.forwardRef((_ref2, ref) => {
154
155
  ...pressHandlers,
155
156
  onPress: handlePress,
156
157
  tokens: getButtonTokens,
158
+ heightFull: heightFull,
157
159
  inactive: singleOption || inactive,
158
160
  icon: () => null,
159
161
  accessibilityRole: accessibilityRole,
@@ -4,12 +4,13 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = void 0;
7
- var _react = _interopRequireDefault(require("react"));
7
+ var _react = _interopRequireWildcard(require("react"));
8
8
  var _propTypes = _interopRequireDefault(require("prop-types"));
9
9
  var _airbnbPropTypes = _interopRequireDefault(require("airbnb-prop-types"));
10
10
  var _Platform = _interopRequireDefault(require("react-native-web/dist/cjs/exports/Platform"));
11
+ var _View = _interopRequireDefault(require("react-native-web/dist/cjs/exports/View"));
12
+ var _StyleSheet = _interopRequireDefault(require("react-native-web/dist/cjs/exports/StyleSheet"));
11
13
  var _ButtonBase = _interopRequireDefault(require("./ButtonBase"));
12
- var _StackView = require("../StackView");
13
14
  var _Fieldset = _interopRequireDefault(require("../Fieldset"));
14
15
  var _ViewportProvider = require("../ViewportProvider");
15
16
  var _ThemeProvider = require("../ThemeProvider");
@@ -17,17 +18,10 @@ var _utils = require("../utils");
17
18
  var _pressability = require("../utils/pressability");
18
19
  var _jsxRuntime = require("react/jsx-runtime");
19
20
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
21
+ function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
22
+ function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
20
23
  const [selectProps, selectedSystemPropTypes] = (0, _utils.selectSystemProps)([_utils.a11yProps, _utils.viewProps]);
21
24
  const [selectItemProps, selectedItemPropTypes] = (0, _utils.selectSystemProps)([_utils.a11yProps, _utils.focusHandlerProps, _utils.pressProps, _utils.viewProps]);
22
- const getStackWrapTokens = variant => {
23
- return _Platform.default.select({
24
- web: {
25
- justifyContent: variant?.width === 'equal' ? 'space-evenly' : 'flex-start',
26
- width: variant?.width === 'equal' ? '100%' : 'auto'
27
- },
28
- default: {}
29
- });
30
- };
31
25
  const ButtonGroup = /*#__PURE__*/_react.default.forwardRef((_ref, ref) => {
32
26
  let {
33
27
  variant,
@@ -60,12 +54,6 @@ const ButtonGroup = /*#__PURE__*/_react.default.forwardRef((_ref, ref) => {
60
54
  const themeTokens = (0, _ThemeProvider.useThemeTokens)('ButtonGroup', tokens, variant, {
61
55
  viewport
62
56
  });
63
- const themeStackTokens = (0, _utils.selectTokens)('StackView', themeTokens);
64
- const variantStackTokens = getStackWrapTokens(variant);
65
- const stackTokens = {
66
- ...themeStackTokens,
67
- ...variantStackTokens
68
- };
69
57
  const {
70
58
  direction,
71
59
  space,
@@ -75,15 +63,34 @@ const ButtonGroup = /*#__PURE__*/_react.default.forwardRef((_ref, ref) => {
75
63
  padding,
76
64
  gap
77
65
  } = themeTokens;
66
+ const isMobileNonContained = _Platform.default.OS !== 'web' && (!variant || variant?.style !== 'contained');
78
67
  const themeButtonTokensCallback = (0, _ThemeProvider.useThemeTokensCallback)('ButtonGroupItem', tokens, variant);
79
- const getButtonTokens = state => {
68
+ const gapValue = (0, _utils.useSpacingScale)(gap || space);
69
+ const getButtonTokens = (0, _react.useCallback)(state => {
80
70
  const themeButtonTokens = themeButtonTokensCallback(state);
71
+ const shouldUseTransparentBackground = isMobileNonContained && !state.selected && !state.pressed && !state.hover && !state.focus;
81
72
  return {
82
73
  ...themeButtonTokens,
83
- width: variant?.width === 'equal' ? '100%' : 'auto',
84
- flex: variant?.width === 'equal' ? 1 : undefined
74
+ ...(variant?.width === 'equal' && staticStyles.equalWidth),
75
+ ...(shouldUseTransparentBackground && {
76
+ backgroundColor: 'transparent'
77
+ }),
78
+ alignSelf: themeButtonTokens.width ? 'flex-start' : 'center'
85
79
  };
86
- };
80
+ }, [themeButtonTokensCallback, isMobileNonContained, variant?.width]);
81
+ const fieldsetStyles = (0, _react.useMemo)(() => ({
82
+ ...staticStyles.fieldsetBase,
83
+ borderRadius,
84
+ backgroundColor: isMobileNonContained ? 'transparent' : backgroundColor || 'transparent',
85
+ padding,
86
+ width: variant?.width === 'equal' ? '100%' : 'auto'
87
+ }), [borderRadius, backgroundColor, padding, variant?.width, isMobileNonContained]);
88
+ const viewStyles = (0, _react.useMemo)(() => ({
89
+ ...staticStyles.viewBase,
90
+ flexDirection: direction === 'column' ? 'column' : 'row',
91
+ gap: gapValue || 0,
92
+ justifyContent: variant?.width === 'equal' ? 'space-evenly' : 'flex-start'
93
+ }), [direction, gapValue, variant?.width]);
87
94
  const {
88
95
  currentValues,
89
96
  toggleOneValue
@@ -120,25 +127,11 @@ const ButtonGroup = /*#__PURE__*/_react.default.forwardRef((_ref, ref) => {
120
127
  inactive: inactive,
121
128
  validation: validation,
122
129
  accessibilityRole: accessibilityRole,
123
- style: {
124
- borderRadius,
125
- backgroundColor,
126
- padding,
127
- ...(_Platform.default.OS === 'web' ? {
128
- gap,
129
- width: variant?.width === 'equal' ? '100%' : 'auto'
130
- } : {
131
- alignSelf: 'flex-start'
132
- })
133
- },
130
+ style: fieldsetStyles,
134
131
  ...selectProps(rest),
135
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_StackView.StackWrap, {
132
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_View.default, {
136
133
  accessibilityRole: innerRole,
137
- space: space,
138
- direction: direction,
139
- tokens: stackTokens,
140
- gap: gap,
141
- ref: ref,
134
+ style: viewStyles,
142
135
  children: items.map((_ref2, index) => {
143
136
  let {
144
137
  label,
@@ -298,4 +291,18 @@ ButtonGroup.propTypes = {
298
291
  */
299
292
  copy: _propTypes.default.oneOf(['en', 'fr'])
300
293
  };
294
+ const staticStyles = _StyleSheet.default.create({
295
+ fieldsetBase: {
296
+ alignSelf: 'flex-start',
297
+ display: 'inline'
298
+ },
299
+ viewBase: {
300
+ flexWrap: 'wrap',
301
+ alignItems: 'center'
302
+ },
303
+ equalWidth: {
304
+ width: '100%',
305
+ flex: 1
306
+ }
307
+ });
301
308
  var _default = exports.default = ButtonGroup;
@@ -13,6 +13,12 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
13
13
  const textAndA11yText = exports.textAndA11yText = _airbnbPropTypes.default.childrenOf(_propTypes.default.oneOfType([_airbnbPropTypes.default.elementType(_A11yText.default), _propTypes.default.string]));
14
14
  const buttonPropTypes = {
15
15
  tokens: (0, _props.getTokensPropType)('Button'),
16
+ /**
17
+ * If true, the button will honor the align-items value from its parent flex container.
18
+ * If false, the button will always align to 'flex-start' in a flex container.
19
+ * Currently, `heightFull`'s default behaviour will expand to the full height of its parent's flex container. In an upcoming major release, the default behaviour will be changed to expand based on the content. To maintain the expected behaviour, you must explicitly set `heightFull: true`
20
+ */
21
+ heightFull: _propTypes.default.bool,
16
22
  /**
17
23
  * If true, prevents the button from being pressed, changes the cursor (on web) and accessibility
18
24
  * attributes to communicate this to the user, and applies `inactive: true` appearances from the theme
@@ -181,7 +181,9 @@ const staticStyles = _StyleSheet.default.create({
181
181
  },
182
182
  linkContainer: {
183
183
  flex: 1,
184
- display: 'flex'
184
+ display: 'flex',
185
+ alignItems: 'stretch',
186
+ justifyContent: 'flex-start'
185
187
  }
186
188
  });
187
189
  PressableCardBase.displayName = 'PressableCardBase';
@@ -358,11 +358,13 @@ const Carousel = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) => {
358
358
  copy,
359
359
  slideDuration = 0,
360
360
  transitionDuration = 0,
361
+ loopDuration = transitionDuration,
361
362
  autoPlay = false,
362
363
  enablePeeking = false,
363
364
  ...rest
364
365
  } = _ref3;
365
366
  let childrenArray = (0, _utils.unpackFragment)(children);
367
+ const isTransitioningRef = _react.default.useRef(false);
366
368
  const viewport = (0, _ViewportProvider.useViewport)();
367
369
  const totalItems = getTotalItems(enableDisplayMultipleItemsPerSlide, childrenArray, viewport);
368
370
  const autoPlayFeatureEnabled = autoPlay && slideDuration > 0 && transitionDuration > 0 && totalItems > 1;
@@ -433,9 +435,14 @@ const Carousel = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) => {
433
435
  setIsAnimating(true);
434
436
  }, [onAnimationStart]);
435
437
  const handleAnimationEnd = _react.default.useCallback(function () {
436
- if (typeof onAnimationEnd === 'function') onAnimationEnd(...arguments);
437
- setIsAnimating(false);
438
- }, [onAnimationEnd]);
438
+ var _ref4;
439
+ const result = (_ref4 = arguments.length - 1, _ref4 < 0 || arguments.length <= _ref4 ? undefined : arguments[_ref4]);
440
+ if (result?.finished) {
441
+ if (typeof onAnimationEnd === 'function') onAnimationEnd(...arguments);
442
+ setIsAnimating(false);
443
+ isTransitioningRef.current = false;
444
+ }
445
+ }, [onAnimationEnd, isTransitioningRef]);
439
446
  const updateOffset = _react.default.useCallback(() => {
440
447
  if (enablePeeking) {
441
448
  const {
@@ -477,6 +484,7 @@ const Carousel = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) => {
477
484
  }
478
485
  }, [pan, animatedX, heroPan, heroAnimatedX, enableHero, viewport, enablePeeking]);
479
486
  const animate = _react.default.useCallback((panToAnimate, toValue, toIndex) => {
487
+ const applicableTransitionDuration = isLastSlide && toIndex === 0 ? loopDuration : transitionDuration;
480
488
  const handleAnimationEndToIndex = function () {
481
489
  for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
482
490
  args[_key] = arguments[_key];
@@ -494,14 +502,14 @@ const Carousel = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) => {
494
502
  ...springConfig,
495
503
  toValue,
496
504
  useNativeDriver: false,
497
- duration: transitionDuration * 1000
505
+ duration: applicableTransitionDuration * 1000
498
506
  }).start(handleAnimationEndToIndex);
499
507
  } else if (enablePeeking || enableDisplayMultipleItemsPerSlide) {
500
508
  _Animated.default.timing(panToAnimate, {
501
509
  ...springConfig,
502
510
  toValue,
503
511
  useNativeDriver: false,
504
- duration: transitionDuration ? transitionDuration * 1000 : 1000
512
+ duration: applicableTransitionDuration ? applicableTransitionDuration * 1000 : 1000
505
513
  }).start(handleAnimationEndToIndex);
506
514
  } else {
507
515
  _Animated.default.spring(panToAnimate, {
@@ -510,7 +518,7 @@ const Carousel = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) => {
510
518
  useNativeDriver: false
511
519
  }).start(handleAnimationEndToIndex);
512
520
  }
513
- }, [springConfig, handleAnimationEnd, transitionDuration, isAutoPlayEnabled, enablePeeking, enableDisplayMultipleItemsPerSlide]);
521
+ }, [springConfig, handleAnimationEnd, transitionDuration, loopDuration, isLastSlide, isAutoPlayEnabled, enablePeeking, enableDisplayMultipleItemsPerSlide]);
514
522
  const stopAutoplay = _react.default.useCallback(() => {
515
523
  if (autoPlayRef?.current) {
516
524
  clearTimeout(autoPlayRef?.current);
@@ -542,6 +550,7 @@ const Carousel = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) => {
542
550
  }
543
551
  const index = activeIndexRef.current + calcDelta;
544
552
  if (skipChanges) {
553
+ isTransitioningRef.current = true;
545
554
  animate(pan, toValue, index);
546
555
  if (enableHero) {
547
556
  animate(heroPan, toValue, index);
@@ -557,6 +566,7 @@ const Carousel = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) => {
557
566
  y: 0
558
567
  };
559
568
  heroToValue.x = heroContainerLayoutRef.current.width * -1 * calcDelta;
569
+ isTransitioningRef.current = true;
560
570
  animate(pan, toValue, index);
561
571
  if (enableHero) {
562
572
  animate(heroPan, heroToValue, index);
@@ -612,29 +622,29 @@ const Carousel = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) => {
612
622
  heroContainerLayoutRef.current = heroContainerLayout;
613
623
  }, [heroContainerLayout]);
614
624
  _react.default.useEffect(() => {
615
- pan.x.addListener(_ref4 => {
625
+ pan.x.addListener(_ref5 => {
616
626
  let {
617
627
  value
618
- } = _ref4;
628
+ } = _ref5;
619
629
  animatedX.current = value;
620
630
  });
621
- pan.y.addListener(_ref5 => {
631
+ pan.y.addListener(_ref6 => {
622
632
  let {
623
633
  value
624
- } = _ref5;
634
+ } = _ref6;
625
635
  animatedY.current = value;
626
636
  });
627
637
  if (enableHero) {
628
- heroPan.x.addListener(_ref6 => {
638
+ heroPan.x.addListener(_ref7 => {
629
639
  let {
630
640
  value
631
- } = _ref6;
641
+ } = _ref7;
632
642
  heroAnimatedX.current = value;
633
643
  });
634
- heroPan.y.addListener(_ref7 => {
644
+ heroPan.y.addListener(_ref8 => {
635
645
  let {
636
646
  value
637
- } = _ref7;
647
+ } = _ref8;
638
648
  heroAnimatedY.current = value;
639
649
  });
640
650
  }
@@ -671,7 +681,7 @@ const Carousel = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) => {
671
681
  stopAutoplay();
672
682
  };
673
683
  }, [stopAutoplay]);
674
- const onContainerLayout = _ref8 => {
684
+ const onContainerLayout = _ref9 => {
675
685
  let {
676
686
  nativeEvent: {
677
687
  layout: {
@@ -681,7 +691,7 @@ const Carousel = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) => {
681
691
  height
682
692
  }
683
693
  }
684
- } = _ref8;
694
+ } = _ref9;
685
695
  return setContainerLayout(prevState => ({
686
696
  ...prevState,
687
697
  x,
@@ -690,7 +700,7 @@ const Carousel = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) => {
690
700
  height
691
701
  }));
692
702
  };
693
- const onHeroContainerLayout = _ref9 => {
703
+ const onHeroContainerLayout = _ref10 => {
694
704
  let {
695
705
  nativeEvent: {
696
706
  layout: {
@@ -700,7 +710,7 @@ const Carousel = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) => {
700
710
  height
701
711
  }
702
712
  }
703
- } = _ref9;
713
+ } = _ref10;
704
714
  return setHeroContainerLayout(prevState => ({
705
715
  ...prevState,
706
716
  x,
@@ -709,14 +719,14 @@ const Carousel = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) => {
709
719
  height
710
720
  }));
711
721
  };
712
- const onPreviousNextNavigationButtonLayout = _ref10 => {
722
+ const onPreviousNextNavigationButtonLayout = _ref11 => {
713
723
  let {
714
724
  nativeEvent: {
715
725
  layout: {
716
726
  width
717
727
  }
718
728
  }
719
- } = _ref10;
729
+ } = _ref11;
720
730
  return setPreviousNextNavigationButtonWidth(width);
721
731
  };
722
732
  const isSwipeAllowed = _react.default.useCallback(() => {
@@ -878,6 +888,11 @@ const Carousel = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) => {
878
888
  }
879
889
  setisCarouselPlaying(prevState => !prevState);
880
890
  }, [isCarouselPlaying, stopAutoplay, startAutoplay]);
891
+ const handleKeyDown = _react.default.useCallback(event => {
892
+ if (isTransitioningRef.current && event.key === 'Tab') {
893
+ event.preventDefault();
894
+ }
895
+ }, [isTransitioningRef]);
881
896
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_View.default, {
882
897
  style: selectRootContainerStyles(enableHero, viewport),
883
898
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_View.default, {
@@ -903,6 +918,9 @@ const Carousel = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) => {
903
918
  ref: ref,
904
919
  ...systemProps,
905
920
  ...containerProps,
921
+ ...(_Platform.default.OS === 'web' ? {
922
+ onKeyDown: handleKeyDown
923
+ } : {}),
906
924
  children: [isAutoPlayEnabled ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_View.default, {
907
925
  style: [staticStyles.animationControlButton, selectControlButtonPositionStyles({
908
926
  positionVariant: previousNextNavigationPosition,
@@ -957,10 +975,27 @@ const Carousel = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) => {
957
975
  // This is a known Voiceover bug: https://github.com/phetsims/a11y-research/issues/132
958
976
  accessibilityLiveRegion: accessibilityLiveRegion,
959
977
  children: childrenArray.map((element, index) => {
960
- const hidden = !isAnimating && index !== activeIndex;
978
+ let hidden = !isAnimating && index !== activeIndex;
979
+ if (enablePeeking && !isAnimating) {
980
+ if (enableDisplayMultipleItemsPerSlide) {
981
+ const maxItemsForSlide = getMaximumItemsForSlide(enableDisplayMultipleItemsPerSlide, viewport);
982
+ if (index >= activeIndex * maxItemsForSlide - 1 && index < activeIndex * maxItemsForSlide + maxItemsForSlide + 1) {
983
+ hidden = false;
984
+ } else {
985
+ hidden = true;
986
+ }
987
+ } else if (index >= activeIndex - 1 && index <= activeIndex + 1) {
988
+ hidden = false;
989
+ }
990
+ } else if (!enablePeeking && enableDisplayMultipleItemsPerSlide && !isAnimating) {
991
+ const maxItemsForSlide = getMaximumItemsForSlide(enableDisplayMultipleItemsPerSlide, viewport);
992
+ if (index >= activeIndex * maxItemsForSlide && index < activeIndex * maxItemsForSlide + maxItemsForSlide) {
993
+ hidden = false;
994
+ }
995
+ }
961
996
  const clonedElement = /*#__PURE__*/_react.default.cloneElement(element, {
962
997
  elementIndex: index,
963
- hidden: enablePeeking || enableDisplayMultipleItemsPerSlide ? false : hidden,
998
+ hidden,
964
999
  enablePeeking,
965
1000
  peekingProps: getPeekingProps(viewport),
966
1001
  enableDisplayMultipleItemsPerSlide,
@@ -1204,6 +1239,12 @@ Carousel.propTypes = {
1204
1239
  * - `autoPlay` and `slideDuration` are required to be set for this to work
1205
1240
  */
1206
1241
  transitionDuration: _propTypes.default.number,
1242
+ /**
1243
+ * Time it takes in seconds to transition from last slide to first slide
1244
+ * - Default value equals `transitionDuration`'s value
1245
+ * - `autoPlay` and `transitionDuration` are required to be set for this to work
1246
+ */
1247
+ loopDuration: _propTypes.default.number,
1207
1248
  /**
1208
1249
  * If set to `true`, the Carousel will show the previous and next slides
1209
1250
  * - Default value is `false`
@@ -104,13 +104,33 @@ const CarouselItem = /*#__PURE__*/_react.default.forwardRef((_ref2, ref) => {
104
104
  });
105
105
  const focusabilityProps = activeIndex === elementIndex || enablePeeking ? {} : _utils.a11yProps.nonFocusableProps;
106
106
  const handleFocus = _react.default.useCallback(event => {
107
- if (_Platform.default.OS === 'web' && elementIndex >= maximumItemsForSlide * (activeIndex + 1)) {
108
- goTo(activeIndex + 1);
107
+ if (_Platform.default.OS === 'web') {
108
+ if (enablePeeking) {
109
+ if (enableDisplayMultipleItemsPerSlide) {
110
+ const startIndex = maximumItemsForSlide * activeIndex;
111
+ const endIndex = startIndex + maximumItemsForSlide - 1;
112
+ if (elementIndex < startIndex) {
113
+ if (activeIndex - 1 < 0) {
114
+ goTo(0);
115
+ } else {
116
+ goTo(activeIndex - 1);
117
+ }
118
+ } else if (elementIndex > endIndex) {
119
+ goTo(activeIndex + 1);
120
+ }
121
+ } else if (elementIndex !== activeIndex) {
122
+ if (elementIndex > activeIndex) {
123
+ goTo(activeIndex + 1);
124
+ } else if (elementIndex < activeIndex) {
125
+ goTo(activeIndex - 1);
126
+ }
127
+ }
128
+ }
109
129
  }
110
130
  if (rest.onFocus) {
111
131
  rest.onFocus(event);
112
132
  }
113
- }, [elementIndex, activeIndex, goTo, maximumItemsForSlide, rest]);
133
+ }, [elementIndex, activeIndex, goTo, maximumItemsForSlide, rest, enablePeeking, enableDisplayMultipleItemsPerSlide]);
114
134
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_View.default, {
115
135
  style: selectContainerStyle({
116
136
  width,
@@ -43,17 +43,14 @@ const Icon = /*#__PURE__*/_react.default.forwardRef((_ref, ref) => {
43
43
  padding: themeTokens.padding
44
44
  };
45
45
  const getIconContentForMobile = () => {
46
- if (Object.keys(paddingStyles).length) {
47
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_View.default, {
48
- style: {
49
- backgroundColor: themeTokens.backgroundColor,
50
- borderRadius: themeTokens.borderRadius,
51
- ...paddingStyles
52
- },
53
- children: iconContent
54
- });
55
- }
56
- return iconContent;
46
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_View.default, {
47
+ style: {
48
+ backgroundColor: themeTokens.backgroundColor,
49
+ borderRadius: themeTokens.borderRadius,
50
+ ...paddingStyles
51
+ },
52
+ children: iconContent
53
+ });
57
54
  };
58
55
  return _Platform.default.OS === 'web' ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_View.default, {
59
56
  ref: ref