@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
@@ -0,0 +1,297 @@
1
+ import React from 'react'
2
+ import { Image, Platform, Pressable, StyleSheet, View } from 'react-native'
3
+ import PropTypes from 'prop-types'
4
+
5
+ import { applyTextStyles, useThemeTokensCallback } from '../ThemeProvider'
6
+ import {
7
+ a11yProps,
8
+ clickProps,
9
+ getTokensPropType,
10
+ hrefAttrsProp,
11
+ linkProps,
12
+ resolvePressableState,
13
+ selectSystemProps,
14
+ variantProp,
15
+ viewProps,
16
+ wrapStringsInText
17
+ } from '../utils'
18
+ import Icon from '../Icon'
19
+
20
+ const DYNAMIC_WIDTH_VARIANT = 'dynamic'
21
+ const EQUAL_WIDTH_VARIANT = 'equal'
22
+
23
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, linkProps, viewProps])
24
+
25
+ const selectPressableStyles = (tokens, widthVariant, equalWidth) => {
26
+ const styles = {
27
+ borderColor: tokens.borderColor,
28
+ borderRadius: tokens.borderRadius,
29
+ borderWidth: tokens.borderWidth,
30
+ ...Platform.select({
31
+ web: {
32
+ outline: 'none'
33
+ }
34
+ })
35
+ }
36
+
37
+ if (widthVariant === DYNAMIC_WIDTH_VARIANT) {
38
+ styles.width = 'auto'
39
+ } else if (widthVariant === EQUAL_WIDTH_VARIANT) {
40
+ if (equalWidth) {
41
+ styles.width = equalWidth
42
+ } else {
43
+ styles.minWidth = tokens.width
44
+ }
45
+ } else {
46
+ styles.width = tokens.width
47
+ }
48
+
49
+ return styles
50
+ }
51
+
52
+ const selectIconContainerStyles = (tokens) => ({
53
+ paddingBottom: tokens.iconContainerPaddingBottom,
54
+ paddingLeft: tokens.iconContainerPaddingLeft,
55
+ paddingRight: tokens.iconContainerPaddingRight,
56
+ paddingTop: tokens.iconContainerPaddingTop
57
+ })
58
+
59
+ const selectIconVariant = () => ({
60
+ background: true,
61
+ padding: 'medium'
62
+ })
63
+
64
+ const selectIconTokens = (tokens) => ({
65
+ backgroundColor: tokens.iconBackgroundColor,
66
+ color: tokens.iconColor,
67
+ size: tokens.iconSize,
68
+ width: tokens.iconWidth
69
+ })
70
+
71
+ const selectImageStyles = (tokens) => ({
72
+ width: tokens.imageWidth,
73
+ height: tokens.imageHeight
74
+ })
75
+
76
+ const selectLabelContainerStyles = (tokens) => ({
77
+ paddingBottom: tokens.labelContainerPaddingBottom,
78
+ paddingLeft: tokens.labelContainerPaddingLeft,
79
+ paddingRight: tokens.labelContainerPaddingRight,
80
+ paddingTop: tokens.labelContainerPaddingTop
81
+ })
82
+
83
+ const selectTitleTextStyles = (tokens) =>
84
+ applyTextStyles({
85
+ fontColor: tokens.labelFontColor,
86
+ fontName: tokens.labelFontName,
87
+ fontSize: tokens.labelFontSize,
88
+ fontWeight: tokens.labelFontWeight,
89
+ lineHeight: tokens.labelLineHeight,
90
+ textDecorationLine: tokens.labelUnderline,
91
+ textAlign: tokens.labelTextAlign
92
+ })
93
+
94
+ /**
95
+ * A clickable shortcut item component that displays an icon or image with an optional label.
96
+ * Can be used within a Shortcuts container to create a grid of navigation shortcuts.
97
+ *
98
+ * @component
99
+ * @param {Object} props - Component props
100
+ * @param {string} [props.icon] - Icon identifier to display
101
+ * @param {Object} [props.image={ src: '', alt: '' }] - Image object with src and alt properties
102
+ * @param {string} [props.image.src] - Image source URL
103
+ * @param {string} [props.image.alt] - Image alt text for accessibility
104
+ * @param {string|React.ReactNode} props.label - Label text or content to display below the icon/image
105
+ * @param {boolean} [props.hideLabel=false] - Whether to hide the label for this specific item
106
+ * @param {string} [props.href] - Link URL for navigation
107
+ * @param {Object} [props.iconVariant] - Icon variant to apply to this specific item
108
+ * @param {Object} [props.tokens] - Theme tokens to customize appearance
109
+ * @param {Object} [props.variant] - Variant configuration object for this specific item
110
+ * @param {string} [props.variant.width] - Width variant (e.g., 'dynamic', 'equal')
111
+ * @param {Function} [props.onPressableStateChange] - Callback function that receives the pressable state object (pressed, hovered, focused)
112
+ * @param {number} [props.maxWidth] - Maximum width for equal width variant (injected by Shortcuts container)
113
+ * @param {Function} [props.registerWidth] - Callback to register width for equal width variant (injected by Shortcuts container)
114
+ * @param {Object} [props.containerVariant] - Variant configuration from Shortcuts container (injected by Shortcuts container)
115
+ * @param {boolean} [props.containerHideLabels] - Hide labels setting from Shortcuts container (injected by Shortcuts container)
116
+ * @param {Object} [props.containerIconVariant] - Icon variant from Shortcuts container (injected by Shortcuts container)
117
+ * @param {React.Ref} ref - Forwarded ref to the Pressable component
118
+ * @returns {React.ReactElement} The rendered shortcut item
119
+ *
120
+ * @example
121
+ * <ShortcutsItem
122
+ * icon={HomeIcon}
123
+ * label="Home"
124
+ * href="/home"
125
+ * onPressableStateChange={(state) => console.log(state)}
126
+ * />
127
+ */
128
+ const ShortcutsItem = React.forwardRef(
129
+ (
130
+ {
131
+ icon,
132
+ image = { src: '', alt: '' },
133
+ label,
134
+ hideLabel = false,
135
+ href,
136
+ iconVariant,
137
+ tokens,
138
+ variant,
139
+ onPressableStateChange,
140
+ maxWidth,
141
+ registerWidth,
142
+ containerVariant,
143
+ containerHideLabels,
144
+ containerIconVariant,
145
+ ...rest
146
+ },
147
+ ref
148
+ ) => {
149
+ const mergedVariant = { ...containerVariant, ...variant }
150
+ const widthVariant = mergedVariant?.width
151
+ const shouldHideLabel = hideLabel || containerHideLabels
152
+ const mergedIconVariant = iconVariant ?? containerIconVariant
153
+
154
+ const getThemeTokens = useThemeTokensCallback('ShortcutsItem', tokens, mergedVariant)
155
+ const getTokens = (pressableState) => getThemeTokens(resolvePressableState(pressableState))
156
+
157
+ const { onPress, ...props } = clickProps.toPressProps(rest)
158
+ const { hrefAttrs, rawRest } = hrefAttrsProp.bundle(props)
159
+ const selectedProps = selectProps({
160
+ href,
161
+ onPress: linkProps.handleHref({ href, onPress }),
162
+ hrefAttrs,
163
+ ...rawRest
164
+ })
165
+
166
+ const handleLayout = (event) => {
167
+ if (widthVariant === EQUAL_WIDTH_VARIANT && registerWidth) {
168
+ const { width } = event.nativeEvent.layout
169
+ registerWidth(width)
170
+ }
171
+ }
172
+
173
+ return (
174
+ <Pressable
175
+ ref={ref}
176
+ style={(pressableState) =>
177
+ selectPressableStyles(getTokens(pressableState), widthVariant, maxWidth)
178
+ }
179
+ onLayout={handleLayout}
180
+ {...selectedProps}
181
+ >
182
+ {(pressableState) => {
183
+ const themeTokens = getTokens(pressableState)
184
+
185
+ if (onPressableStateChange) {
186
+ onPressableStateChange(resolvePressableState(pressableState))
187
+ }
188
+
189
+ return (
190
+ <View style={staticStyles.container}>
191
+ {icon && (
192
+ <View style={selectIconContainerStyles(themeTokens)}>
193
+ <Icon
194
+ icon={icon}
195
+ variant={mergedIconVariant ?? selectIconVariant()}
196
+ tokens={mergedIconVariant ? {} : selectIconTokens(themeTokens)}
197
+ {...(Platform.OS === 'web' && { accessibilityLabel: label })}
198
+ />
199
+ </View>
200
+ )}
201
+ {!icon && image && (
202
+ <Image
203
+ source={image.src}
204
+ alt={image.alt}
205
+ style={selectImageStyles(themeTokens)}
206
+ resizeMethod="resize"
207
+ accessibilityIgnoresInvertColors
208
+ />
209
+ )}
210
+ {label && !shouldHideLabel && (
211
+ <View style={[staticStyles.label, selectLabelContainerStyles(themeTokens)]}>
212
+ {wrapStringsInText(label, { style: selectTitleTextStyles(themeTokens) })}
213
+ </View>
214
+ )}
215
+ </View>
216
+ )
217
+ }}
218
+ </Pressable>
219
+ )
220
+ }
221
+ )
222
+
223
+ ShortcutsItem.displayName = 'ShortcutsItem'
224
+
225
+ ShortcutsItem.propTypes = {
226
+ ...selectedSystemPropTypes,
227
+ tokens: getTokensPropType('ShortcutsItem'),
228
+ variant: variantProp.propType,
229
+ /**
230
+ * Icon for the ShortcutsItem
231
+ */
232
+ icon: PropTypes.elementType,
233
+ /**
234
+ * Image for the ShortcutsItem
235
+ */
236
+ image: PropTypes.shape({
237
+ src: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]),
238
+ alt: PropTypes.string
239
+ }),
240
+ /**
241
+ * Label for the ShortcutsItem
242
+ */
243
+ label: PropTypes.string,
244
+ /**
245
+ * Hide the label for this specific ShortcutsItem. When true, the label is visually hidden but remains accessible to screen readers via the icon's accessibilityLabel.
246
+ */
247
+ hideLabel: PropTypes.bool,
248
+ /**
249
+ * href for the ShortcutsItem
250
+ */
251
+ href: PropTypes.string,
252
+ /**
253
+ * Icon variant for this specific ShortcutsItem
254
+ */
255
+ iconVariant: variantProp.propType,
256
+ /**
257
+ * Callback function that receives the pressable state object containing pressed, hovered, and focused boolean properties
258
+ */
259
+ onPressableStateChange: PropTypes.func,
260
+ /**
261
+ * Maximum width for equal width variant (automatically injected by Shortcuts container)
262
+ * @private
263
+ */
264
+ maxWidth: PropTypes.number,
265
+ /**
266
+ * Callback to register width for equal width variant (automatically injected by Shortcuts container)
267
+ * @private
268
+ */
269
+ registerWidth: PropTypes.func,
270
+ /**
271
+ * Variant configuration from Shortcuts container (automatically injected by Shortcuts container)
272
+ * @private
273
+ */
274
+ containerVariant: variantProp.propType,
275
+ /**
276
+ * Hide labels setting from Shortcuts container (automatically injected by Shortcuts container)
277
+ * @private
278
+ */
279
+ containerHideLabels: PropTypes.bool,
280
+ /**
281
+ * Icon variant from Shortcuts container (automatically injected by Shortcuts container)
282
+ * @private
283
+ */
284
+ containerIconVariant: variantProp.propType
285
+ }
286
+
287
+ const staticStyles = StyleSheet.create({
288
+ container: {
289
+ alignItems: 'center',
290
+ justifyContent: 'center'
291
+ },
292
+ label: {
293
+ flexWrap: 'wrap'
294
+ }
295
+ })
296
+
297
+ export default ShortcutsItem
@@ -0,0 +1,4 @@
1
+ import Shortcuts from './Shortcuts'
2
+
3
+ export { default as ShortcutsItem } from './ShortcutsItem'
4
+ export default Shortcuts
@@ -127,12 +127,12 @@ const Tooltip = React.forwardRef(
127
127
  nativeID,
128
128
  activateOnHover = false,
129
129
  tooltipButtonTokens,
130
+ testID,
130
131
  ...rest
131
132
  },
