@telus-uds/components-base 3.28.1 → 3.29.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 (54) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/lib/cjs/Autocomplete/Autocomplete.js +86 -32
  3. package/lib/cjs/Autocomplete/constants.js +2 -1
  4. package/lib/cjs/Card/CardBase.js +12 -0
  5. package/lib/cjs/Carousel/Carousel.js +1 -2
  6. package/lib/cjs/ColourToggle/ColourBubble.js +17 -3
  7. package/lib/cjs/ColourToggle/ColourToggle.js +8 -2
  8. package/lib/cjs/ExpandCollapse/Control.js +17 -3
  9. package/lib/cjs/ExpandCollapse/Panel.js +6 -0
  10. package/lib/cjs/ExpandCollapseMini/ExpandCollapseMini.js +14 -2
  11. package/lib/cjs/ExpandCollapseMini/ExpandCollapseMiniControl.js +15 -2
  12. package/lib/cjs/Link/ChevronLink.js +1 -0
  13. package/lib/cjs/Link/LinkBase.js +29 -13
  14. package/lib/cjs/Link/MobileIconTextContent.js +156 -0
  15. package/lib/cjs/Listbox/ListboxOverlay.js +7 -1
  16. package/lib/cjs/Listbox/PressableItem.js +2 -2
  17. package/lib/cjs/TabBar/TabBar.js +7 -2
  18. package/lib/cjs/TextInput/TextInputBase.js +2 -2
  19. package/lib/esm/Autocomplete/Autocomplete.js +87 -33
  20. package/lib/esm/Autocomplete/constants.js +1 -0
  21. package/lib/esm/Card/CardBase.js +12 -0
  22. package/lib/esm/Carousel/Carousel.js +1 -2
  23. package/lib/esm/ColourToggle/ColourBubble.js +17 -3
  24. package/lib/esm/ColourToggle/ColourToggle.js +8 -2
  25. package/lib/esm/ExpandCollapse/Control.js +17 -3
  26. package/lib/esm/ExpandCollapse/Panel.js +6 -0
  27. package/lib/esm/ExpandCollapseMini/ExpandCollapseMini.js +14 -2
  28. package/lib/esm/ExpandCollapseMini/ExpandCollapseMiniControl.js +15 -2
  29. package/lib/esm/Link/ChevronLink.js +1 -0
  30. package/lib/esm/Link/LinkBase.js +29 -13
  31. package/lib/esm/Link/MobileIconTextContent.js +147 -0
  32. package/lib/esm/Listbox/ListboxOverlay.js +7 -1
  33. package/lib/esm/Listbox/PressableItem.js +3 -3
  34. package/lib/esm/TabBar/TabBar.js +7 -2
  35. package/lib/esm/TextInput/TextInputBase.js +2 -2
  36. package/lib/package.json +1 -1
  37. package/package.json +1 -1
  38. package/src/Autocomplete/Autocomplete.jsx +142 -77
  39. package/src/Autocomplete/constants.js +1 -0
  40. package/src/Card/CardBase.jsx +12 -0
  41. package/src/Carousel/Carousel.jsx +1 -2
  42. package/src/ColourToggle/ColourBubble.jsx +18 -3
  43. package/src/ColourToggle/ColourToggle.jsx +7 -2
  44. package/src/ExpandCollapse/Control.jsx +24 -4
  45. package/src/ExpandCollapse/Panel.jsx +6 -0
  46. package/src/ExpandCollapseMini/ExpandCollapseMini.jsx +23 -3
  47. package/src/ExpandCollapseMini/ExpandCollapseMiniControl.jsx +14 -2
  48. package/src/Link/ChevronLink.jsx +1 -0
  49. package/src/Link/LinkBase.jsx +47 -20
  50. package/src/Link/MobileIconTextContent.jsx +129 -0
  51. package/src/Listbox/ListboxOverlay.jsx +9 -1
  52. package/src/Listbox/PressableItem.jsx +1 -1
  53. package/src/TabBar/TabBar.jsx +21 -4
  54. package/src/TextInput/TextInputBase.jsx +2 -2
@@ -16,6 +16,7 @@ import { resolvePressableTokens } from '../utils/pressability'
16
16
  import { withLinkRouter } from '../utils'
17
17
 
18
18
  import InlinePressable from './InlinePressable'
19
+ import MobileIconTextContent from './MobileIconTextContent'
19
20
  import { applyTextStyles, applyOuterBorder, useTheme } from '../ThemeProvider'
20
21
  import { IconText, iconComponentPropTypes } from '../Icon'
21
22
 
@@ -133,6 +134,7 @@ const LinkBase = React.forwardRef(
133
134
  tokens = {},
134
135
  children,
135
136
  dataSet,
137
+ useMeasuredMobileIconLayout = false,
136
138
  accessibilityRole = 'link',
137
139
  ...rawRest
138
140
  },
