@telus-uds/components-base 3.12.1 → 3.13.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 (45) hide show
  1. package/CHANGELOG.md +29 -2
  2. package/lib/cjs/Button/ButtonDropdown.js +105 -12
  3. package/lib/cjs/Carousel/Carousel.js +26 -0
  4. package/lib/cjs/Carousel/CarouselContext.js +7 -4
  5. package/lib/cjs/Carousel/CarouselItem/CarouselItem.js +13 -2
  6. package/lib/cjs/Carousel/Constants.js +2 -1
  7. package/lib/cjs/ExpandCollapse/ExpandCollapse.js +3 -1
  8. package/lib/cjs/ExpandCollapseMini/ExpandCollapseMini.js +1 -1
  9. package/lib/cjs/ExpandCollapseMini/ExpandCollapseMiniControl.js +30 -6
  10. package/lib/cjs/FlexGrid/FlexGrid.js +54 -5
  11. package/lib/cjs/Icon/Icon.js +3 -1
  12. package/lib/cjs/InputLabel/InputLabel.js +1 -1
  13. package/lib/cjs/InputSupports/InputSupports.js +1 -1
  14. package/lib/cjs/Notification/Notification.js +27 -8
  15. package/lib/cjs/utils/props/inputSupportsProps.js +1 -1
  16. package/lib/esm/Button/ButtonDropdown.js +107 -14
  17. package/lib/esm/Carousel/Carousel.js +27 -1
  18. package/lib/esm/Carousel/CarouselContext.js +7 -4
  19. package/lib/esm/Carousel/CarouselItem/CarouselItem.js +13 -2
  20. package/lib/esm/Carousel/Constants.js +1 -0
  21. package/lib/esm/ExpandCollapse/ExpandCollapse.js +4 -2
  22. package/lib/esm/ExpandCollapseMini/ExpandCollapseMini.js +2 -2
  23. package/lib/esm/ExpandCollapseMini/ExpandCollapseMiniControl.js +30 -6
  24. package/lib/esm/FlexGrid/FlexGrid.js +55 -6
  25. package/lib/esm/Icon/Icon.js +3 -1
  26. package/lib/esm/InputLabel/InputLabel.js +1 -1
  27. package/lib/esm/InputSupports/InputSupports.js +1 -1
  28. package/lib/esm/Notification/Notification.js +27 -8
  29. package/lib/esm/utils/props/inputSupportsProps.js +1 -1
  30. package/lib/package.json +2 -2
  31. package/package.json +2 -2
  32. package/src/Button/ButtonDropdown.jsx +109 -16
  33. package/src/Carousel/Carousel.jsx +30 -0
  34. package/src/Carousel/CarouselContext.jsx +17 -4
  35. package/src/Carousel/CarouselItem/CarouselItem.jsx +17 -2
  36. package/src/Carousel/Constants.js +1 -0
  37. package/src/ExpandCollapse/ExpandCollapse.jsx +5 -2
  38. package/src/ExpandCollapseMini/ExpandCollapseMini.jsx +2 -2
  39. package/src/ExpandCollapseMini/ExpandCollapseMiniControl.jsx +39 -9
  40. package/src/FlexGrid/FlexGrid.jsx +62 -6
  41. package/src/Icon/Icon.jsx +3 -1
  42. package/src/InputLabel/InputLabel.jsx +1 -1
  43. package/src/InputSupports/InputSupports.jsx +1 -1
  44. package/src/Notification/Notification.jsx +58 -9
  45. package/src/utils/props/inputSupportsProps.js +1 -1