132
133
  ref
133
134
  ) => {
134
135
  const [isOpen, setIsOpen] = React.useState(false)
135
-
136
136
  const controlRef = React.useRef()
137
137
  const [controlLayout, setControlLayout] = React.useState(null)
138
138
  const [tooltipDimensions, setTooltipDimensions] = React.useState(null)
@@ -276,6 +276,7 @@ const Tooltip = React.forwardRef(
276
276
  }
277
277
  })
278
278
  ]}
279
+ testID={testID}
279
280
  {...selectProps(rest)}
280
281
  >
281
282
  <Pressable
package/src/index.js CHANGED
@@ -51,6 +51,7 @@ export { default as RadioCard, RadioCardGroup } from './RadioCard'
51
51
  export { default as Responsive } from './Responsive'
52
52
  export { default as Search } from './Search'
53
53
  export { default as Select } from './Select'
54
+ export { default as Shortcuts, ShortcutsItem } from './Shortcuts'
54
55
  export { default as SideNav } from './SideNav'
55
56
  export { default as Skeleton } from './Skeleton'
56
57
  export { default as SkipLink } from './SkipLink'
@@ -40,6 +40,29 @@ type ListboxTokens = {
40
40
  itemHeight?: number
41
41
  groupHeight?: number
42
42
  lineHeight?: number
43
+ secondLevelHeaderBackgroundColor?: string
44
+ secondLevelHeaderPaddingTop?: number
45
+ secondLevelHeaderPaddingBottom?: number
46
+ secondLevelHeaderPaddingLeft?: number
47
+ secondLevelHeaderPaddingRight?: number
48
+ secondLevelBackIcon?: string
49
+ secondLevelBackIconColor?: string
50
+ secondLevelBackIconSize?: number
51
+ secondLevelBackLinkColor?: string
52
+ secondLevelBackLinkFontSize?: number
53
+ secondLevelBackLinkFontName?: string
54
+ secondLevelBackLinkFontWeight?: string
55
+ secondLevelCloseIcon?: string
56
+ secondLevelCloseIconColor?: string
57
+ secondLevelCloseIconSize?: number
58
+ secondLevelCloseButtonBackgroundColor?: string
59
+ secondLevelCloseButtonBorderColor?: string
60
+ secondLevelCloseButtonBorderWidth?: number | string
61
+ secondLevelCloseButtonBorderRadius?: number | string
62
+ secondLevelCloseButtonPadding?: number
63
+ secondLevelDividerColor?: string
64
+ secondLevelDividerWidth?: number
65
+ secondLevelParentIcon?: string
43
66
  }