@@ -171,12 +173,14 @@ const LinkBase = React.forwardRef(
171
173
  const themeTokens = resolveLinkTokens(linkState)
172
174
  const outerBorderStyles = selectOuterBorderStyles(themeTokens)
173
175
  const decorationStyles = selectDecorationStyles(themeTokens)
174
-
175
- const mobileCompensation = null
176
+ const shouldUseMeasuredMobileContent =
177
+ Platform.OS !== 'web' && useMeasuredMobileIconLayout
176
178
 
177
179
  return [
178
180
  outerBorderStyles,
179
- mobileCompensation,
181
+ shouldUseMeasuredMobileContent
182
+ ? staticStyles.measuredMobileOuterBorderCompensation
183
+ : null,
180
184
  blockLeftStyle,
181
185
  decorationStyles,
182
186
  hasIcon && staticStyles.rowContainer
@@ -196,28 +200,50 @@ const LinkBase = React.forwardRef(
196
200
 
197
201
  const isTextOnlyLink = !IconComponent && !icon && accessibilityRole === 'link'
198
202
  const adjustedIconSpace = Platform.OS !== 'web' && isTextOnlyLink ? 0 : iconSpace
203
+ const shouldUseMeasuredMobileContent =
204
+ Platform.OS !== 'web' && useMeasuredMobileIconLayout
205
+ const textBaselineStyle = shouldUseMeasuredMobileContent ? null : staticStyles.baseline
206
+
207
+ const linkTextContent = (
208
+ <Text
209
+ style={[
210
+ textStyles,
211
+ blockTextStyles,
212
+ textBaselineStyle,
213
+ staticStyles.bubblePointerEvents
214
+ ]}
215
+ >
216
+ {typeof children === 'function' ? children(linkState) : children}
217
+ </Text>
218
+ )
219
+
220
+ const sharedIconProps = {
221
+ ...iconProps,
222
+ tokens: iconTokens,
223
+ style: staticStyles.bubblePointerEvents
224
+ }
225
+
226
+ if (shouldUseMeasuredMobileContent) {
227
+ return (
228
+ <MobileIconTextContent
229
+ icon={IconComponent}
230
+ iconPosition={iconPosition}
231
+ space={adjustedIconSpace}
232
+ iconProps={sharedIconProps}
233
+ >
234
+ {linkTextContent}
235
+ </MobileIconTextContent>
236
+ )
237
+ }
199
238
 
200
239
  return (
201
240
  <IconText
202
241
  icon={IconComponent}
203
242
  iconPosition={iconPosition}
204
243
  space={adjustedIconSpace}
205
- iconProps={{
206
- ...iconProps,
207
- tokens: iconTokens,
208
- style: staticStyles.bubblePointerEvents
209
- }}
244
+ iconProps={sharedIconProps}
210
245
  >
211
- <Text
212
- style={[
213
- textStyles,
214
- blockTextStyles,
215
- staticStyles.baseline,
216
- staticStyles.bubblePointerEvents
217
- ]}
218
- >
219
- {typeof children === 'function' ? children(linkState) : children}
220
- </Text>
246
+ {linkTextContent}
221
247
  </IconText>
222
248
  )
223
249
  }}
@@ -280,11 +306,12 @@ const staticStyles = StyleSheet.create({
280
306
  }
281
307
  })
282
308
  },
283
- outerBorderCompensation: {
309
+ measuredMobileOuterBorderCompensation: {
284
310
  ...(Platform.OS !== 'web' && {
285
311
  marginHorizontal: 2,
312
+ marginVertical: 2,
286
313
  paddingHorizontal: Platform.OS === 'android' ? 2 : 0,
287
- paddingTop: Platform.OS === 'android' ? 2 : 0
314
+ paddingVertical: Platform.OS === 'android' ? 2 : 0
288
315
  })
289
316
  }
290
317
  })
