@telus-uds/components-base 3.23.0 → 3.24.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 (51) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/lib/cjs/Card/CardBase.js +97 -17
  3. package/lib/cjs/Card/PressableCardBase.js +12 -8
  4. package/lib/cjs/HorizontalScroll/HorizontalScroll.js +5 -2
  5. package/lib/cjs/Icon/Icon.js +3 -0
  6. package/lib/cjs/Listbox/GroupControl.js +12 -6
  7. package/lib/cjs/Listbox/Listbox.js +41 -7
  8. package/lib/cjs/Listbox/ListboxGroup.js +139 -8
  9. package/lib/cjs/Listbox/ListboxOverlay.js +10 -5
  10. package/lib/cjs/Listbox/SecondLevelHeader.js +201 -0
  11. package/lib/cjs/Listbox/dictionary.js +14 -0
  12. package/lib/cjs/Shortcuts/Shortcuts.js +169 -0
  13. package/lib/cjs/Shortcuts/ShortcutsItem.js +280 -0
  14. package/lib/cjs/Shortcuts/index.js +16 -0
  15. package/lib/cjs/Tooltip/Tooltip.native.js +2 -0
  16. package/lib/cjs/index.js +15 -0
  17. package/lib/esm/Card/CardBase.js +97 -17
  18. package/lib/esm/Card/PressableCardBase.js +10 -8
  19. package/lib/esm/HorizontalScroll/HorizontalScroll.js +6 -3
  20. package/lib/esm/Icon/Icon.js +3 -0
  21. package/lib/esm/Listbox/GroupControl.js +12 -6
  22. package/lib/esm/Listbox/Listbox.js +41 -7
  23. package/lib/esm/Listbox/ListboxGroup.js +141 -10
  24. package/lib/esm/Listbox/ListboxOverlay.js +10 -5
  25. package/lib/esm/Listbox/SecondLevelHeader.js +194 -0
  26. package/lib/esm/Listbox/dictionary.js +8 -0
  27. package/lib/esm/Shortcuts/Shortcuts.js +160 -0
  28. package/lib/esm/Shortcuts/ShortcutsItem.js +273 -0
  29. package/lib/esm/Shortcuts/index.js +3 -0
  30. package/lib/esm/Tooltip/Tooltip.native.js +2 -0
  31. package/lib/esm/index.js +1 -0
  32. package/lib/package.json +2 -2
  33. package/package.json +2 -2
  34. package/src/Card/CardBase.jsx +113 -14
  35. package/src/Card/PressableCardBase.jsx +17 -5
  36. package/src/HorizontalScroll/HorizontalScroll.jsx +6 -3
  37. package/src/Icon/Icon.jsx +3 -0
  38. package/src/Listbox/GroupControl.jsx +41 -33
  39. package/src/Listbox/Listbox.jsx +41 -2
  40. package/src/Listbox/ListboxGroup.jsx +158 -26
  41. package/src/Listbox/ListboxOverlay.jsx +18 -5
  42. package/src/Listbox/SecondLevelHeader.jsx +182 -0
  43. package/src/Listbox/dictionary.js +8 -0
  44. package/src/Shortcuts/Shortcuts.jsx +174 -0
  45. package/src/Shortcuts/ShortcutsItem.jsx +297 -0
  46. package/src/Shortcuts/index.js +4 -0
  47. package/src/Tooltip/Tooltip.native.jsx +2 -1
  48. package/src/index.js +1 -0
  49. package/types/Listbox.d.ts +24 -0
  50. package/types/Shortcuts.d.ts +136 -0
  51. package/types/index.d.ts +12 -0
@@ -2,21 +2,36 @@
2
2
  import React from 'react'
3
3
  import PropTypes from 'prop-types'
4
4
  import { View, StyleSheet, Platform } from 'react-native'
5
- import { withLinkRouter } from '../utils'
5
+ import { withLinkRouter, variantProp, copyPropTypes } from '../utils'
6
+ import { useThemeTokens } from '../ThemeProvider'
6
7
  import ExpandCollapse from '../ExpandCollapse'
7
8
  import ListboxItem from './ListboxItem'
8
9
  import { useListboxContext } from './ListboxContext'
