@telus-uds/components-base 3.28.2 → 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 (36) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/lib/cjs/Card/CardBase.js +12 -0
  3. package/lib/cjs/ColourToggle/ColourBubble.js +17 -3
  4. package/lib/cjs/ColourToggle/ColourToggle.js +8 -2
  5. package/lib/cjs/ExpandCollapse/Control.js +17 -3
  6. package/lib/cjs/ExpandCollapse/Panel.js +6 -0
  7. package/lib/cjs/ExpandCollapseMini/ExpandCollapseMini.js +14 -2
  8. package/lib/cjs/ExpandCollapseMini/ExpandCollapseMiniControl.js +15 -2
  9. package/lib/cjs/Link/ChevronLink.js +1 -0
  10. package/lib/cjs/Link/LinkBase.js +29 -13
  11. package/lib/cjs/Link/MobileIconTextContent.js +156 -0
  12. package/lib/cjs/TabBar/TabBar.js +7 -2
  13. package/lib/esm/Card/CardBase.js +12 -0
  14. package/lib/esm/ColourToggle/ColourBubble.js +17 -3
  15. package/lib/esm/ColourToggle/ColourToggle.js +8 -2
  16. package/lib/esm/ExpandCollapse/Control.js +17 -3
  17. package/lib/esm/ExpandCollapse/Panel.js +6 -0
  18. package/lib/esm/ExpandCollapseMini/ExpandCollapseMini.js +14 -2
  19. package/lib/esm/ExpandCollapseMini/ExpandCollapseMiniControl.js +15 -2
  20. package/lib/esm/Link/ChevronLink.js +1 -0
  21. package/lib/esm/Link/LinkBase.js +29 -13
  22. package/lib/esm/Link/MobileIconTextContent.js +147 -0
  23. package/lib/esm/TabBar/TabBar.js +7 -2
  24. package/lib/package.json +1 -1
  25. package/package.json +1 -1
  26. package/src/Card/CardBase.jsx +12 -0
  27. package/src/ColourToggle/ColourBubble.jsx +18 -3
  28. package/src/ColourToggle/ColourToggle.jsx +7 -2
  29. package/src/ExpandCollapse/Control.jsx +24 -4
  30. package/src/ExpandCollapse/Panel.jsx +6 -0
  31. package/src/ExpandCollapseMini/ExpandCollapseMini.jsx +23 -3
  32. package/src/ExpandCollapseMini/ExpandCollapseMiniControl.jsx +14 -2
  33. package/src/Link/ChevronLink.jsx +1 -0
  34. package/src/Link/LinkBase.jsx +47 -20
  35. package/src/Link/MobileIconTextContent.jsx +129 -0
  36. package/src/TabBar/TabBar.jsx +21 -4