@@ -0,0 +1,129 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { Text, View, StyleSheet } from 'react-native'
4
+
5
+ import Icon, { iconComponentPropTypes } from '../Icon/Icon'
6
+ import { spacingProps } from '../utils'
7
+
8
+ const MobileIconTextContent = React.forwardRef(
9
+ ({ space = 0, iconPosition = 'left', icon: IconComponent, iconProps = {}, children }, ref) => {
10
+ const [translateY, setTranslateY] = React.useState(0)
11
+ const latestTranslateYRef = React.useRef(0)
12
+ const layoutsRef = React.useRef({
13
+ container: null,
14
+ text: null,
15
+ icon: null
16
+ })
17
+
18
+ const applyAlignment = React.useCallback(() => {
19
+ const { container, text, icon } = layoutsRef.current
20
+
21
+ if (!container || !icon || !icon.height) return
22
+
23
+ const targetY = text ? text.y + text.height / 2 : container.height / 2
24
+ const iconY = icon.y + icon.height / 2
25
+ const nextTranslateY = Math.round((targetY - iconY) * 100) / 100
26
+
27
+ if (!Number.isFinite(nextTranslateY)) return
28
+ if (Math.abs(nextTranslateY - latestTranslateYRef.current) < 0.5) return
29
+
30
+ latestTranslateYRef.current = nextTranslateY
31
+ setTranslateY(nextTranslateY)
32
+ }, [])
33
+
34
+ const handleContainerLayout = React.useCallback(
35
+ ({ nativeEvent: { layout } }) => {
36
+ layoutsRef.current.container = layout
37
+ applyAlignment()
38
+ },
39
+ [applyAlignment]
40
+ )
41
+
42
+ const handleTextLayout = React.useCallback(
43
+ ({ nativeEvent: { layout } }) => {
44
+ layoutsRef.current.text = layout
45
+ applyAlignment()
46
+ },
47
+ [applyAlignment]
48
+ )
49
+
50
+ const handleIconLayout = React.useCallback(
51
+ ({ nativeEvent: { layout } }) => {
52
+ layoutsRef.current.icon = layout
53
+ applyAlignment()
54
+ },
55
+ [applyAlignment]
56
+ )
57
+
58
+ const iconContent = IconComponent ? (
59
+ <Icon ref={ref} icon={IconComponent} scalesWithText {...iconProps} />
60
+ ) : null
61
+
62
+ const iconWrapper = IconComponent ? (
63
+ <View
64
+ onLayout={handleIconLayout}
65
+ style={[staticStyles.iconContainer, { transform: [{ translateY }] }]}
66
+ >
67
+ {iconContent}
68
+ </View>
69
+ ) : null
70
+
71
+ if (iconPosition === 'inline') {
72
+ return (
73
+ <Text onLayout={handleContainerLayout}>
74
+ <Text onLayout={handleTextLayout}>{children}</Text>{' '}
75
+ <View style={staticStyles.inlineIconContainer}>{iconWrapper}</View>
76
+ </Text>
77
+ )
78
+ }
79
+
80
+ const iconSpaceStyle = iconPosition === 'left' ? { marginRight: space } : { marginLeft: space }
81
+
82
+ return (
83
+ <View onLayout={handleContainerLayout} style={staticStyles.rowContainer}>
84
+ {iconPosition === 'left' && <View style={iconSpaceStyle}>{iconWrapper}</View>}
85
+ <View onLayout={handleTextLayout}>{children}</View>
86
+ {iconPosition === 'right' && <View style={iconSpaceStyle}>{iconWrapper}</View>}
87
+ </View>
88
+ )
89
+ }
90
+ )
91
+
92
+ MobileIconTextContent.displayName = 'MobileIconTextContent'
93
+
94
+ MobileIconTextContent.propTypes = {
95
+ /**
96
+ * Amount of space between text and icon. Uses the theme spacing scale.
97
+ */
98
+ space: spacingProps.types.spacingValue,
99
+ /**
100
+ * Position of the icon relative to text.
101
+ */
102
+ iconPosition: PropTypes.oneOf(['left', 'right', 'inline']),
103
+ /**
104
+ * A valid UDS icon component imported from a UDS palette.
105
+ */
106
+ icon: PropTypes.elementType,
107
+ /**
108
+ * Props passed to the icon component.
109
+ */
110
+ iconProps: PropTypes.exact(iconComponentPropTypes),
111
+ /**
112
+ * Content rendered alongside the icon.
113
+ */
114
+ children: PropTypes.node
115
+ }
116
+
117
+ const staticStyles = StyleSheet.create({
118
+ rowContainer: {
119
+ flexDirection: 'row'
120
+ },
121
+ iconContainer: {
122
+ alignSelf: 'flex-start'
123
+ },
124
+ inlineIconContainer: {
125
+ position: 'absolute'
126
+ }
127
+ })
128
+
129
+ export default MobileIconTextContent
@@ -28,6 +28,7 @@ const DropdownOverlay = React.forwardRef(
28
28
  isReady = false,
29
29
  overlaidPosition,
30
30
  maxWidth,
31
+ maxHeight,
31
32
  minWidth,
32
33
  onLayout,
33
34
  tokens,
@@ -50,12 +51,18 @@ const DropdownOverlay = React.forwardRef(
50
51
  staticStyles.positioner,
51
52
  !isReady && staticStyles.hidden
52
53
  ]}
54
+ onMouseDown={(e) => {
55
+ e.preventDefault()
56
+ }}
53
57
  >