9
10
  import GroupControl from './GroupControl'
11
+ import SecondLevelHeader from './SecondLevelHeader'
12
+ import defaultDictionary from './dictionary'
10
13
 
11
14
  const styles = StyleSheet.create({
12
15
  groupWrapper: {
13
16
  margin: 0,
14
17
  padding: 0,
15
- overflow: 'hidden'
18
+ overflow: 'visible'
16
19
  },
17
20
  list: {
18
21
  margin: 0,
19
22
  padding: 0
23
+ },
24
+ secondLevelContainer: {
25
+ margin: 0,
26
+ padding: 0,
27
+ width: '100%',
28
+ display: 'flex',
29
+ flexDirection: 'column'
30
+ },
31
+ secondLevelList: {
32
+ margin: 0,
33
+ padding: 0,
34
+ width: '100%'
20
35
  }
21
36
  })
22
37
 
@@ -27,6 +42,10 @@ const getAccessibilityRole = () =>
27
42
  web: 'listitem'
28
43
  })
29
44
 
45
+ const selectSecondLevelContainerStyles = ({ secondLevelHeaderBackgroundColor }) => ({
46
+ backgroundColor: secondLevelHeaderBackgroundColor
47
+ })
48
+
30
49
  const ListboxGroup = React.forwardRef(
31
50
  (
32
51
  {
@@ -38,11 +57,86 @@ const ListboxGroup = React.forwardRef(
38
57
  expandProps,
39
58
  onLastItemBlur,
40
59
  nextItemRef,
41
- prevItemRef
60
+ prevItemRef,
61
+ copy = 'en',
62
+ dictionary = defaultDictionary,
63
+ variant = {},
64
+ tokens = {},
65
+ onClose
42
66
  },
43
67
  ref
44
68
  ) => {
45
- const { selectedId } = useListboxContext()
69
+ const { selectedId, activeSecondLevelGroup, setActiveSecondLevelGroup } = useListboxContext()
70
+ const [secondLevelOpen, setSecondLevelOpen] = React.useState(false)
71
+ const isSecondLevel = variant?.secondLevel === true
72
+ const listboxTokens = useThemeTokens('Listbox', variant, tokens)
73
+ const groupId = id ?? label
74
+
75
+ const handleGroupClick = React.useCallback(() => {
76
+ if (isSecondLevel) {
77
+ setSecondLevelOpen(true)
78
+ setActiveSecondLevelGroup(groupId)
79
+ }
80
+ }, [isSecondLevel, groupId, setActiveSecondLevelGroup])
81
+
82
+ const handleBackClick = React.useCallback(() => {
83
+ setSecondLevelOpen(false)
84
+ setActiveSecondLevelGroup(null)
85
+ }, [setActiveSecondLevelGroup])
86
+
87
+ const handleCloseClick = React.useCallback(() => {
88
+ setSecondLevelOpen(false)
89
+ setActiveSecondLevelGroup(null)
90
+ if (onClose) {
91
+ onClose()
92
+ }
93
+ }, [setActiveSecondLevelGroup, onClose])
94
+
95
+ if (isSecondLevel && activeSecondLevelGroup && activeSecondLevelGroup !== groupId) {
96
+ return null
97
+ }
98
+
99
+ if (isSecondLevel && secondLevelOpen) {
100
+ return (
101
+ <View
102
+ style={[styles.secondLevelContainer, selectSecondLevelContainerStyles(listboxTokens)]}
103
+ >
104
+ <SecondLevelHeader
105
+ label={label}
106
+ onBack={handleBackClick}
107
+ onClose={handleCloseClick}
108
+ copy={copy}
109
+ dictionary={dictionary}
110
+ variant={variant}
111
+ tokens={tokens}
112
+ />
113
+ <View style={styles.secondLevelList}>
114
+ {items &&
115
+ items.map((item, index) => {
116
+ return (
117
+ <ListboxItem
118
+ key={item.label}
119
+ id={item.id ?? item.label}
120
+ {...item}
121
+ selected={
122
+ (item.id && item.id === selectedId) ||
123
+ (item.label && item.label === selectedId)
124
+ }
125
+ isChild={false}
126
+ LinkRouter={LinkRouter}
127
+ linkRouterProps={linkRouterProps}
128
+ variant={variant}
129
+ tokens={tokens}
130
+ {...(index === 0 && { prevItemRef })}
131
+ {...(index === items.length - 1 && { nextItemRef })}
132
+ {...(index === items.length - 1 && { onBlur: onLastItemBlur })}
133
+ />
134
+ )
135
+ })}
136
+ </View>
137
+ </View>
138
+ )
139
+ }
46
140
 
47
141
  // TODO: implement keyboard navigation via refs for grouped items separately here
48
142
  return (
@@ -63,9 +157,11 @@ const ListboxGroup = React.forwardRef(
63
157
  // TODO refactor
64
158
  // eslint-disable-next-line react/no-unstable-nested-components
65
159
  control={(controlProps) => (
66
- <GroupControl id={id ?? label} {...controlProps} label={label} />
160
+ <GroupControl id={id ?? label} {...controlProps} label={label} variant={variant} />
67
161
  )}
68
162
  {...expandProps}
163
+ {...(isSecondLevel && { open: false })}
164
+ {...(isSecondLevel && { onPress: handleGroupClick })}
69
165
  tokens={{
70
166
  contentPaddingLeft: 0,
71
167
  contentPaddingRight: 0,
@@ -79,26 +175,31 @@ const ListboxGroup = React.forwardRef(
79
175
  }}
80
176
  controlRef={ref}
81
177
  >
82
- <View style={styles.list}>
83
- {items.map((item, index) => {
84
- return (
85
- <ListboxItem
86
- key={item.label}
87
- id={item.id ?? item.label}
88
- {...item}
89
- selected={
90
- (item.id && item.id === selectedId) || (item.label && item.label === selectedId)
91
- }
92
- isChild
93
- LinkRouter={LinkRouter}
94
- linkRouterProps={linkRouterProps}
95
- {...(index === 0 && { prevItemRef })}
96
- {...(index === items.length - 1 && { nextItemRef })}
97
- {...(index === items.length - 1 && { onBlur: onLastItemBlur })}
98
- />
99
- )
100
- })}
101
- </View>
178
+ {!isSecondLevel && (
179
+ <View style={styles.list}>
180
+ {items.map((item, index) => {
181
+ return (
182
+ <ListboxItem
183
+ key={item.label}
184
+ id={item.id ?? item.label}
185
+ {...item}
186
+ selected={
187
+ (item.id && item.id === selectedId) ||
188
+ (item.label && item.label === selectedId)
189
+ }
190
+ isChild
191
+ LinkRouter={LinkRouter}
192
+ linkRouterProps={linkRouterProps}
193
+ variant={variant}
194
+ tokens={tokens}
195
+ {...(index === 0 && { prevItemRef })}
196
+ {...(index === items.length - 1 && { nextItemRef })}
197
+ {...(index === items.length - 1 && { onBlur: onLastItemBlur })}
198
+ />
199
+ )
200
+ })}
201
+ </View>
202
+ )}
102
203
  </ExpandCollapse.Panel>
103
204
  </View>
104
205
  )
@@ -108,6 +209,10 @@ ListboxGroup.displayName = 'ListboxGroup'
108
209
 
109
210
  ListboxGroup.propTypes = {
110
211
  ...withLinkRouter.propTypes,
212
+ /**
213
+ * Unique identifier for the group
214
+ */
215
+ id: PropTypes.string,
111
216
  label: PropTypes.string,
112
217
  items: PropTypes.arrayOf(
113
218
  PropTypes.shape({
@@ -122,7 +227,34 @@ ListboxGroup.propTypes = {
122
227
  /**
123
228
  * Use this callback to redirect the focus after it leaves the last item of the group.
124
229
  */
125
- onLastItemBlur: PropTypes.func
230
+ onLastItemBlur: PropTypes.func,
231
+ /**
232
+ * Select English or French copy
233
+ */
234
+ copy: copyPropTypes,
235
+ /**
236
+ * Override the default dictionary, by passing the complete dictionary object for `en` and `fr`
237
+ */
238
+ dictionary: PropTypes.shape({
239
+ en: PropTypes.shape({
240
+ closeMenu: PropTypes.string.isRequired
241
+ }),
242
+ fr: PropTypes.shape({
243
+ closeMenu: PropTypes.string.isRequired
244
+ })
245
+ }),
246
+ /**
247
+ * Variant configuration for secondLevel behavior
248
+ */
249
+ variant: variantProp.propType,
250
+ /**
251
+ * Custom tokens
252
+ */
253
+ tokens: PropTypes.object,
254
+ /**
255
+ * Callback when the menu is closed
256
+ */
257
+ onClose: PropTypes.func
126
258
  }
127
259
 
128
260
  export default ListboxGroup
@@ -23,10 +23,21 @@ const paddingHorizontal = 0
23
23
 
24
24
  const DropdownOverlay = React.forwardRef(
25
25
  (
26
- { children, isReady = false, overlaidPosition, maxWidth, minWidth, onLayout, tokens, testID },
26
+ {
27
+ children,
28
+ isReady = false,
29
+ overlaidPosition,
30
+ maxWidth,
31
+ minWidth,
32
+ onLayout,
33
+ tokens,
34
+ testID,
35
+ variant
36
+ },
37
+
27
38
  ref
28
39
  ) => {
29
- const systemTokens = useThemeTokens('Listbox', {}, {})
40
+ const systemTokens = useThemeTokens('Listbox', variant, tokens)
30
41
 
31
42
  return (
32
43
  <View
@@ -43,11 +54,12 @@ const DropdownOverlay = React.forwardRef(
43
54
  <Card
44
55
  tokens={{
45
56
  shadow: systemTokens.shadow,
57
+ borderRadius: systemTokens.borderRadius,
58
+ ...(Platform.OS === 'web' && { overflowY: 'hidden' }),
46
59
  paddingBottom: paddingVertical,
47
60
  paddingTop: paddingVertical,
48
61
  paddingLeft: paddingHorizontal,
49
- paddingRight: paddingHorizontal,
50
- ...tokens
62
+ paddingRight: paddingHorizontal
51
63
  }}
52
64
  >
53
65
  {children}
@@ -80,7 +92,8 @@ DropdownOverlay.propTypes = {
80
92
  minWidth: PropTypes.number,
81
93
  onLayout: PropTypes.func,
82
94
  tokens: PropTypes.object,
83
- testID: PropTypes.string
95
+ testID: PropTypes.string,
96
+ variant: PropTypes.object
84
97
  }
85
98
 
86
99
  export default Platform.OS === 'web' ? withPortal(DropdownOverlay) : DropdownOverlay
@@ -0,0 +1,182 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { View, StyleSheet, Text, Pressable } from 'react-native'
4
+ import { useThemeTokens } from '../ThemeProvider'
5
+ import { useCopy, variantProp, copyPropTypes } from '../utils'
6
+ import Icon from '../Icon'
7
+ import IconButton from '../IconButton'
8
+ import Divider from '../Divider'
9
+ import defaultDictionary from './dictionary'
10
+
11
+ const styles = StyleSheet.create({
12
+ headerContainer: {
13
+ width: '100%'
14
+ },
15
+ headerContent: {
16
+ flexDirection: 'row',
17
+ alignItems: 'center',
18
+ width: '100%'
19
+ },
20
+ leftSection: {
21
+ flexDirection: 'row',
22
+ alignItems: 'center',
23
+ flex: 1
24
+ },
25
+ backIcon: {
26
+ marginRight: 8,
27
+ flexShrink: 0
28
+ },
29
+ labelText: {
30
+ flex: 1
31
+ },
32
+ closeButton: {
33
+ flexShrink: 0
34
+ },
35
+ dividerContainer: {
36
+ width: '100%'
37
+ }
38
+ })
39
+
40
+ const selectHeaderContainerStyles = ({ secondLevelHeaderBackgroundColor }) => ({
41
+ backgroundColor: secondLevelHeaderBackgroundColor
42
+ })
43
+
44
+ const selectHeaderContentStyles = ({
45
+ secondLevelHeaderPaddingTop,
46
+ secondLevelHeaderPaddingBottom,
47
+ secondLevelHeaderPaddingLeft,
48
+ secondLevelHeaderPaddingRight
49
+ }) => ({
50
+ paddingTop: secondLevelHeaderPaddingTop,
51
+ paddingBottom: secondLevelHeaderPaddingBottom,
52
+ paddingLeft: secondLevelHeaderPaddingLeft,
53
+ paddingRight: secondLevelHeaderPaddingRight
54
+ })
55
+
56
+ const selectLabelTextStyles = ({
57
+ secondLevelBackLinkFontName,
58
+ secondLevelBackLinkFontWeight,
59
+ secondLevelBackLinkFontSize,
60
+ secondLevelBackLinkColor
61
+ }) => ({
62
+ fontFamily: `${secondLevelBackLinkFontName}${secondLevelBackLinkFontWeight}normal`,
63
+ fontSize: secondLevelBackLinkFontSize,
64
+ color: secondLevelBackLinkColor
65
+ })
66
+
67
+ /**
68
+ * SecondLevelHeader component for Listbox secondLevel variant.
69
+ * Displays a header with back button icon, title text, and close button (IconButton),
70
+ * separated from content by a Divider.
71
+ */
72
+ const SecondLevelHeader = React.forwardRef(
73
+ (
74
+ {
75
+ label,
76
+ onBack,
77
+ onClose,
78
+ copy = 'en',
79
+ dictionary = defaultDictionary,
80
+ tokens: tokensProp = {},
81
+ variant = {}
82
+ },
83
+ ref
84
+ ) => {
85
+ const tokens = useThemeTokens('Listbox', variant, tokensProp)
86
+ const getCopy = useCopy({ dictionary, copy })
87
+
88
+ const {
89
+ secondLevelBackIcon,
90
+ secondLevelBackIconColor,
91
+ secondLevelCloseIcon,
92
+ secondLevelCloseIconSize,
93
+ secondLevelCloseButtonBorderWidth,
94
+ secondLevelCloseButtonPadding,
95
+ secondLevelDividerColor,
96
+ secondLevelDividerWidth
97
+ } = tokens
98
+
99
+ return (
100
+ <View style={[styles.headerContainer, selectHeaderContainerStyles(tokens)]} ref={ref}>
101
+ <View style={[styles.headerContent, selectHeaderContentStyles(tokens)]}>
102
+ <Pressable onPress={onBack} style={styles.leftSection}>
103
+ <View style={styles.backIcon}>
104
+ <Icon
105
+ icon={secondLevelBackIcon}
106
+ tokens={{
107
+ color: secondLevelBackIconColor
108
+ }}
109
+ variant={{ size: 'micro' }}
110
+ />
111
+ </View>
112
+ <Text numberOfLines={1} style={[styles.labelText, selectLabelTextStyles(tokens)]}>
113
+ {label}
114
+ </Text>
115
+ </Pressable>
116
+ <View style={styles.closeButton}>
117
+ <IconButton
118
+ icon={secondLevelCloseIcon}
119
+ onPress={onClose}
120
+ accessibilityLabel={getCopy('closeMenu')}
121
+ tokens={{
122
+ iconSize: secondLevelCloseIconSize,
123
+ borderWidth: secondLevelCloseButtonBorderWidth,
124
+ padding: secondLevelCloseButtonPadding
125
+ }}
126
+ />
127
+ </View>
128
+ </View>
129
+ <View style={styles.dividerContainer}>
130
+ <Divider
131
+ tokens={{
132
+ color: secondLevelDividerColor,
133
+ width: secondLevelDividerWidth
134
+ }}
135
+ />
136
+ </View>
137
+ </View>
138
+ )
139
+ }
140
+ )
141
+
142
+ SecondLevelHeader.displayName = 'SecondLevelHeader'
143
+
144
+ SecondLevelHeader.propTypes = {
145
+ /**
146
+ * The label text to display (typically the parent item label)
147
+ */
148
+ label: PropTypes.string.isRequired,
149
+ /**
150
+ * Callback when back button is clicked
151
+ */
152
+ onBack: PropTypes.func.isRequired,
153
+ /**
154
+ * Callback when close button is clicked
155
+ */
156
+ onClose: PropTypes.func.isRequired,
157
+ /**
158
+ * Select English or French copy
159
+ */
160
+ copy: copyPropTypes,
161
+ /**
162
+ * Override the default dictionary, by passing the complete dictionary object for `en` and `fr`
163
+ */
164
+ dictionary: PropTypes.shape({
165
+ en: PropTypes.shape({
166
+ closeMenu: PropTypes.string.isRequired
167
+ }),
168
+ fr: PropTypes.shape({
169
+ closeMenu: PropTypes.string.isRequired
170
+ })
171
+ }),
172
+ /**
173
+ * Custom tokens to override theme tokens
174
+ */
175
+ tokens: PropTypes.object,
176
+ /**
177
+ * Variant configuration
178
+ */
179
+ variant: variantProp.propType
180
+ }
181
+
182
+ export default SecondLevelHeader
@@ -0,0 +1,8 @@
1
+ export default {
2
+ en: {
3
+ closeMenu: 'Close menu'
4
+ },
5
+ fr: {
6
+ closeMenu: 'Fermer le menu'
7
+ }
8
+ }
@@ -0,0 +1,174 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { Platform, StyleSheet, View } from 'react-native'
4
+ import { viewports } from '@telus-uds/system-constants'
5
+
6
+ import { useTheme, useThemeTokens } from '../ThemeProvider'
7
+ import { useViewport } from '../ViewportProvider'
8
+ import {
9
+ a11yProps,
10
+ getTokensPropType,
11
+ selectSystemProps,
12
+ useResponsiveProp,
13
+ variantProp,
14
+ viewProps
15
+ } from '../utils'
16
+
17
+ import HorizontalScroll, {
18
+ horizontalScrollUtils,
19
+ HorizontalScrollButton
20
+ } from '../HorizontalScroll'
21
+
22
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
23
+
24
+ const { selectHorizontalScrollTokens, useItemPositions } = horizontalScrollUtils
25
+
26
+ const selectStyles = (themeTokens, maxWidth, viewport) => {
27
+ const isDesktop =
28
+ viewport === viewports.md || viewport === viewports.lg || viewport === viewports.xl
29
+
30
+ return {
31
+ wrapper: {
32
+ alignItems: isDesktop ? 'center' : 'flex-start'
33
+ },
34
+ scrollContainer: {
35
+ width: '100%',
36
+ ...(isDesktop && { maxWidth })
37
+ },
38
+ container: {
39
+ paddingTop: themeTokens.mainContainerTopPadding,
40
+ paddingBottom: themeTokens.mainContainerBottomPadding,
41
+ paddingLeft: themeTokens.mainContainerLeftPadding,
42
+ paddingRight: themeTokens.mainContainerRightPadding,
43
+ gap: themeTokens.mainContainerGap,
44
+ ...(isDesktop && {
45
+ alignItems: 'flex-start',
46
+ justifyContent: 'center'
47
+ })
48
+ }
49
+ }
50
+ }
51
+
52
+ /**
53
+ * A horizontal scrollable shortcuts component that displays a collection of shortcut items.
54
+ * This component automatically injects shared configuration props to all ShortcutsItem children
55
+ * via React.cloneElement, including variant settings, hideLabels, and iconVariant.
56
+ *
57
+ * @component
58
+ * @param {Object} props - Component properties
59
+ * @param {Object} [props.tokens] - Theme tokens to customize the component's appearance
60
+ * @param {Object} [props.variant] - Visual variant configuration for the shortcuts container and its items
61
+ * @param {string} [props.variant.width] - Width variant to apply to all items (e.g., 'equal', 'dynamic')
62
+ * @param {Object} [props.scrollButtonTokens] - Tokens to customize scroll button appearance
63
+ * @param {boolean} [props.hideLabels=false] - Whether to hide labels on all shortcut items (can be overridden per item)
64
+ * @param {Object} [props.iconVariant] - Icon variant to apply to all shortcut items (can be overridden per item)
65
+ * @param {React.ReactNode} props.children - ShortcutsItem components to render
66
+ * @param {React.Ref} ref - Forwarded ref to the component's root element
67
+ * @returns {React.ReactElement} Rendered shortcuts component with horizontal scroll functionality
68
+ *
69
+ * @example
70
+ * <Shortcuts hideLabels={false} variant={{ width: 'equal' }}>
71
+ * <ShortcutsItem icon={HomeIcon} label="Home" href="/home" />
72
+ * <ShortcutsItem icon={SettingsIcon} label="Settings" href="/settings" />
73
+ * </Shortcuts>
74
+ *
75
+ * @example
76
+ * // Item-level props override container props
77
+ * <Shortcuts hideLabels iconVariant={{ size: 'small' }}>
78
+ * <ShortcutsItem icon={HomeIcon} label="Home" hideLabel={false} />
79
+ * <ShortcutsItem icon={SettingsIcon} label="Settings" />
80
+ * </Shortcuts>
81
+ */
82
+ const Shortcuts = React.forwardRef(
83
+ (
84
+ { tokens, variant, scrollButtonTokens, hideLabels = false, iconVariant, children, ...rest },
85
+ ref
86
+ ) => {
87
+ const viewport = useViewport()
88
+ const themeTokens = useThemeTokens('Shortcuts', tokens, variant, {
89
+ viewport
90
+ })
91
+
92
+ const { themeOptions } = useTheme()
93
+ const maxWidth = useResponsiveProp(
94
+ themeOptions?.contentMaxWidth,
95
+ viewports.map.get(viewports.xl)
96
+ )
97
+
98
+ const [itemPositions] = useItemPositions()
99
+
100
+ const [maxItemWidth, setMaxItemWidth] = React.useState(null)
101
+
102
+ const registerWidth = React.useCallback(
103
+ (width) => setMaxItemWidth((prev) => (prev == null || width > prev ? width : prev)),
104
+ []
105
+ )
106
+
107
+ const styles = selectStyles(themeTokens, maxWidth, viewport)
108
+
109
+ const childrenWithProps = React.Children.map(children, (child) => {
110
+ if (!React.isValidElement(child)) {
111
+ return child
112
+ }
113
+ return React.cloneElement(child, {
114
+ maxWidth: maxItemWidth,
115
+ registerWidth,
116
+ containerVariant: variant,
117
+ containerHideLabels: hideLabels,
118
+ containerIconVariant: iconVariant
119
+ })
120
+ })
121
+
122
+ return (
123
+ <View style={[staticStyles.wrapper, styles.wrapper]} ref={ref} {...selectProps(rest)}>
124
+ <View style={styles.scrollContainer}>
125
+ <HorizontalScroll
126
+ ScrollButton={HorizontalScrollButton}
127
+ itemPositions={itemPositions}
128
+ tokens={selectHorizontalScrollTokens(themeTokens)}
129
+ scrollButtonTokens={scrollButtonTokens}
130
+ variant={{ hideNavigationButtons: Platform.OS !== 'web' }}
131
+ >
132
+ <View style={[staticStyles.container, styles.container]}>{childrenWithProps}</View>
133
+ </HorizontalScroll>
134
+ </View>
135
+ </View>
136
+ )
137
+ }
138
+ )
139
+
140
+ Shortcuts.displayName = 'Shortcuts'
141
+
142
+ Shortcuts.propTypes = {
143
+ ...selectedSystemPropTypes,
144
+ tokens: getTokensPropType('Shortcuts'),
145
+ variant: variantProp.propType,
146
+ /**
147
+ * Custom tokens for `HorizontalScrollButton`
148
+ */
149
+ scrollButtonTokens: getTokensPropType('HorizontalScrollButton'),
150
+ /**
151
+ * Hide labels for all ShortcutsItem children. When true, labels are visually hidden but remain accessible to screen readers via the icon's accessibilityLabel.
152
+ */
153
+ hideLabels: PropTypes.bool,
154
+ /**
155
+ * Icon variant to apply to all ShortcutsItem children.
156
+ */
157
+ iconVariant: variantProp.propType,
158
+ /**
159
+ * ShortcutsItem components to be rendered within the Shortcuts container
160
+ */
161
+ children: PropTypes.node
162
+ }
163
+
164
+ const staticStyles = StyleSheet.create({
165
+ wrapper: {
166
+ flexGrow: 1
167
+ },
168
+ container: {
169
+ flexDirection: 'row',
170
+ flex: 1
171
+ }
172
+ })
173
+
174
+ export default Shortcuts