@@ -90,6 +90,7 @@ const ExpandCollapsePanel = React.forwardRef(
90
90
  onPress,
91
91
  control,
92
92
  controlTokens,
93
+ controlAlign,
93
94
  children,
94
95
  tokens,
95
96
  variant,
@@ -174,6 +175,7 @@ const ExpandCollapsePanel = React.forwardRef(
174
175
  {...selectedProps}
175
176
  isExpanded={isExpanded}
176
177
  tokens={controlTokens}
178
+ controlAlign={controlAlign}
177
179
  variant={variant}
178
180
  onPress={handleControlPress}
179
181
  ref={controlRef}
@@ -284,6 +286,10 @@ ExpandCollapsePanel.propTypes = {
284
286
  * Optional theme token overrides that may be passed to the ExpandCollapseControl element.
285
287
  */
286
288
  controlTokens: getTokensPropType('ExpandCollapseControl'),
289
+ /**
290
+ * Optional alignment for control content.
291
+ */
292
+ controlAlign: PropTypes.oneOf(['flex-start', 'center', 'flex-end']),
287
293
  /**
288
294
  * An optional ref to be attached to the control
289
295
  */
@@ -7,9 +7,24 @@ import ExpandCollapseMiniControl from './ExpandCollapseMiniControl'
7
7
 
8
8
  const [selectContainerProps, selectedContainerPropTypes] = selectSystemProps([contentfulProps])
9
9
 
10
+ const alignMap = {
11
+ start: 'flex-start',
12
+ middle: 'center',
13
+ end: 'flex-end'
14
+ }
15
+
10
16
  const ExpandCollapseMini = React.forwardRef(
11
17
  (
12
- { children, onToggle = () => {}, tokens = {}, nativeID, initialOpen = false, dataSet, ...rest },
18
+ {
19
+ children,
20
+ onToggle = () => {},
21
+ tokens = {},
22
+ nativeID,
23
+ initialOpen = false,
24
+ dataSet,
25
+ align,
26
+ ...rest
27
+ },
13
28
  ref
14
29
  ) => {
15
30
  const expandCollapeMiniPanelId = useUniqueId('ExpandCollapseMiniPanel')
@@ -40,10 +55,11 @@ const ExpandCollapseMini = React.forwardRef(
40
55
  textLine: tokens.textLine ?? 'none',
41
56
  backgroundColor: 'transparent'
42
57
  }}
58
+ controlAlign={align && alignMap[align]}
43
59
  // TODO refactor
44
60
  // eslint-disable-next-line react/no-unstable-nested-components
45
61
  control={(pressableState) => (
46
- <ExpandCollapseMiniControl pressableState={pressableState} {...rest} />
62
+ <ExpandCollapseMiniControl pressableState={pressableState} align={align} {...rest} />
47
63
  )}
48
64
  controlRef={ref}
49
65
  nativeID={nativeID}
@@ -87,7 +103,11 @@ ExpandCollapseMini.propTypes = {
87
103
  /**
88
104
  * The dataSet prop allows to pass data-* attributes element to the component.
89
105
  */
90
- dataSet: PropTypes.object
106
+ dataSet: PropTypes.object,
107
+ /**
108
+ * Controls the horizontal alignment of the trigger label and icon within the panel width.
109
+ */
110
+ align: PropTypes.oneOf(['start', 'middle', 'end'])
91
111
  }
92
112
 
93
113
  export default ExpandCollapseMini
@@ -7,6 +7,12 @@ import { htmlAttrs, viewProps, selectSystemProps } from '../utils'
7
7
 
8
8
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs, viewProps])
9
9
 
10
+ const alignSelfMap = {
11
+ start: 'flex-start',
12
+ middle: 'center',
13
+ end: 'flex-end'
14
+ }
15
+
10
16
  // The ExpandCollapseControl has all the appropriate role, a11y, press handling etc
11
17
  // and a more appropriate press area, defer interaction handling to it.
12
18
  const presentationOnly = {
@@ -24,6 +30,7 @@ const ExpandCollapseMiniControl = React.forwardRef(
24
30
  iconPosition = 'right',
25
31
  tokens,
26
32
  variant = {},
33
+ align,
27
34
  ...rest
28
35
  },
29
36
  ref
@@ -96,7 +103,8 @@ const ExpandCollapseMiniControl = React.forwardRef(
96
103
  ...getTokens(linkState),
97
104
  iconSize,
98
105
  blockFontSize: fontSize,
99
- blockLineHeight: lineHeight
106
+ blockLineHeight: lineHeight,
107
+ ...(align && { alignSelf: alignSelfMap[align] })
100
108
  })}
101
109
  ref={ref}
102
110
  {...presentationOnly}
@@ -132,7 +140,11 @@ ExpandCollapseMiniControl.propTypes = {
132
140
  /**
133
141
  * Optional variant object to override the default theme tokens
134
142
  */
135
- variant: PropTypes.object
143
+ variant: PropTypes.object,
144
+ /**
145
+ * Controls the horizontal alignment of the trigger label and icon
146
+ */
147
+ align: PropTypes.oneOf(['start', 'middle', 'end'])
136
148
  }
137
149
 
138
150
  export default ExpandCollapseMiniControl
@@ -34,6 +34,7 @@ const ChevronLink = React.forwardRef(
34
34
  return (
35
35
  <LinkBase
36
36
  {...otherlinkProps}
37
+ useMeasuredMobileIconLayout
37
38
  iconPosition={direction}
38
39
  tokens={getTokens}
39
40
  dataSet={dataSet}
@@ -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
@@ -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({