54
58
  <Card
55
59
  tokens={{
56
60
  shadow: systemTokens.shadow,
57
61
  borderRadius: systemTokens.borderRadius,
58
- ...(Platform.OS === 'web' && { overflowY: 'hidden' }),
62
+ ...(Platform.OS === 'web' && {
63
+ maxHeight,
64
+ overflowY: 'auto'
65
+ }),
59
66
  paddingBottom: paddingVertical,
60
67
  paddingTop: paddingVertical,
61
68
  paddingLeft: paddingHorizontal,
@@ -89,6 +96,7 @@ DropdownOverlay.propTypes = {
89
96
  width: PropTypes.number
90
97
  }),
91
98
  maxWidth: PropTypes.number,
99
+ maxHeight: PropTypes.number,
92
100
  minWidth: PropTypes.number,
93
101
  onLayout: PropTypes.func,
94
102
  tokens: PropTypes.object,
@@ -120,7 +120,7 @@ const PressableItem = React.forwardRef(
120
120
  >
121
121
  {(pressableState) => {
122
122
  return (
123
- <Text style={selectTextStyles(resolveButtonTokens(pressableState))}>{children} </Text>
123
+ <Text style={selectTextStyles(resolveButtonTokens(pressableState))}>{children}</Text>
124
124
  )
125
125
  }}
126
126
  </Pressable>
@@ -42,12 +42,24 @@ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, vie
42
42
  * items={items}
43
43
  * initiallySelectedItem="1"
44
44
  * onChange={(itemId) => console.log(itemId)}
45
+ * accessibilityLabel="Main navigation"
45
46
  * />
46
47
  * )
47
48
  */
48
49
 
49
50
  const TabBar = React.forwardRef(
50
- ({ items = [], initiallySelectedItem = '0', onChange, variant, tokens, ...rest }, ref) => {
51
+ (
52
+ {
53
+ items = [],
54
+ initiallySelectedItem = '0',
55
+ onChange,
56
+ variant,
57
+ tokens,
58
+ accessibilityLabel,
59
+ ...rest
60
+ },
61
+ ref
62
+ ) => {
51
63
  const [isSelected, setIsSelected] = React.useState(initiallySelectedItem)
52
64
  const themeTokens = useThemeTokens('TabBar', tokens, variant)
53
65
 
@@ -62,7 +74,11 @@ const TabBar = React.forwardRef(
62
74
  style={[styles.tabBar, selectTabBarContainerStyles(themeTokens)]}
63
75
  {...selectProps(rest)}
64
76
  >
65
- <View style={[styles.tabBarItem, selectTabBarItemContainerStyles(themeTokens)]}>
77
+ <View
78
+ style={[styles.tabBarItem, selectTabBarItemContainerStyles(themeTokens)]}
79
+ accessibilityRole="tablist"
80
+ accessibilityLabel={accessibilityLabel}
81
+ >
66
82
  {items.map((item, index) => (
67
83
  <TabBarItem
68
84
  key={item.id}
@@ -73,7 +89,6 @@ const TabBar = React.forwardRef(
73
89
  iconActive={item.iconActive}
74
90
  onPress={() => handlePress(item.id)}
75
91
  id={`tab-item-${index}`}
76
- accessibilityRole="tablist"
77
92
  tokens={item.tokens}
78
93
  />
79
94
  ))}
@@ -105,7 +120,9 @@ TabBar.propTypes = {
105
120
  /** Variant of TabBar for styling purposes. */
106
121
  variant: variantProp.propType,
107
122
  /** Tokens for theming and styling. */
108
- tokens: getTokensPropType('TabBar')
123
+ tokens: getTokensPropType('TabBar'),
124
+ /** Accessible label for the tab bar navigation region, used by screen readers to identify the tablist. */
125
+ accessibilityLabel: PropTypes.string
109
126
  }
110
127
 
111
128
  const styles = StyleSheet.create({
@@ -255,8 +255,8 @@ const TextInputBase = React.forwardRef(
255
255
  }, [element, pattern])
256
256
 
257
257
  const handleChangeText = (event) => {
258
- const text = event.nativeEvent?.text || event.target?.value
259
- let filteredText = isNumeric ? text?.replace(/[^\d]/g, '') : text
258
+ const text = event.nativeEvent?.text ?? event.target?.value
259
+ let filteredText = isNumeric ? text?.replace(/[^\d]/g, '') || undefined : text
260
260
  if (type === 'card' && filteredText) {
261
261
  const formattedValue = filteredText.replace(/[^a-zA-Z0-9]/g, '')
262
262
  const regex = new RegExp(`([a-zA-Z0-9]{4})(?=[a-zA-Z0-9])`, 'g')