@telus-uds/components-base 1.39.0 → 1.41.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 (34) hide show
  1. package/CHANGELOG.md +30 -2
  2. package/component-docs.json +358 -67
  3. package/lib/A11yInfoProvider/index.js +0 -8
  4. package/lib/Button/ButtonBase.js +68 -38
  5. package/lib/ExpandCollapse/Panel.js +9 -3
  6. package/lib/Icon/IconText.js +1 -1
  7. package/lib/InputSupports/InputSupports.js +10 -3
  8. package/lib/Notification/Notification.js +21 -10
  9. package/lib/Pagination/Pagination.js +57 -14
  10. package/lib/Radio/Radio.js +5 -1
  11. package/lib/Select/Select.js +14 -4
  12. package/lib/Typography/Typography.js +14 -0
  13. package/lib-module/A11yInfoProvider/index.js +0 -7
  14. package/lib-module/Button/ButtonBase.js +68 -38
  15. package/lib-module/ExpandCollapse/Panel.js +9 -3
  16. package/lib-module/Icon/IconText.js +1 -1
  17. package/lib-module/InputSupports/InputSupports.js +10 -3
  18. package/lib-module/Notification/Notification.js +20 -10
  19. package/lib-module/Pagination/Pagination.js +56 -14
  20. package/lib-module/Radio/Radio.js +5 -1
  21. package/lib-module/Select/Select.js +14 -4
  22. package/lib-module/Typography/Typography.js +14 -0
  23. package/package.json +2 -2
  24. package/src/A11yInfoProvider/index.jsx +0 -10
  25. package/src/Button/ButtonBase.jsx +44 -26
  26. package/src/ExpandCollapse/Control.jsx +1 -0
  27. package/src/ExpandCollapse/Panel.jsx +12 -2
  28. package/src/Icon/IconText.jsx +1 -1
  29. package/src/InputSupports/InputSupports.jsx +13 -1
  30. package/src/Notification/Notification.jsx +15 -10
  31. package/src/Pagination/Pagination.jsx +63 -13
  32. package/src/Radio/Radio.jsx +5 -1
  33. package/src/Select/Select.jsx +10 -1
  34. package/src/Typography/Typography.jsx +13 -2
@@ -49,15 +49,21 @@ const selectOuterContainerStyles = ({
49
49
  })
50
50
  })
51
51
 