44
67
 
45
68
  type ListboxItems = {
@@ -55,6 +78,7 @@ export interface ListboxProps {
55
78
  LinkRouter?: React.ElementType
56
79
  linkRouterProps?: object
57
80
  tokens?: ListboxTokens
81
+ variant?: { secondLevel?: boolean }
58
82
  selectedId?: string
59
83
  onClose?: () => void
60
84
  }
@@ -0,0 +1,136 @@
1
+ import type { ComponentType, ReactNode } from 'react'
2
+ import type { IconVariant } from './Icon'
3
+
4
+ export interface ShortcutsProps {
5
+ /**
6
+ * ShortcutsItem components to render as shortcuts.
7
+ */
8
+ children: ReactNode
9
+ /**
10
+ * Shortcuts variant options.
11
+ */
12
+ variant?: ShortcutsVariant
13
+ /**
14
+ * Custom tokens for styling the Shortcuts container.
15
+ */
16
+ tokens?: ShortcutsTokens
17
+ /**
18
+ * Hide labels for all shortcuts items globally.
19
+ * Can be overridden by individual ShortcutsItem hideLabel prop.
20
+ */
21
+ hideLabels?: boolean
22
+ /**
23
+ * Icon variant to apply to all shortcuts items.
24
+ * Can be overridden by individual ShortcutsItem iconVariant prop.
25
+ */
26
+ iconVariant?: IconVariant
27
+ }
28
+
29
+ export type ShortcutsVariant = {
30
+ /**
31
+ * Width distribution of shortcuts items.
32
+ * - 'equal': All items have equal width (default)
33
+ * - 'dynamic': Items size based on content
34
+ */
35
+ width?: 'equal' | 'dynamic'
36
+ }
37
+
38
+ export interface ShortcutsTokens {
39
+ mainContainerGap?: number
40
+ mainContainerPaddingHorizontal?: number
41
+ mainContainerPaddingVertical?: number
42
+ itemContainerMinHeight?: number
43
+ itemContainerPaddingHorizontal?: number
44
+ itemContainerPaddingVertical?: number
45
+ itemContainerBackgroundColor?: string
46
+ itemContainerBorderRadius?: number
47
+ labelFontColor?: string
48
+ labelFontSize?: number
49
+ labelFontWeight?: number
50
+ labelLineHeight?: number
51
+ labelMarginTop?: number
52
+ }
53
+
54
+ export interface ShortcutsItemProps {
55
+ /**
56
+ * Label text for the shortcut item.
57
+ */
58
+ label: string
59
+ /**
60
+ * Icon component to display.
61
+ */
62
+ icon?: ComponentType<any>
63
+ /**
64
+ * Image to display instead of icon.
65
+ */
66
+ image?: ShortcutsItemImage
67
+ /**
68
+ * URL to navigate to when pressed.
69
+ */
70
+ href?: string
71
+ /**
72
+ * Function to call when the item is pressed.
73
+ */
74
+ onPress?: () => void
75
+ /**
76
+ * Hide the label for this specific item.
77
+ * Overrides parent Shortcuts hideLabels prop.
78
+ */
79
+ hideLabel?: boolean
80
+ /**
81
+ * Icon variant for this specific item.
82
+ * Overrides parent Shortcuts iconVariant prop.
83
+ */
84
+ iconVariant?: IconVariant
85
+ /**
86
+ * Custom tokens for styling this item.
87
+ */
88
+ tokens?: ShortcutsItemTokens
89
+ /**
90
+ * Variant options for this item.
91
+ */
92
+ variant?: ShortcutsItemVariant
93
+ /**
94
+ * Callback function that receives the pressable state.
95
+ * Can be used to dynamically set iconVariant based on press state.
96
+ */
97
+ onPressableStateChange?: (state: PressableState) => IconVariant | void
98
+ }
99
+
100
+ export type ShortcutsItemImage = {
101
+ src: string
102
+ alt: string
103
+ }
104
+
105
+ export type ShortcutsItemVariant = {
106
+ /**
107
+ * Whether the item is in a pressed state.
108
+ */
109
+ pressed?: boolean
110
+ }
111
+
112
+ export interface ShortcutsItemTokens {
113
+ containerMinHeight?: number
114
+ containerPaddingHorizontal?: number
115
+ containerPaddingVertical?: number
116
+ containerBackgroundColor?: string
117
+ containerBorderRadius?: number
118
+ labelFontColor?: string
119
+ labelFontSize?: number
120
+ labelFontWeight?: number
121
+ labelLineHeight?: number
122
+ labelMarginTop?: number
123
+ iconSize?: number
124
+ }
125
+
126
+ export type PressableState = {
127
+ pressed: boolean
128
+ hovered?: boolean
129
+ focused?: boolean
130
+ }
131
+
132
+ declare const Shortcuts: ComponentType<ShortcutsProps>
133
+ declare const ShortcutsItem: ComponentType<ShortcutsItemProps>
134
+
135
+ export { Shortcuts, ShortcutsItem }
136
+ export default Shortcuts
package/types/index.d.ts CHANGED
@@ -51,6 +51,18 @@ export { SearchTokens, SearchProps } from './Search'
51
51
  export { default as Select } from './Select'
52
52
  export { SelectTokens, SelectProps, SelectItemProps, SelectGroupProps } from './Select'
53
53
 
54
+ export { default as Shortcuts, ShortcutsItem } from './Shortcuts'
55
+ export {
56
+ ShortcutsProps,
57
+ ShortcutsTokens,
58
+ ShortcutsVariant,
59
+ ShortcutsItemProps,
60
+ ShortcutsItemTokens,
61
+ ShortcutsItemVariant,
62
+ ShortcutsItemImage,
63
+ PressableState
64
+ } from './Shortcuts'
65
+
54
66
  export { default as Spacer } from './Spacer'
55
67
  export { SpacerProps } from './Spacer'
56
68