@@ -116,7 +116,7 @@ InputSupports.propTypes = {
116
116
  * 1. `tooltip` as a string - The content of the tooltip.
117
117
  * 2. `tooltip` as an object - Tooltip component props to be passed.
118
118
  */
119
- tooltip: PropTypes.oneOfType([tooltipPropTypes, PropTypes.string]),
119
+ tooltip: PropTypes.oneOfType([PropTypes.shape(tooltipPropTypes), PropTypes.string]),
120
120
  /**
121
121
  * Use to visually mark an input as valid or invalid.
122
122
  */
@@ -9,6 +9,7 @@ import useCopy from '../utils/useCopy';
9
9
  import dictionary from './dictionary';
10
10
  import { useViewport } from '../ViewportProvider';
11
11
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
+ const CONTENT_MAX_WIDTH = 'max';
12
13
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps]);
13
14
  const selectContainerStyles = tokens => ({
14
15
  ...tokens
@@ -63,13 +64,13 @@ const selectDismissButtonContainerStyles = _ref4 => {
63
64
  placeContent: 'start'
64
65
  };
65
66
  };
66
- const selectContentContainerStyle = (themeTokens, maxWidth, system, viewport) => ({
67
- maxWidth: system && viewport === 'xl' ? maxWidth : '100%',
67
+ const selectContentContainerStyle = (themeTokens, maxWidth, system, viewport, useContentMaxWidth) => ({
68
+ maxWidth: system && (useContentMaxWidth || viewport === 'xl') ? maxWidth : '100%',
68
69
  width: '100%',
69
70
  paddingRight: themeTokens?.containerPaddingRight,
70
71
  paddingLeft: themeTokens?.containerPaddingLeft
71
72
  });
72
- const getMediaQueryStyles = (themeTokens, themeOptions, maxWidth, mediaIdsRef, dismissible, viewport, system) => {
73
+ const getMediaQueryStyles = (themeTokens, themeOptions, maxWidth, mediaIdsRef, dismissible, viewport, system, useContentMaxWidth) => {
73
74
  const transformedSelectContainerStyles = Object.entries(themeTokens).reduce((acc, _ref5) => {
74
75
  let [vp, viewportTokens] = _ref5;
75
76
  acc[vp] = {
@@ -92,7 +93,7 @@ const getMediaQueryStyles = (themeTokens, themeOptions, maxWidth, mediaIdsRef, d
92
93
  const transformedSelectContentContainerStyles = Object.entries(themeTokens).reduce((acc, _ref6) => {
93
94
  let [vp, viewportTokens] = _ref6;
94
95
  acc[vp] = {
95
- ...selectContentContainerStyle(viewportTokens, maxWidth, system, vp),
96
+ ...selectContentContainerStyle(viewportTokens, maxWidth, system, vp, useContentMaxWidth),
96
97
  flexDirection: 'row',
97
98
  flexShrink: 1,
98
99
  justifyContent: 'space-between',
@@ -169,7 +170,7 @@ const getMediaQueryStyles = (themeTokens, themeOptions, maxWidth, mediaIdsRef, d
169
170
  selectDismissIconPropsStyles
170
171
  };
171
172
  };
172
- const getDefaultStyles = (themeTokens, themeOptions, maxWidth, dismissible, viewport, system) => ({
173
+ const getDefaultStyles = (themeTokens, themeOptions, maxWidth, dismissible, viewport, system, useContentMaxWidth) => ({
173
174
  containerStyles: {
174
175
  container: {
175
176
  flexDirection: 'column',
@@ -181,7 +182,7 @@ const getDefaultStyles = (themeTokens, themeOptions, maxWidth, dismissible, view
181
182
  flexDirection: 'row',
182
183
  flexShrink: 1,
183
184
  justifyContent: 'space-between',
184
- ...selectContentContainerStyle(themeTokens, maxWidth, system, viewport),
185
+ ...selectContentContainerStyle(themeTokens, maxWidth, system, viewport, useContentMaxWidth),
185
186
  ...(system && {
186
187
  alignSelf: 'center'
187
188
  })
@@ -280,6 +281,7 @@ const Notification = /*#__PURE__*/React.forwardRef((_ref7, ref) => {
280
281
  tokens,
281
282
  variant,
282
283
  onDismiss,
284
+ contentMinWidth,
283
285
  ...rest
284
286
  } = _ref7;
285
287
  const [isDismissed, setIsDismissed] = React.useState(false);
@@ -302,6 +304,7 @@ const Notification = /*#__PURE__*/React.forwardRef((_ref7, ref) => {
302
304
  system: isSystemEnabled,
303
305
  viewport
304
306
  });
307
+ const useContentMaxWidth = useResponsiveProp(contentMinWidth) === CONTENT_MAX_WIDTH;
305
308
  const maxWidth = useResponsiveProp(themeOptions?.contentMaxWidth, viewports.map.get(viewports.xl));
306
309
  const notificationComponentRef = React.useRef({
307
310
  containerStyles: {},
@@ -324,9 +327,9 @@ const Notification = /*#__PURE__*/React.forwardRef((_ref7, ref) => {
324
327
  selectDismissIconPropsIds: {}
325
328
  });
326
329
  if (enableMediaQueryStyleSheet) {
327
- notificationComponentRef.current = getMediaQueryStyles(themeTokens, themeOptions, maxWidth, mediaIdsRef, dismissible, viewport, isSystemEnabled);
330
+ notificationComponentRef.current = getMediaQueryStyles(themeTokens, themeOptions, maxWidth, mediaIdsRef, dismissible, viewport, isSystemEnabled, useContentMaxWidth);
328
331
  } else {
329
- notificationComponentRef.current = getDefaultStyles(themeTokens, themeOptions, maxWidth, dismissible, viewport, isSystemEnabled);
332
+ notificationComponentRef.current = getDefaultStyles(themeTokens, themeOptions, maxWidth, dismissible, viewport, isSystemEnabled, useContentMaxWidth);
330
333
  }
331
334
  if (isDismissed) {
332
335
  return null;
@@ -423,6 +426,22 @@ Notification.propTypes = {
423
426
  * Callback function called when the dismiss button is clicked
424
427
  */
425
428
  onDismiss: PropTypes.func,
429
+ /**
430
+ * The minimum width of the content in the Notification when using the system variant.
431
+ * This prop accepts responsive values for different viewports.
432
+ * - `xs`: 'max' | 'full'
433
+ * - `sm`: 'max' | 'full'
434
+ * - `md`: 'max' | 'full'
435
+ * - `lg`: 'max' | 'full'
436
+ * - `xl`: 'max' | 'full'
437
+ */
438
+ contentMinWidth: PropTypes.shape({
439
+ xl: PropTypes.oneOf(['max', 'full']),
440
+ lg: PropTypes.oneOf(['max', 'full']),
441
+ md: PropTypes.oneOf(['max', 'full']),
442
+ sm: PropTypes.oneOf(['max', 'full']),
443
+ xs: PropTypes.oneOf(['max', 'full'])
444
+ }),
426
445
  tokens: getTokensPropType('Notification'),
427
446
  variant: variantProp.propType
428
447
  };
@@ -37,7 +37,7 @@ export default {
37
37
  * 1. `tooltip` as a string - The content of the tooltip.
38
38
  * 2. `tooltip` as an object - Tooltip component props to be passed.
39
39
  */
40
- tooltip: PropTypes.oneOfType([tooltipPropTypes, PropTypes.string]),
40
+ tooltip: PropTypes.oneOfType([PropTypes.shape(tooltipPropTypes), PropTypes.string]),
41
41
  /**
42
42
  * Use to visually mark an input as valid or invalid.
43
43
  */
package/lib/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "@gorhom/portal": "^1.0.14",
13
13
  "@react-native-picker/picker": "^2.9.0",
14
14
  "@telus-uds/system-constants": "^3.0.0",
15
- "@telus-uds/system-theme-tokens": "^4.10.0",
15
+ "@telus-uds/system-theme-tokens": "^4.12.0",
16
16
  "airbnb-prop-types": "^2.16.0",
17
17
  "css-mediaquery": "^0.1.2",
18
18
  "expo-document-picker": "^13.0.1",
@@ -84,6 +84,6 @@
84
84
  "standard-engine": {
85
85
  "skip": true
86
86
  },
87
- "version": "3.12.1",
87
+ "version": "3.13.0",
88
88
  "types": "types/index.d.ts"
89
89
  }
package/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "@gorhom/portal": "^1.0.14",
13
13
  "@react-native-picker/picker": "^2.9.0",
14
14
  "@telus-uds/system-constants": "^3.0.0",
15
- "@telus-uds/system-theme-tokens": "^4.10.0",
15
+ "@telus-uds/system-theme-tokens": "^4.12.0",
16
16
  "airbnb-prop-types": "^2.16.0",
17
17
  "css-mediaquery": "^0.1.2",
18
18
  "expo-document-picker": "^13.0.1",
@@ -84,6 +84,6 @@
84
84
  "standard-engine": {
85
85
  "skip": true
86
86
  },
87
- "version": "3.12.1",
87
+ "version": "3.13.0",
88
88
  "types": "types/index.d.ts"
89
89
  }
@@ -1,9 +1,9 @@
1
1
  import React from 'react'
2
2
  import PropTypes from 'prop-types'
3
- import { Platform, Text, View } from 'react-native'
3
+ import { Platform, StyleSheet, Text, View } from 'react-native'
4
4
  import buttonPropTypes, { textAndA11yText } from './propTypes'
5
5
  import ButtonBase from './ButtonBase'
6
- import { useThemeTokensCallback } from '../ThemeProvider'
6
+ import { applyTextStyles, useThemeTokensCallback } from '../ThemeProvider'
7
7
  import {
8
8
  a11yProps,
9
9
  getTokensPropType,
@@ -13,9 +13,10 @@ import {
13
13
  useInputValue
14
14
  } from '../utils'
15
15
  import Icon from '../Icon'
16
- import { getStackedContent } from '../StackView'
17
16
  import { getPressHandlersWithArgs } from '../utils/pressability'
18
17
 
18
+ const FULL_WIDTH_STYLE = 'full'
19
+
19
20
  const selectIconTokens = ({
20
21
  icon,
21
22
  iconPosition,
@@ -50,6 +51,44 @@ const selectIconTokens = ({
50
51
  }
51
52
  })
52
53
 
54
+ const selectDescriptionTextStyles = (tokens) => ({
55
+ ...applyTextStyles({
56
+ fontName: tokens?.descriptionFontName,
57
+ fontSize: tokens?.descriptionFontSize,
58
+ fontWeight: tokens?.descriptionFontWeight,
59
+ fontColor: tokens?.color
60
+ }),
61
+ paddingBottom: tokens?.descriptionTextPaddingBottom
62
+ })
63
+
64
+ const selectLeadIconTokens = (tokens) => ({
65
+ color: tokens?.leadIconColor,
66
+ backgroundColor: tokens?.leadIconBackgroundColor,
67
+ size: tokens?.leadIconSize,
68
+ borderRadius: tokens?.leadIconBorderRadius,
69
+ padding: tokens?.leadIconPadding
70
+ })
71
+
72
+ const selectLeadIconContainerStyles = (tokens) => ({
73
+ paddingTop: tokens?.leadIconContainerPaddingTop,
74
+ paddingBottom: tokens?.leadIconContainerPaddingBottom,
75
+ paddingLeft: tokens?.leadIconContainerPaddingLeft,
76
+ paddingRight: tokens?.leadIconContainerPaddingRight
77
+ })
78
+
79
+ const selectTextContainerStyles = (tokens) => ({
80
+ paddingLeft: tokens?.textPaddingLeft,
81
+ paddingRight: tokens?.textPaddingRight
82
+ })
83
+
84
+ const selectStackedContentStyles = (tokens, iconPosition, isFullWidth) => ({
85
+ ...staticStyles.stackedContent,
86
+ gap: tokens?.iconSpace,
87
+ flexDirection: iconPosition === 'left' ? 'row' : 'row-reverse',
88
+ ...(Platform.OS === 'web' && { flex: 1 }),
89
+ ...(isFullWidth && { justifyContent: 'space-between', flex: 1 })
90
+ })
91
+
53
92
  const ButtonDropdown = React.forwardRef(
54
93
  (
55
94
  {
@@ -63,10 +102,14 @@ const ButtonDropdown = React.forwardRef(
63
102
  readOnly = false,
64
103
  children = null,
65
104
  accessibilityRole = 'radio',
105
+ description,
106
+ singleOption,
66
107
  ...props
67
108
  },
68
109
  ref
69
110
  ) => {
111
+ const isFullWidth = variant?.width === FULL_WIDTH_STYLE
112
+
70
113
  const { currentValue: isOpen, setValue: setIsOpen } = useInputValue(
71
114
  {
72
115
  value,
@@ -85,7 +128,11 @@ const ButtonDropdown = React.forwardRef(
85
128
 
86
129
  const getTokens = useThemeTokensCallback('ButtonDropdown', tokens, extraState)
87
130
 
88
- const getButtonTokens = (buttonState) => selectTokens('Button', getTokens(buttonState))
131
+ const getButtonTokens = (buttonState) => ({
132
+ ...selectTokens('Button', getTokens(buttonState)),
133
+ iconSpace: props?.icon ? getTokens(buttonState)?.iconSpace : 0,
134
+ ...(isFullWidth && { width: 'full' })
135
+ })
89
136
 
90
137
  // Pass an object of relevant component state as first argument for any passed-in press handlers
91
138
  const pressHandlers = getPressHandlersWithArgs(props, [{ label, open: isOpen }])
@@ -103,7 +150,7 @@ const ButtonDropdown = React.forwardRef(
103
150
  {...pressHandlers}
104
151
  onPress={handlePress}
105
152
  tokens={getButtonTokens}
106
- inactive={inactive}
153
+ inactive={singleOption || inactive}
107
154
  icon={() => null}
108
155
  accessibilityRole={accessibilityRole}
109
156
  {...props}
@@ -116,31 +163,50 @@ const ButtonDropdown = React.forwardRef(
116
163
  // - Token sets: https://github.com/telus/universal-design-system/issues/782
117
164
 
118
165
  const itemTokens = getTokens(buttonState)
166
+ const leadIcon = itemTokens?.leadIcon
119
167
 
120
168
  const {
121
169
  iconTokens,
122
170
  iconPosition,
123
- iconSpace,
124
171
  iconWrapperStyle,
125
172
  icon: IconComponent
126
173
  } = selectIconTokens(itemTokens)
127
174
 
128
- const iconContent = IconComponent ? (
129
- <View style={iconWrapperStyle}>
130
- <Icon icon={IconComponent} tokens={iconTokens} />
131
- </View>
132
- ) : null
175
+ const iconContent =
176
+ IconComponent && !singleOption ? (
177
+ <View style={iconWrapperStyle}>
178
+ <Icon icon={IconComponent} tokens={iconTokens} />
179
+ </View>
180
+ ) : null
133
181
 
134
182
  const childrenContent = () =>
135
183
  typeof children === 'function'
136
184
  ? children({ ...resolvePressableState(buttonState, extraState), textStyles })
137
185
  : children
138
186
 
139
- const content = children ? childrenContent() : <Text style={textStyles}>{label}</Text>
187
+ const content = children ? (
188
+ childrenContent()
189
+ ) : (
190
+ <View style={staticStyles.contentContainer}>
191
+ {leadIcon && (
192
+ <View style={selectLeadIconContainerStyles(itemTokens)}>
193
+ <Icon icon={leadIcon} tokens={selectLeadIconTokens(itemTokens)} />
194
+ </View>
195
+ )}
196
+ <View style={[staticStyles.textContainer, selectTextContainerStyles(itemTokens)]}>
197
+ <Text style={textStyles}>{label}</Text>
198
+ {description && (
199
+ <Text style={selectDescriptionTextStyles(itemTokens)}>{description}</Text>
200
+ )}
201
+ </View>
202
+ </View>
203
+ )
140
204
 
141
- return getStackedContent(
142
- iconPosition === 'left' ? [iconContent, content] : [content, iconContent],
143
- { space: iconSpace, direction: 'row' }
205
+ return (
206
+ <View style={selectStackedContentStyles(itemTokens, iconPosition, isFullWidth)}>
207
+ {iconContent}
208
+ {content}
209
+ </View>
144
210
  )
145
211
  }}
146
212
  </ButtonBase>
@@ -176,7 +242,34 @@ ButtonDropdown.propTypes = {
176
242
  /**
177
243
  * By default, `ButtonDropdown` is treated by accessibility tools as a radio button.
178
244
  */
179
- accessibilityRole: PropTypes.string
245
+ accessibilityRole: PropTypes.string,
246
+ /**
247
+ * The description of ButtonDropdown.
248
+ */
249
+ description: PropTypes.string,
250
+ /**
251
+ * Use this prop to render the ButtonDropdown as display only without any interaction when there is only one option.
252
+ */
253
+ singleOption: PropTypes.bool
180
254
  }
181
255
 
256
+ const staticStyles = StyleSheet.create({
257
+ textContainer: {
258
+ alignItems: 'flex-start',
259
+ flexShrink: 1,
260
+ ...(Platform.OS === 'web' && { flex: 1 })
261
+ },
262
+ contentContainer: {
263
+ flexDirection: 'row',
264
+ alignContent: 'center',
265
+ alignItems: 'center',
266
+ ...(Platform.OS === 'web' && { flex: 1 }),
267
+ flexShrink: 1
268
+ },
269
+ stackedContent: {
270
+ flexDirection: 'row',
271
+ alignItems: 'center'
272
+ }
273
+ })
274
+
182
275
  export default ButtonDropdown
@@ -27,6 +27,7 @@ import CarouselTabsPanelItem from './CarouselTabs/CarouselTabsPanelItem'
27
27
  import dictionary from './dictionary'
28
28
  import Box from '../Box'
29
29
  import {
30
+ ITEMS_PER_VIEWPORT_XS_SM,
30
31
  ITEMS_PER_VIEWPORT_MD,
31
32
  ITEMS_PER_VIEWPORT_LG_XL,
32
33
  DEFAULT_POSITION_OFFSET,
@@ -211,6 +212,31 @@ const getTotalItems = (enableDisplayMultipleItemsPerSlide, childrenArray, viewpo
211
212
  return childrenArray.length
212
213
  }
213
214
 
215
+ /**
216
+ * Determines the maximum number of items that can be displayed per slide based on viewport
217
+ *
218
+ * @param {boolean} enableDisplayMultipleItemsPerSlide - Flag indicating whether multiple items per slide is enabled
219
+ * @param {string} viewport - The current viewport size ('xs', 'sm', 'md', 'lg', 'xl')
220
+ * @returns {number} The maximum number of items that can be displayed per slide
221
+ */
222
+ const getMaximumItemsForSlide = (enableDisplayMultipleItemsPerSlide, viewport) => {
223
+ if (enableDisplayMultipleItemsPerSlide) {
224
+ switch (viewport) {
225
+ case 'xs':
226
+ case 'sm':
227
+ return ITEMS_PER_VIEWPORT_XS_SM
228
+ case 'md':
229
+ return ITEMS_PER_VIEWPORT_MD
230
+ case 'lg':
231
+ case 'xl':
232
+ return ITEMS_PER_VIEWPORT_LG_XL
233
+ default:
234
+ return ITEMS_PER_VIEWPORT_XS_SM
235
+ }
236
+ }
237
+ return ITEMS_PER_VIEWPORT_XS_SM
238
+ }
239
+
214
240
  const selectRootContainerStyles = (enableHero, viewport) => {
215
241
  if (enableHero && viewport === 'xl' && Platform.OS === 'web') {
216
242
  return {
@@ -993,6 +1019,10 @@ const Carousel = React.forwardRef(
993
1019
  firstFocusRef={firstFocusRef}
994
1020
  refocus={refocus}
995
1021
  width={containerLayout.width}
1022
+ maximumItemsForSlide={getMaximumItemsForSlide(
1023
+ enableDisplayMultipleItemsPerSlide,
1024
+ viewport
1025
+ )}
996
1026
  >
997
1027
  <View
998
1028
  style={[
@@ -13,7 +13,8 @@ const CarouselProvider = ({
13
13
  refocus = false,
14
14
  themeTokens,
15
15
  totalItems,
16
- width
16
+ width,
17
+ maximumItemsForSlide
17
18
  }) => {
18
19
  const value = React.useMemo(
19
20
  () => ({
@@ -24,9 +25,20 @@ const CarouselProvider = ({
24
25
  refocus,
25
26
  themeTokens,
26
27
  totalItems,
27
- width
28
+ width,
29
+ maximumItemsForSlide
28
30
  }),
29
- [activeIndex, goTo, getCopyWithPlaceholders, itemLabel, refocus, totalItems, themeTokens, width]
31
+ [
32
+ activeIndex,
33
+ goTo,
34
+ getCopyWithPlaceholders,
35
+ itemLabel,
36
+ refocus,
37
+ totalItems,
38
+ themeTokens,
39
+ width,
40
+ maximumItemsForSlide
41
+ ]
30
42
  )
31
43
  return <CarouselContext.Provider value={value}>{children}</CarouselContext.Provider>
32
44
  }
@@ -48,7 +60,8 @@ CarouselProvider.propTypes = {
48
60
  refocus: PropTypes.bool,
49
61
  themeTokens: getTokensPropType('Carousel'),
50
62
  totalItems: PropTypes.number.isRequired,
51
- width: PropTypes.number.isRequired
63
+ width: PropTypes.number.isRequired,
64
+ maximumItemsForSlide: PropTypes.number
52
65
  }
53
66
 
54
67
  export { CarouselProvider, useCarousel }
@@ -102,14 +102,28 @@ const CarouselItem = React.forwardRef(
102
102
  },
103
103
  ref
104
104
  ) => {
105
- const { width, activeIndex } = useCarousel()
105
+ const { width, activeIndex, goTo, maximumItemsForSlide } = useCarousel()
106
106
 
107
107
  const selectedProps = selectProps({
108
108
  ...rest,
109
109
  ...getA11yPropsFromHtmlTag(tag, rest.accessibilityRole)
110
110
  })
111
111
 
112
- const focusabilityProps = activeIndex === elementIndex ? {} : a11yProps.nonFocusableProps
112
+ const focusabilityProps =
113
+ activeIndex === elementIndex || enablePeeking ? {} : a11yProps.nonFocusableProps
114
+
115
+ const handleFocus = React.useCallback(
116
+ (event) => {
117
+ if (Platform.OS === 'web' && elementIndex >= maximumItemsForSlide * (activeIndex + 1)) {
118
+ goTo(activeIndex + 1)
119
+ }
120
+
121
+ if (rest.onFocus) {
122
+ rest.onFocus(event)
123
+ }
124
+ },
125
+ [elementIndex, activeIndex, goTo, maximumItemsForSlide, rest]
126
+ )
113
127
 
114
128
  return (
115
129
  <View
@@ -125,6 +139,7 @@ const CarouselItem = React.forwardRef(
125
139
  {...selectedProps}
126
140
  {...focusabilityProps}
127
141
  ref={ref}
142
+ onFocus={handleFocus}
128
143
  >
129
144
  {children}
130
145
  </View>
@@ -1,3 +1,4 @@
1
+ export const ITEMS_PER_VIEWPORT_XS_SM = 1
1
2
  export const ITEMS_PER_VIEWPORT_MD = 2
2
3
  export const ITEMS_PER_VIEWPORT_LG_XL = 3
3
4
  export const GAP_BETWEEN_ITEMS = 16
@@ -10,7 +10,8 @@ import {
10
10
  useMultipleInputValues,
11
11
  variantProp,
12
12
  viewProps,
13
- contentfulProps
13
+ contentfulProps,
14
+ useUniqueId
14
15
  } from '../utils'
15
16
 
16
17
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([
@@ -36,6 +37,8 @@ function selectBorderStyles(tokens) {
36
37
  */
37
38
  const ExpandCollapse = React.forwardRef(
38
39
  ({ children, tokens, variant, maxOpen, open, initialOpen, onChange, dataSet, ...rest }, ref) => {
40
+ const instanceId = useUniqueId('ExpandCollapse')
41
+
39
42
  const {
40
43
  currentValues: openIds,
41
44
  toggleOneValue: onToggle,
@@ -54,7 +57,7 @@ const ExpandCollapse = React.forwardRef(
54
57
  <View style={staticStyles.container} ref={ref} {...selectProps(rest)} dataSet={dataSet}>
55
58
  <View style={selectBorderStyles(themeTokens)}>
56
59
  {typeof children === 'function'
57
- ? children({ openIds, onToggle, resetValues, setValues, unsetValues })
60
+ ? children({ openIds, onToggle, resetValues, setValues, unsetValues, instanceId })
58
61
  : children}
59
62
  </View>
60
63
  </View>
@@ -1,7 +1,7 @@
1
1
  import React from 'react'
2
2
  import PropTypes from 'prop-types'
3
3
  import ExpandCollapse from '../ExpandCollapse'
4
- import { getTokensPropType, selectSystemProps, contentfulProps } from '../utils'
4
+ import { getTokensPropType, selectSystemProps, contentfulProps, useUniqueId } from '../utils'
5
5
  import { variantProp } from '../utils/props'
6
6
  import ExpandCollapseMiniControl from './ExpandCollapseMiniControl'
7
7
 
@@ -12,7 +12,7 @@ const ExpandCollapseMini = React.forwardRef(
12
12
  { children, onToggle = () => {}, tokens = {}, nativeID, initialOpen = false, dataSet, ...rest },
13
13
  ref
14
14
  ) => {
15
- const expandCollapeMiniPanelId = 'ExpandCollapseMiniPanel'
15
+ const expandCollapeMiniPanelId = useUniqueId('ExpandCollapseMiniPanel')
16
16
  const handleChange = (openPanels, event) => {
17
17
  if (typeof onToggle === 'function') {
18
18
  const isOpen = openPanels.length > 0
@@ -42,10 +42,15 @@ const ExpandCollapseMiniControl = React.forwardRef(
42
42
  pressed
43
43
  }
44
44
  )
45
- const { size, icon } = useThemeTokens('ExpandCollapseMiniControl', tokens, variant, {
46
- expanded,
47
- focus
48
- })
45
+ const { fontSize, lineHeight, iconSize, icon } = useThemeTokens(
46
+ 'ExpandCollapseMiniControl',
47
+ tokens,
48
+ variant,
49
+ {
50
+ expanded,
51
+ focus
52
+ }
53
+ )
49
54
 
50
55
  // Choose hover styles when any part of Control is hoverred
51
56
  const appearance = { ...variant, hover }
@@ -54,16 +59,39 @@ const ExpandCollapseMiniControl = React.forwardRef(
54
59
  const { hover: linkHover } = linkState || {}
55
60
  const isHovered = hover || linkHover
56
61
 
62
+ const iconBaselineOffset = 0
63
+ const hoverTranslateY = 4
64
+
65
+ // Calculate baseline alignment to vertically center icon with text
66
+ // This combines font and icon metrics with adjustments for visual balance
67
+ const fontBaseline = fontSize / hoverTranslateY // Quarter of font size - adjusts for text's visual center point
68
+ const iconBaseline = iconSize / hoverTranslateY // Quarter of icon size - adjusts for icon's visual center point
69
+ const staticOffset = hoverTranslateY // Fixed downward adjustment to fine-tune vertical alignment
70
+ const sizeCompensation = -Math.abs(iconSize - fontSize) // Compensates when icon and text sizes differ significantly
71
+
72
+ const baselineAlignment = fontBaseline + iconBaseline - staticOffset + sizeCompensation
73
+
57
74
  if (Platform.OS !== 'web') {
58
- return { iconTranslateY: -1 }
75
+ // For native platforms, use baseline alignment with optional offset
76
+ return { iconTranslateY: baselineAlignment + iconBaselineOffset }
59
77
  }
60
78
 
61
79
  if (isHovered) {
62
- // Include vertical icon animation on hover alongside built-in Link theme, the size is size4
63
- return { iconTranslateY: (expanded ? -1 : 1) * size }
80
+ // Apply animation offset to the baseline-aligned position
81
+ // When expanded: move icon UP (1.3 the hover distance for clear movement)
82
+ // When collapsed: move icon DOWN (single hover distance)
83
+ const hoverMovementDistance = 1.3
84
+ const animationOffset = expanded
85
+ ? -(hoverTranslateY * hoverMovementDistance)
86
+ : hoverTranslateY
87
+
88
+ return {
89
+ iconTranslateY: baselineAlignment + iconBaselineOffset + animationOffset
90
+ }
64
91
  }
65
92
 
66
- return {}
93
+ // Default state uses baseline alignment with optional offset
94
+ return { iconTranslateY: baselineAlignment + iconBaselineOffset }
67
95
  }
68
96
 
69
97
  return (
@@ -73,7 +101,9 @@ const ExpandCollapseMiniControl = React.forwardRef(
73
101
  iconPosition={iconPosition}
74
102
  tokens={(linkState) => ({
75
103
  ...linkTokens,
76
- ...getTokens(linkState)
104
+ ...getTokens(linkState),
105
+ iconSize,
106
+ blockLineHeight: lineHeight
77
107
  })}
78
108
  ref={ref}
79
109
  {...presentationOnly}