52
- const selectOuterWidthStyles = ({ outerBorderGap, outerBorderWidth, width }) => {
52
+ const selectOuterSizeStyles = ({ outerBorderGap, outerBorderWidth, width, height }) => {
53
53
  // The inner container's bounding box is the bounding box of the button overall
54
54
  // so this many device pixels will sit outside of the overall bounding box
55
55
  const outerBorderOffset = getOuterBorderOffset({ outerBorderGap, outerBorderWidth })
56
- const widthStyles = {}
56
+ const sizeStyles = {}
57
+ // Apply width/height tokens: number === pixels, string === explicit units e.g. %
58
+ if (typeof width === 'number' || typeof height === 'number') {
59
+ sizeStyles.width = width ? width + outerBorderOffset * 2 : width
60
+ sizeStyles.height = height ? height + outerBorderOffset * 2 : height
61
+ return sizeStyles
62
+ }
57
63
 
58
64
  if (!width) {
59
65
  return {
60
- ...widthStyles,
66
+ ...sizeStyles,
61
67
  // Wrap content, stopping a flex parent's default align-items: stretch stretching focus ring beyond content
62
68
  ...Platform.select({
63
69
  // width: fit-content isn't supported on Firefox; can't cascade props like CSS `width: fit-content; width: --moz-fit-content;`
@@ -66,26 +72,23 @@ const selectOuterWidthStyles = ({ outerBorderGap, outerBorderWidth, width }) =>
66
72
  }
67
73
  }
68
74
 
69
- // Apply width tokens: number === pixels, string === explicit units e.g. %
70
- if (typeof width === 'number') {
71
- widthStyles.width = width + outerBorderOffset * 2
72
- return widthStyles
73
- }
74
-
75
75
  // Ensure the negative margin doesn't make element off-centre
76
76
  if (Platform.OS === 'web') {
77
- widthStyles.width = `calc(${width} + ${outerBorderOffset * 2}px)`
78
- return widthStyles
77
+ sizeStyles.width = `calc(${width} + ${outerBorderOffset * 2}px)`
78
+ sizeStyles.height = `calc(${height} + ${outerBorderOffset * 2}px)`
79
+ return sizeStyles
79
80
  }
80
81
  // Can't use calc() on native but (unlike on web) flexGrow fills the container width here
81
82
  if (width === '100%') {
82
- widthStyles.flexGrow = 1
83
- return widthStyles
83
+ sizeStyles.flexGrow = 1
84
+ return sizeStyles
84
85
  }
86
+
85
87
  // Slight offset not such a concern if % width < 100%, as the button isn't centered anyway.
86
88
  // If centering support is added (e.g. alignSelf: center), set marginHorizontal 0 when centered.
87
- widthStyles.width = width
88
- return widthStyles
89
+ sizeStyles.width = width
90
+ sizeStyles.height = height
91
+ return sizeStyles
89
92
  }
90
93
 
91
94
  const selectInnerContainerStyles = ({
@@ -96,30 +99,46 @@ const selectInnerContainerStyles = ({
96
99
  paddingBottom,
97
100
  shadow,
98
101
  borderWidth,
102
+ borderLeftWidth,
103
+ borderRightWidth,
104
+ borderTopWidth,
105
+ borderBottomWidth,
99
106
  minWidth
100
107
  }) => {
101
108
  // Subtract border width from padding so overall button width/height doesn't
102
109
  // jump around if the border width changes (avoiding NaN and negative padding)
103
- const offsetBorder = (value) =>
104
- typeof value === 'number' && typeof borderWidth === 'number'
105
- ? Math.max(0, value - borderWidth)
110
+ const offsetBorder = ({ value, borderSize = borderWidth }) =>
111
+ typeof value === 'number' && typeof borderSize === 'number'
112
+ ? Math.max(0, value - borderSize)
106
113
  : value
107
114
 
108
115
  return {
109
- paddingLeft: offsetBorder(paddingLeft),
110
- paddingRight: offsetBorder(paddingRight),
111
- paddingTop: offsetBorder(paddingTop),
112
- paddingBottom: offsetBorder(paddingBottom),
116
+ paddingLeft: offsetBorder({ value: paddingLeft, borderSize: borderLeftWidth }),
117
+ paddingRight: offsetBorder({ value: paddingRight, borderSize: borderRightWidth }),
118
+ paddingTop: offsetBorder({ value: paddingTop, borderSize: borderTopWidth }),
119
+ paddingBottom: offsetBorder({ value: paddingBottom, borderSize: borderBottomWidth }),
113
120
  backgroundColor,
114
121
  minWidth,
115
122
  ...applyShadowToken(shadow)
116
123
  }
117
124
  }
118
125
 
119
- const selectBorderStyles = ({ borderColor, borderWidth, borderRadius }) => ({
126
+ const selectBorderStyles = ({
120
127
  borderColor,
121
128
  borderWidth,
122
- borderRadius
129
+ borderRadius,
130
+ borderLeftWidth,
131
+ borderRightWidth,
132
+ borderTopWidth,
133
+ borderBottomWidth
134
+ }) => ({
135
+ borderColor,
136
+ borderWidth,
137
+ borderRadius,
138
+ borderLeftWidth,
139
+ borderRightWidth,
140
+ borderTopWidth,
141
+ borderBottomWidth
123
142
  })
124
143
 
125
144
  const selectTextStyles = (
@@ -186,7 +205,7 @@ const ButtonBase = forwardRef(
186
205
  staticStyles.row,
187
206
  selectWebOnlyStyles(inactive, themeTokens, systemProps),
188
207
  selectOuterContainerStyles(themeTokens),
189
- selectOuterWidthStyles(themeTokens)
208
+ selectOuterSizeStyles(themeTokens)
190
209
  ]
191
210
  }
192
211
  const { themeOptions } = useTheme()
@@ -269,7 +288,6 @@ const staticStyles = StyleSheet.create({
269
288
  flexDirection: 'row'
270
289
  },
271
290
  text: {
272
- flexGrow: 1, // On native but not web, flexShrink here wraps text prematurely
273
291
  ...Platform.select({
274
292
  // TODO: https://github.com/telus/universal-design-system/issues/487
275
293
  web: { transition: 'color 200ms' }
@@ -80,6 +80,7 @@ const ExpandCollapseControl = forwardRef(
80
80
  <Pressable ref={ref} {...selectedProps} onPress={onPress} style={getPressableStyle}>
81
81
  {(pressableState) => {
82
82
  const { icon: IconComponent, ...themeTokens } = getControlTokens(pressableState)
83
+
83
84
  return (
84
85
  <>
85
86
  {IconComponent && (
@@ -17,7 +17,6 @@ import {
17
17
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
18
18
 
19
19
  // just void functions for now, since there are no style tokens for a panel or control at this point
20
- const selectGroupStyles = () => {}
21
20
  const selectContainerStyles = ({
22
21
  contentPaddingLeft,
23
22
  contentPaddingRight,
@@ -93,16 +92,27 @@ const ExpandCollapsePanel = forwardRef(
93
92
  const focusabilityProps = isExpanded ? {} : a11yProps.nonFocusableProps
94
93
 
95
94
  return (
96
- <View ref={ref} style={selectGroupStyles(themeTokens)}>
95
+ <View ref={ref} style={themeTokens}>
97
96
  <ExpandCollapseControl
98
97
  {...selectedProps}
99
98
  isExpanded={isExpanded}
100
99
  tokens={controlTokens}
100
+ variant={variant}
101
101
  onPress={handleControlPress}
102
102
  ref={controlRef}
103
103
  >
104
104
  {control}
105
105
  </ExpandCollapseControl>
106
+ {isExpanded && (
107
+ <View
108
+ style={{
109
+ borderTopColor: themeTokens.expandDividerColor,
110
+ borderTopWidth: themeTokens.expanddDividerWidth,
111
+ marginLeft: themeTokens.contentPaddingLeft,
112
+ marginRight: themeTokens.contentPaddingRight
113
+ }}
114
+ />
115
+ )}
106
116
  <Animated.View ref={animatedRef} style={animatedStyles} {...focusabilityProps}>
107
117
  <View onLayout={onContainerLayout}>
108
118
  <View style={selectContainerStyles(themeTokens)}>{children}</View>
@@ -22,7 +22,7 @@ const IconText = forwardRef(
22
22
 
23
23
  // Inline images on Android are always baseline-aligned which makes them look misaligned - offset it.
24
24
  // See abandoned issue https://github.com/facebook/react-native/issues/6529
25
- const size = iconProps?.tokens?.size
25
+ const size = iconProps?.tokens?.size ?? 0
26
26
  const iconAdjustedAndriod = (
27
27
  <View style={{ transform: [{ translateY: size * 0.2 }] }}>{iconContent}</View>
28
28
  )
@@ -16,6 +16,7 @@ const InputSupports = forwardRef(
16
16
  hint,
17
17
  hintPosition = 'inline',
18
18
  feedback,
19
+ feedbackTokens = {},
19
20
  tooltip,
20
21
  validation,
21
22
  nativeID
@@ -46,7 +47,14 @@ const InputSupports = forwardRef(
46
47
  />
47
48
  )}
48
49
  {typeof children === 'function' ? children({ inputId, ...a11yProps }) : children}
49
- {feedback && <Feedback id={feedbackId} title={feedback} validation={validation} />}
50
+ {feedback ? (
51
+ <Feedback
52
+ id={feedbackId}
53
+ title={feedback}
54
+ validation={validation}
55
+ tokens={feedbackTokens}
56
+ />
57
+ ) : null}
50
58
  </StackView>
51
59
  )
52
60
  }
@@ -76,6 +84,10 @@ InputSupports.propTypes = {
76
84
  * Visual variant is determined based on the `validation` prop.
77
85
  */
78
86
  feedback: PropTypes.string,
87
+ /**
88
+ * Additional tokens to override the default feedback tokens.
89
+ */
90
+ feedbackTokens: PropTypes.objectOf(PropTypes.string),
79
91
  /**
80
92
  * Content of an optional `Tooltip`. If set, a tooltip button will be shown next to the label.
81
93
  */
@@ -13,9 +13,10 @@ import {
13
13
  wrapStringsInText,
14
14
  useResponsiveProp
15
15
  } from '../utils'
16
- import ButtonBase from '../Button/ButtonBase'
16
+ import IconButton from '../IconButton'
17
17
  import useCopy from '../utils/useCopy'
18
18
  import dictionary from './dictionary'
19
+ import { useViewport } from '../ViewportProvider'
19
20
 
20
21
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
21
22
 
@@ -30,7 +31,8 @@ const selectIconProps = ({ iconSize, iconColor }) => ({
30
31
  })
31
32
 
32
33
  const selectIconContainerStyles = ({ iconGap }) => ({
33
- paddingRight: iconGap
34
+ paddingRight: iconGap,
35
+ placeContent: 'center'
34
36
  })
35
37
 
36
38
  const selectDismissIconProps = ({ dismissIconSize, dismissIconColor }) => ({
@@ -39,7 +41,8 @@ const selectDismissIconProps = ({ dismissIconSize, dismissIconColor }) => ({
39
41
  })
40
42
 
41
43
  const selectDismissButtonContainerStyles = ({ dismissButtonGap }) => ({
42
- paddingLeft: dismissButtonGap
44
+ paddingLeft: dismissButtonGap,
45
+ placeContent: 'center'
43
46
  })
44
47
 
45
48
  const selectContentContainerStyle = (maxWidth) => ({
@@ -100,7 +103,8 @@ const selectContentContainerStyle = (maxWidth) => ({
100
103
  const Notification = forwardRef(
101
104
  ({ children, system, dismissible, copy = 'en', tokens, variant, ...rest }, ref) => {
102
105
  const [isDismissed, setIsDismissed] = useState(false)
103
- const themeTokens = useThemeTokens('Notification', tokens, variant, { system })
106
+ const viewport = useViewport()
107
+ const themeTokens = useThemeTokens('Notification', tokens, variant, { system, viewport })
104
108
  const getCopy = useCopy({ dictionary, copy })
105
109
  const { themeOptions } = useTheme()
106
110
  const contentMaxWidth = useResponsiveProp(themeOptions?.contentMaxWidth)
@@ -116,11 +120,10 @@ const Notification = forwardRef(
116
120
  { style: textStyles }
117
121
  )
118
122
 
119
- const { icon: IconComponent, dismissIcon: DismissIconComponent } = themeTokens
123
+ const { icon: IconComponent, dismissIcon: DismissIconComponent, dismissIconColor } = themeTokens
120
124
 
121
125
  const onDismissPress = () => setIsDismissed(true)
122
126
 
123
- // TODO: replace the dismiss button with IconButton when implemented (https://github.com/telus/universal-design-system/issues/281)
124
127
  return (
125
128
  <View
126
129
  ref={ref}
@@ -138,13 +141,16 @@ const Notification = forwardRef(
138
141
  </View>
139
142
  {dismissible && DismissIconComponent && (
140
143
  <View style={selectDismissButtonContainerStyles(themeTokens)}>
141
- <ButtonBase
144
+ <IconButton
145
+ action="close"
142
146
  onPress={onDismissPress}
147
+ icon={DismissIconComponent}
143
148
  accessibilityRole="button"
144
149
  accessibilityLabel={getCopy('dismiss')}
150
+ variant={{ inverse: dismissIconColor === '#ffffff', size: 'small' }}
145
151
  >
146
152
  {() => <DismissIconComponent {...selectDismissIconProps(themeTokens)} />}
147
- </ButtonBase>
153
+ </IconButton>
148
154
  </View>
149
155
  )}
150
156
  </View>
@@ -183,8 +189,7 @@ export default Notification
183
189
 
184
190
  const staticStyles = StyleSheet.create({
185
191
  container: {
186
- flexDirection: 'row',
187
- justifyContent: 'center'
192
+ flexDirection: 'row'
188
193
  },
189
194
  contentContainer: {
190
195
  flexDirection: 'row',
@@ -52,6 +52,21 @@ const Pagination = forwardRef(
52
52
  })
53
53
  const { themeOptions } = useTheme()
54
54
 
55
+ const {
56
+ borderColor,
57
+ borderWidth,
58
+ borderLeftWidth,
59
+ borderRightWidth,
60
+ borderTopWidth,
61
+ borderBottomWidth,
62
+ ellipsisPadding,
63
+ ellipsisBorderRightWidth,
64
+ ellipsisBorderLeftWidth,
65
+ ellipsisBorderTopWidth,
66
+ ellipsisBorderBottomWidth,
67
+ ellipsisHeight
68
+ } = themeTokens
69
+
55
70
  const items = React.Children.toArray(children)
56
71
 
57
72
  const {
@@ -69,6 +84,17 @@ const Pagination = forwardRef(
69
84
  })
70
85
 
71
86
  const ellipsisTextStyles = selectTextStyles(themeTokens, themeOptions)
87
+ const ellipisContainerStyles = StyleSheet.create({
88
+ container: {
89
+ height: ellipsisHeight,
90
+ padding: ellipsisPadding,
91
+ borderRightWidth: ellipsisBorderRightWidth,
92
+ borderLeftWidth: ellipsisBorderLeftWidth,
93
+ borderTopWidth: ellipsisBorderTopWidth,
94
+ borderBottomWidth: ellipsisBorderBottomWidth,
95
+ borderColor
96
+ }
97
+ })
72
98
 
73
99
  if (items.length === 0) {
74
100
  return null
@@ -110,9 +136,15 @@ const Pagination = forwardRef(
110
136
 
111
137
  if (shouldRenderEllipsis(itemIndex)) {
112
138
  return (
113
- <Text key="..." style={ellipsisTextStyles}>
114
- ...
115
- </Text>
139
+ <View key="..." style={{ ...ellipisContainerStyles.container }}>
140
+ <Text
141
+ style={{
142
+ ...ellipsisTextStyles
143
+ }}
144
+ >
145
+ ...
146
+ </Text>
147
+ </View>
116
148
  )
117
149
  }
118
150
 
@@ -132,16 +164,34 @@ const Pagination = forwardRef(
132
164
  ]
133
165
 
134
166
  return (
135
- <View style={staticStyles.container} ref={ref} {...selectProps(rest)}>
136
- {buttons
137
- // keep the keys in-line with the page numbers regardless of which buttons are actually rendered
138
- .map((element, index) => [element, `page-${index + 1}`])
139
- .filter(([element]) => element !== null)
140
- .map(([element, key]) => (
141
- <Box right={gap} key={key}>
142
- {element}
143
- </Box>
144
- ))}
167
+ <View
168
+ style={{
169
+ ...staticStyles.container
170
+ }}
171
+ ref={ref}
172
+ {...selectProps(rest)}
173
+ >
174
+ <View
175
+ style={{
176
+ borderColor,
177
+ borderWidth,
178
+ borderLeftWidth,
179
+ borderRightWidth,
180
+ borderTopWidth,
181
+ borderBottomWidth,
182
+ ...staticStyles.container
183
+ }}
184
+ >
185
+ {buttons
186
+ // keep the keys in-line with the page numbers regardless of which buttons are actually rendered
187
+ .map((element, index) => [element, `page-${index + 1}`])
188
+ .filter(([element]) => element !== null)
189
+ .map(([element, key]) => (
190
+ <Box right={gap} key={key}>
191
+ {element}
192
+ </Box>
193
+ ))}
194
+ </View>
145
195
  </View>
146
196
  )
147
197
  }
@@ -47,6 +47,8 @@ const selectDescriptionStyles = ({
47
47
  descriptionFontSize,
48
48
  descriptionLineHeight,
49
49
  descriptionMarginLeft,
50
+ descriptionFontName,
51
+ descriptionFontWeight,
50
52
  inputSize,
51
53
  fontColor,
52
54
  labelMarginLeft = 0
@@ -55,7 +57,9 @@ const selectDescriptionStyles = ({
55
57
  color: fontColor,
56
58
  ...applyTextStyles({
57
59
  fontSize: descriptionFontSize,
58
- lineHeight: descriptionLineHeight
60
+ lineHeight: descriptionLineHeight,
61
+ fontName: descriptionFontName,
62
+ fontWeight: descriptionFontWeight
59
63
  })
60
64
  })
61
65
  const selectLabelStyles = ({
@@ -142,6 +142,10 @@ const selectValidationIconContainerStyles = ({
142
142
  : { paddingBottom })
143
143
  })
144
144
 
145
+ const selectCustomFeedbackStyles = ({ feedbackBackgroundColor }) => ({
146
+ backgroundColor: feedbackBackgroundColor
147
+ })
148
+
145
149
  /**
146
150
  * A basic form single-choice select component. Use in forms or individually to receive user's input.
147
151
  *
@@ -221,7 +225,12 @@ const Select = forwardRef(
221
225
  const { themeOptions } = useTheme()
222
226
 
223
227
  return (
224
- <InputSupports {...supportsProps} {...selectedProps} validation={validation}>
228
+ <InputSupports
229
+ {...supportsProps}
230
+ {...selectedProps}
231
+ validation={validation}
232
+ feedbackTokens={selectCustomFeedbackStyles(themeTokens)}
233
+ >
225
234
  {({ inputId, ...props }) => (
226
235
  <View style={selectOuterBorderStyles(themeTokens)}>
227
236
  <Picker
@@ -53,6 +53,7 @@ const Typography = forwardRef(
53
53
  align = undefined,
54
54
  tokens,
55
55
  dataSet,
56
+ strikeThrough = false,
56
57
  ...rest
57
58
  },
58
59
  ref
@@ -100,12 +101,18 @@ const Typography = forwardRef(
100
101
  return resetTagStyling(children)
101
102
  }
102
103
 
104
+ const textDecorationLine = strikeThrough ? 'line-through' : 'none'
105
+ const textStyles = resolvedTextProps.style
106
+ ? { ...resolvedTextProps.style, textDecorationLine }
107
+ : { textDecorationLine }
103
108
  return block ? (
104
109
  <View ref={ref} {...containerProps}>
105
- <Text {...resolvedTextProps}>{sanitizeChildren(children)}</Text>
110
+ <Text {...resolvedTextProps} style={textStyles}>
111
+ {sanitizeChildren(children)}
112
+ </Text>
106
113
  </View>
107
114
  ) : (
108
- <Text ref={ref} {...containerProps} {...resolvedTextProps}>
115
+ <Text ref={ref} {...containerProps} {...resolvedTextProps} style={textStyles}>
109
116
  {sanitizeChildren(children)}
110
117
  </Text>
111
118
  )
@@ -118,6 +125,10 @@ Typography.propTypes = {
118
125
  ...selectedTextPropTypes,
119
126
  tokens: getTokensPropType('Typography'),
120
127
  variant: variantProp.propType,
128
+ /**
129
+ * Renders the text with "text-decoration: 'line-through'" applied.
130
+ */
131
+ strikeThrough: PropTypes.bool,
121
132
  /**
122
133
  * Renders the text as a semantic heading. If a html heading tag is provided, uses that tag on Web.
123
134
  *