@telus-uds/components-base 1.6.1 → 1.8.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 (97) hide show
  1. package/.storybook/main.js +7 -0
  2. package/.turbo/turbo-build.log +3 -3
  3. package/.turbo/turbo-lint.log +3 -13
  4. package/CHANGELOG.json +112 -1
  5. package/CHANGELOG.md +41 -2
  6. package/__fixtures__/Accessible.js +1 -2
  7. package/__fixtures__/Accessible.native.js +1 -2
  8. package/__tests__/FlexGrid/Col.test.jsx +5 -0
  9. package/__tests__/InputLabel/InputLabel.test.jsx +28 -0
  10. package/__tests__/InputLabel/__snapshots__/InputLabel.test.jsx.snap +3 -0
  11. package/__tests__/InputSupports/InputSupports.test.jsx +10 -0
  12. package/component-docs.json +278 -40
  13. package/lib/Button/ButtonGroup.js +118 -45
  14. package/lib/Checkbox/CheckboxGroup.js +3 -3
  15. package/lib/ExpandCollapse/Panel.js +2 -1
  16. package/lib/Fieldset/Fieldset.js +7 -0
  17. package/lib/InputLabel/InputLabel.js +8 -1
  18. package/lib/InputSupports/InputSupports.js +7 -0
  19. package/lib/List/ListItem.js +22 -12
  20. package/lib/Notification/Notification.js +1 -1
  21. package/lib/Radio/RadioGroup.js +12 -5
  22. package/lib/RadioCard/RadioCardGroup.js +7 -0
  23. package/lib/Search/Search.js +28 -20
  24. package/lib/Skeleton/Skeleton.js +48 -2
  25. package/lib/TextInput/TextArea.js +1 -1
  26. package/lib/TextInput/TextInput.js +1 -1
  27. package/lib/TextInput/TextInputBase.js +1 -1
  28. package/lib/ToggleSwitch/ToggleSwitch.js +7 -0
  29. package/lib/ToggleSwitch/ToggleSwitchGroup.js +7 -0
  30. package/lib/Tooltip/Tooltip.js +1 -1
  31. package/lib/Typography/Typography.js +12 -10
  32. package/lib/index.js +22 -1
  33. package/lib/utils/animation/useVerticalExpandAnimation.js +26 -13
  34. package/lib/utils/input.js +5 -6
  35. package/lib/utils/props/index.js +18 -0
  36. package/lib/utils/props/inputSupportsProps.js +7 -0
  37. package/lib/utils/props/textInputProps.js +207 -0
  38. package/lib/utils/props/textProps.js +72 -0
  39. package/lib-module/Button/ButtonGroup.js +117 -45
  40. package/lib-module/Checkbox/CheckboxGroup.js +3 -3
  41. package/lib-module/ExpandCollapse/Panel.js +2 -1
  42. package/lib-module/Fieldset/Fieldset.js +7 -0
  43. package/lib-module/InputLabel/InputLabel.js +8 -1
  44. package/lib-module/InputSupports/InputSupports.js +7 -0
  45. package/lib-module/List/ListItem.js +22 -12
  46. package/lib-module/Notification/Notification.js +1 -1
  47. package/lib-module/Radio/RadioGroup.js +12 -5
  48. package/lib-module/RadioCard/RadioCardGroup.js +7 -0
  49. package/lib-module/Search/Search.js +30 -22
  50. package/lib-module/Skeleton/Skeleton.js +49 -3
  51. package/lib-module/TextInput/TextArea.js +2 -2
  52. package/lib-module/TextInput/TextInput.js +2 -2
  53. package/lib-module/TextInput/TextInputBase.js +2 -2
  54. package/lib-module/ToggleSwitch/ToggleSwitch.js +7 -0
  55. package/lib-module/ToggleSwitch/ToggleSwitchGroup.js +7 -0
  56. package/lib-module/Tooltip/Tooltip.js +1 -1
  57. package/lib-module/Typography/Typography.js +13 -11
  58. package/lib-module/index.js +1 -1
  59. package/lib-module/utils/animation/useVerticalExpandAnimation.js +26 -14
  60. package/lib-module/utils/input.js +6 -6
  61. package/lib-module/utils/props/index.js +2 -0
  62. package/lib-module/utils/props/inputSupportsProps.js +7 -0
  63. package/lib-module/utils/props/textInputProps.js +194 -0
  64. package/lib-module/utils/props/textProps.js +59 -0
  65. package/package.json +9 -4
  66. package/src/Button/ButtonGroup.jsx +106 -41
  67. package/src/Checkbox/Checkbox.jsx +7 -4
  68. package/src/Checkbox/CheckboxGroup.jsx +3 -3
  69. package/src/ExpandCollapse/Panel.jsx +3 -1
  70. package/src/Fieldset/Fieldset.jsx +6 -0
  71. package/src/InputLabel/InputLabel.jsx +17 -2
  72. package/src/InputSupports/InputSupports.jsx +9 -1
  73. package/src/List/ListItem.jsx +17 -9
  74. package/src/Notification/Notification.jsx +1 -1
  75. package/src/Radio/Radio.jsx +5 -1
  76. package/src/Radio/RadioGroup.jsx +11 -5
  77. package/src/RadioCard/RadioCard.jsx +5 -1
  78. package/src/RadioCard/RadioCardGroup.jsx +6 -0
  79. package/src/Search/Search.jsx +34 -22
  80. package/src/Skeleton/Skeleton.jsx +56 -3
  81. package/src/TextInput/TextArea.jsx +2 -0
  82. package/src/TextInput/TextInput.jsx +2 -0
  83. package/src/TextInput/TextInputBase.jsx +2 -0
  84. package/src/ToggleSwitch/ToggleSwitch.jsx +6 -0
  85. package/src/ToggleSwitch/ToggleSwitchGroup.jsx +6 -0
  86. package/src/Tooltip/Tooltip.jsx +1 -1
  87. package/src/Typography/Typography.jsx +13 -9
  88. package/src/index.js +4 -1
  89. package/src/utils/animation/useVerticalExpandAnimation.js +25 -12
  90. package/src/utils/input.js +5 -7
  91. package/src/utils/props/index.js +2 -0
  92. package/src/utils/props/inputSupportsProps.js +6 -1
  93. package/src/utils/props/textInputProps.js +178 -0
  94. package/src/utils/props/textProps.js +58 -0
  95. package/src/utils/props/tokens.js +21 -19
  96. package/stories/Search/Search.stories.jsx +49 -2
  97. package/stories/Tabs/Tabs.stories.jsx +4 -3
@@ -22,8 +22,7 @@ const selectBulletStyles = ({ itemBulletWidth, itemBulletHeight, itemBulletColor
22
22
 
23
23
  const selectBulletContainerStyles = ({ itemBulletContainerWidth, itemBulletContainerAlign }) => ({
24
24
  width: itemBulletContainerWidth,
25
- alignItems: itemBulletContainerAlign,
26
- justifyContent: itemBulletContainerAlign
25
+ alignItems: itemBulletContainerAlign
27
26
  })
28
27
 
29
28
  const selectItemIconTokens = ({ itemIconSize, itemIconColor }) => ({
@@ -31,12 +30,15 @@ const selectItemIconTokens = ({ itemIconSize, itemIconColor }) => ({
31
30
  color: itemIconColor
32
31
  })
33
32
 
34
- const selectCommonIconStyles = ({ iconMarginTop }) => ({
35
- marginTop: iconMarginTop
33
+ const selectSideItemContainerStyles = ({ listGutter, iconMarginTop }) => ({
34
+ marginTop: iconMarginTop,
35
+ marginRight: listGutter
36
36
  })
37
37
 
38
- const selectSideItemContainerStyles = ({ listGutter }) => ({
39
- marginRight: listGutter
38
+ // Align bullets with the top line of text the same way icons are aligned
39
+ const selectBulletPositioningStyles = ({ itemIconSize }) => ({
40
+ width: itemIconSize,
41
+ height: itemIconSize
40
42
  })
41
43
 
42
44
  const selectItemStyles = ({ itemFontWeight, itemFontSize, itemLineHeight, itemFontName }) =>
@@ -73,8 +75,8 @@ const ListItem = forwardRef(
73
75
  const dividerStyles = selectDividerStyles(themeTokens)
74
76
  const itemBulletContainerStyles = selectBulletContainerStyles(themeTokens)
75
77
  const itemBulletStyles = selectBulletStyles(themeTokens)
78
+ const itemBulletPositioningStyles = selectBulletPositioningStyles(themeTokens)
76
79
  const iconTokens = selectItemIconTokens(themeTokens)
77
- const commonIconStyles = selectCommonIconStyles(themeTokens)
78
80
  const sideItemContainerStyles = selectSideItemContainerStyles(themeTokens)
79
81
  const accessibilityRole = Platform.select({ web: 'listitem', default: 'item' })
80
82
 
@@ -109,7 +111,7 @@ const ListItem = forwardRef(
109
111
 
110
112
  if (icon) {
111
113
  return (
112
- <View style={[sideItemContainerStyles, commonIconStyles]}>
114
+ <View style={sideItemContainerStyles}>
113
115
  <IconComponent
114
116
  size={iconSize || iconTokens.size}
115
117
  color={iconColor || iconTokens.color}
@@ -120,7 +122,9 @@ const ListItem = forwardRef(
120
122
 
121
123
  return (
122
124
  <View style={[sideItemContainerStyles, itemBulletContainerStyles]}>
123
- <View style={itemBulletStyles} testID="unordered-item-bullet" />
125
+ <View style={[staticStyles.bulletPositioning, itemBulletPositioningStyles]}>
126
+ <View style={itemBulletStyles} testID="unordered-item-bullet" />
127
+ </View>
124
128
  </View>
125
129
  )
126
130
  }
@@ -146,6 +150,10 @@ const staticStyles = StyleSheet.create({
146
150
  },
147
151
  wrap: {
148
152
  flex: 1
153
+ },
154
+ bulletPositioning: {
155
+ alignItems: 'center',
156
+ justifyContent: 'center'
149
157
  }
150
158
  })
151
159
 
@@ -159,7 +159,7 @@ Notification.propTypes = {
159
159
  */
160
160
  dismissible: PropTypes.bool,
161
161
  /**
162
- * Select english or french copy for the accessible label of the dismiss button.
162
+ * Select English or French copy for the accessible label of the dismiss button.
163
163
  */
164
164
  copy: PropTypes.oneOfType([
165
165
  PropTypes.oneOf(['en', 'fr']),
@@ -121,7 +121,11 @@ const Radio = forwardRef(
121
121
  },
122
122
  ref
123
123
  ) => {
124
- const { currentValue: isChecked, setValue: setIsChecked, isControlled } = useInputValue(
124
+ const {
125
+ currentValue: isChecked,
126
+ setValue: setIsChecked,
127
+ isControlled
128
+ } = useInputValue(
125
129
  {
126
130
  value: checked,
127
131
  initialValue: defaultChecked,
@@ -44,7 +44,7 @@ const [selectItemProps, selectedItemPropTypes] = selectSystemProps([
44
44
  * ### Uncontrolled version
45
45
  *
46
46
  * If the RadioGroup manages its own state, you can use `initialCheckedId` prop to provide the initial value.
47
- * Whenever the radio card gets toggled, it calls the `onChange` callback with the new value (string).
47
+ * Whenever the radio gets toggled, it calls the `onChange` callback with the new value (string).
48
48
  *
49
49
  * ### Use in forms
50
50
  *
@@ -76,6 +76,7 @@ const [selectItemProps, selectedItemPropTypes] = selectSystemProps([
76
76
  const RadioGroup = forwardRef(
77
77
  (
78
78
  {
79
+ copy = 'en',
79
80
  tokens,
80
81
  radioTokens,
81
82
  variant,
@@ -142,6 +143,7 @@ const RadioGroup = forwardRef(
142
143
 
143
144
  return (
144
145
  <Fieldset
146
+ copy={copy}
145
147
  ref={ref}
146
148
  name={inputGroupName}
147
149
  legend={legend}
@@ -163,6 +165,10 @@ RadioGroup.displayName = 'RadioGroup'
163
165
 
164
166
  RadioGroup.propTypes = {
165
167
  ...selectedSystemPropTypes,
168
+ /**
169
+ * Whether the English or French copy will be used (e.g. for accessibility labels).
170
+ */
171
+ copy: PropTypes.oneOf(['en', 'fr']),
166
172
  /**
167
173
  * Optional theme token overrides for the outer RadioGroup component
168
174
  */
@@ -208,11 +214,11 @@ RadioGroup.propTypes = {
208
214
  */
209
215
  feedback: PropTypes.string,
210
216
  /**
211
- * If provided, the radio card with this id is selected on first render.
217
+ * If provided, the radio with this id is selected on first render.
212
218
  */
213
219
  initialCheckedId: PropTypes.string,
214
220
  /**
215
- * If not undefined, the radio card with this id is selected (or none is selected if `null`), and the
221
+ * If not undefined, the radio with this id is selected (or none is selected if `null`), and the
216
222
  * element's selection state will be controlled by its parent using the `onChange` function.
217
223
  */
218
224
  checkedId: PropTypes.string,
@@ -222,11 +228,11 @@ RadioGroup.propTypes = {
222
228
  */
223
229
  onChange: PropTypes.func,
224
230
  /**
225
- * If true, the radio cards cannot be selected by the user and simply show their current state.
231
+ * If true, the radios cannot be selected by the user and simply show their current state.
226
232
  */
227
233
  readOnly: PropTypes.bool,
228
234
  /**
229
- * If true, the radio card cannot be interacted with, elements are set as `disabled` and if the
235
+ * If true, the radios cannot be interacted with, elements are set as `disabled` and if the
230
236
  * theme supports `inactive` appearances rules, these are applied.
231
237
  */
232
238
  inactive: PropTypes.bool,
@@ -77,7 +77,11 @@ const RadioCard = forwardRef(
77
77
  },
78
78
  ref
79
79
  ) => {
80
- const { currentValue: isChecked, setValue: setIsChecked, isControlled } = useInputValue({
80
+ const {
81
+ currentValue: isChecked,
82
+ setValue: setIsChecked,
83
+ isControlled
84
+ } = useInputValue({
81
85
  value: checked,
82
86
  initialValue: defaultChecked,
83
87
  onChange
@@ -77,6 +77,7 @@ const [selectItemProps, selectedItemPropTypes] = selectSystemProps([
77
77
  const RadioCardGroup = forwardRef(
78
78
  (
79
79
  {
80
+ copy = 'en',
80
81
  tokens,
81
82
  radioCardTokens,
82
83
  variant,
@@ -120,6 +121,7 @@ const RadioCardGroup = forwardRef(
120
121
 
121
122
  return (
122
123
  <Fieldset
124
+ copy={copy}
123
125
  ref={ref}
124
126
  name={inputGroupName}
125
127
  legend={legend}
@@ -170,6 +172,10 @@ RadioCardGroup.displayName = 'RadioCardGroup'
170
172
 
171
173
  RadioCardGroup.propTypes = {
172
174
  ...selectedSystemPropTypes,
175
+ /**
176
+ * Whether the English or French copy will be used (e.g. for accessibility labels).
177
+ */
178
+ copy: PropTypes.oneOf(['en', 'fr']),
173
179
  /**
174
180
  * Optional theme token overrides for the outer RadioCardGroup component
175
181
  */
@@ -1,4 +1,4 @@
1
- import React, { forwardRef, useState } from 'react'
1
+ import React, { forwardRef } from 'react'
2
2
  import { View, StyleSheet } from 'react-native'
3
3
 
4
4
  import PropTypes from 'prop-types'
@@ -8,7 +8,10 @@ import {
8
8
  getTokensPropType,
9
9
  selectSystemProps,
10
10
  selectTokens,
11
+ useInputValue,
11
12
  useSpacingScale,
13
+ textInputHandlerProps,
14
+ textInputProps,
12
15
  variantProp,
13
16
  viewProps
14
17
  } from '../utils'
@@ -18,7 +21,11 @@ import StackView from '../StackView'
18
21
  import useCopy from '../utils/useCopy'
19
22
  import dictionary from './dictionary'
20
23
 
21
- const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
24
+ const [selectContainerProps, selectedContainerPropTypes] = selectSystemProps([a11yProps, viewProps])
25
+ const [selectInputProps, selectedInputPropTypes] = selectSystemProps([
26
+ textInputHandlerProps,
27
+ textInputProps
28
+ ])
22
29
 
23
30
  const selectInputTokens = ({ searchTokens, buttonTokens, buttonsGapSize }) => {
24
31
  const { paddingRight: inputPaddingRight, clearButtonIcon, submitButtonIcon } = searchTokens
@@ -54,8 +61,9 @@ const selectIconTokens = ({ iconSize, iconColor }) => ({ color: iconColor, size:
54
61
  const Search = forwardRef(
55
62
  (
56
63
  {
57
- initialValue = '',
58
- placeholder = 'Search',
64
+ initialValue,
65
+ value,
66
+ placeholder,
59
67
  inactive,
60
68
  onChange,
61
69
  onSubmit,
@@ -68,7 +76,12 @@ const Search = forwardRef(
68
76
  },
69
77
  ref
70
78
  ) => {
71
- const [value, setValue] = useState(initialValue)
79
+ const { currentValue = '', setValue } = useInputValue({
80
+ value,
81
+ initialValue,
82
+ onChange
83
+ })
84
+
72
85
  const themeTokens = useThemeTokens('Search', tokens, variant)
73
86
  const buttonTokens = useThemeTokens('SearchButton', tokens, variant)
74
87
 
@@ -90,28 +103,26 @@ const Search = forwardRef(
90
103
 
91
104
  const handleSubmit = (event) => {
92
105
  if (onSubmit !== undefined) {
93
- onSubmit(value, event)
106
+ onSubmit(currentValue, event)
94
107
  }
95
108
  }
96
109
 
97
- const handleChange = (currentValue, event) => {
98
- setValue(currentValue, event)
99
-
100
- if (onChange !== undefined) onChange(currentValue, event)
101
- }
102
-
103
110
  const handleClear = (event) => {
104
111
  setValue('', event)
105
-
106
112
  if (onClear !== undefined) onClear('', event)
107
- if (onChange !== undefined) onChange('', event)
108
113
  }
109
114
 
110
- const isEmpty = value === ''
115
+ const isEmpty = currentValue === ''
116
+
117
+ // Accessibility label should always be present and correctly localised
118
+ const a11yLabelText = accessibilityLabel || getCopy('accessibilityLabel')
119
+ // Placeholder is optional and may be unset by passing an empty string
120
+ const placeholderText = placeholder ?? a11yLabelText
111
121
 
112
122
  return (
113
- <View style={staticStyles.container} {...selectProps(rest)}>
123
+ <View style={staticStyles.container} {...selectContainerProps(rest)}>
114
124
  <TextInputBase
125
+ {...selectInputProps(rest)}
115
126
  ref={ref}
116
127
  tokens={(appearances) =>
117
128
  selectInputTokens({
@@ -121,15 +132,15 @@ const Search = forwardRef(
121
132
  isEmpty
122
133
  })
123
134
  }
124
- placeholder={placeholder}
135
+ placeholder={placeholderText}
125
136
  placeholderTextColor={placeholderColor}
126
137
  inactive={inactive}
127
138
  enablesReturnKeyAutomatically
128
139
  returnKeyType="search"
129
- value={value}
130
- onChange={handleChange}
140
+ value={currentValue}
141
+ onChange={setValue}
131
142
  onSubmitEditing={handleSubmit}
132
- accessibilityLabel={accessibilityLabel || getCopy('accessibilityLabel')}
143
+ accessibilityLabel={a11yLabelText}
133
144
  />
134
145
  <View style={[staticStyles.iconsContainer, selectIconsContainerStyle(themeTokens)]}>
135
146
  <StackView direction="row" space={buttonsGap}>
@@ -172,7 +183,8 @@ const Search = forwardRef(
172
183
  Search.displayName = 'Search'
173
184
 
174
185
  Search.propTypes = {
175
- ...selectedSystemPropTypes,
186
+ ...selectedContainerPropTypes,
187
+ ...selectedInputPropTypes,
176
188
  /**
177
189
  * Use this to set the initial value of the search input.
178
190
  * Updating `initialValue` will **not** update the actual value.
@@ -206,7 +218,7 @@ Search.propTypes = {
206
218
  */
207
219
  accessibilityLabel: PropTypes.string,
208
220
  /**
209
- * Select english or french copy for the accessible labels.
221
+ * Select English or French copy for the accessible labels.
210
222
  * You may also pass in a custom dictionary object.
211
223
  */
212
224
  copy: PropTypes.oneOfType([
@@ -7,7 +7,10 @@ import {
7
7
  a11yProps,
8
8
  getTokensPropType,
9
9
  selectSystemProps,
10
+ responsiveProps,
11
+ useResponsiveProp,
10
12
  useSpacingScale,
13
+ spacingProps,
11
14
  variantProp,
12
15
  viewProps
13
16
  } from '../utils'
@@ -37,9 +40,29 @@ const selectSquareStyles = ({ radius }) => ({
37
40
  })
38
41
 
39
42
  const Skeleton = forwardRef(
40
- ({ tokens, variant, size, characters, lines, shape = 'line', ...rest }, ref) => {
43
+ (
44
+ {
45
+ tokens,
46
+ variant,
47
+ size,
48
+ sizeIndex = size,
49
+ sizePixels,
50
+ characters,
51
+ lines,
52
+ shape = 'line',
53
+ ...rest
54
+ },
55
+ ref
56
+ ) => {
41
57
  const themeTokens = useThemeTokens('Skeleton', tokens, variant)
42
- const skeletonHeight = useSpacingScale(size || themeTokens.size)
58
+ const pixels = useResponsiveProp(sizePixels)
59
+ const spacingScaleValue =
60
+ typeof pixels === 'number'
61
+ ? // Size by an exact number of pixels
62
+ { options: { size: pixels } }
63
+ : // Size by an index on the spacing scale (getting default index from theme if none provided)
64
+ sizeIndex || themeTokens.size
65
+ const skeletonHeight = useSpacingScale(spacingScaleValue)
43
66
  const nativeAnimation = useSkeletonNativeAnimation()
44
67
 
45
68
  const getAnimationBaseOnPlatform = () => {
@@ -97,9 +120,39 @@ Skeleton.propTypes = {
97
120
  ...selectedSystemPropTypes,
98
121
  tokens: getTokensPropType('Skeleton'),
99
122
  variant: variantProp.propType,
100
- size: propTypes.number,
123
+ /**
124
+ * Sets the size of Skeleton lines or shape according to the theme's spacing scale. For example, size={1} gives the smallest non-zero theme-defined spacing size.
125
+ *
126
+ * May also accept an object with responsive viewport keys or spacing scale options - see `useSpacingScale` for details.
127
+ */
128
+ sizeIndex: spacingProps.types.spacingValue,
129
+ /**
130
+ * @deprecated alias for `sizeIndex`
131
+ */
132
+ size: spacingProps.types.spacingValue,
133
+ /**
134
+ * Sets the size of Skeleton lines or shape to an exact number of pixels. Use when it's necessary to exactly match sizes of images or other boxes.
135
+ *
136
+ * Accepts a number or an object with responsive viewport keys, e.g. { xs: 32, lg: 64 } would be 32px at xs, sm and md and 64 at lg and xl viewports.
137
+ */
138
+ sizePixels: responsiveProps.getTypeOptionallyByViewport(propTypes.number),
139
+ /**
140
+ * Determines the width of simulated lines of text if the Skeleton's shape is 'line' (the default shape).
141
+ *
142
+ * Only has any affect if shape is line (the default). If unset, takes a default value from the theme.
143
+ */
101
144
  characters: propTypes.number,
145
+ /**
146
+ * Determines how many Skeleton items are rendered (default 1).
147
+ *
148
+ * Recommended usage is to simulate paragraphs of text when Skeleton's shape is 'line' (the default shape).
149
+ *
150
+ * The amount of spacing between multiple lines is controlled by theme tokens.
151
+ */
102
152
  lines: propTypes.number,
153
+ /**
154
+ * Determines if the skeleton should resemble lines of text (default), a circle, or a square box with themed rounded corners.
155
+ */
103
156
  shape: propTypes.oneOf(['line', 'circle', 'box'])
104
157
  }
105
158
 
@@ -8,6 +8,7 @@ import {
8
8
  inputSupportsProps,
9
9
  selectSystemProps,
10
10
  textInputHandlerProps,
11
+ textInputProps,
11
12
  variantProp,
12
13
  viewProps
13
14
  } from '../utils'
@@ -21,6 +22,7 @@ const [selectProps, selectedSystemPropTypes] = selectSystemProps([
21
22
  focusHandlerProps,
22
23
  inputSupportsProps,
23
24
  textInputHandlerProps,
25
+ textInputProps,
24
26
  viewProps
25
27
  ])
26
28
 
@@ -7,6 +7,7 @@ import {
7
7
  inputSupportsProps,
8
8
  selectSystemProps,
9
9
  textInputHandlerProps,
10
+ textInputProps,
10
11
  variantProp,
11
12
  viewProps
12
13
  } from '../utils'
@@ -19,6 +20,7 @@ const [selectProps, selectedSystemPropTypes] = selectSystemProps([
19
20
  focusHandlerProps,
20
21
  inputSupportsProps,
21
22
  textInputHandlerProps,
23
+ textInputProps,
22
24
  viewProps
23
25
  ])
24
26
 
@@ -8,6 +8,7 @@ import {
8
8
  getTokensPropType,
9
9
  selectSystemProps,
10
10
  textInputHandlerProps,
11
+ textInputProps,
11
12
  useInputValue,
12
13
  variantProp,
13
14
  viewProps
@@ -16,6 +17,7 @@ import {
16
17
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([
17
18
  a11yProps,
18
19
  textInputHandlerProps,
20
+ textInputProps,
19
21
  viewProps
20
22
  ])
21
23
 
@@ -85,6 +85,7 @@ const selectLabelTokens = ({
85
85
  const ToggleSwitch = forwardRef(
86
86
  (
87
87
  {
88
+ copy = 'en',
88
89
  value,
89
90
  initialValue,
90
91
  onChange,
@@ -119,6 +120,7 @@ const ToggleSwitch = forwardRef(
119
120
  {Boolean(label) && (
120
121
  <View style={[selectLabelStyles(themeTokens), staticStyles.containText]}>
121
122
  <InputLabel
123
+ copy={copy}
122
124
  forId={inputId}
123
125
  label={label}
124
126
  tokens={selectLabelTokens(themeTokens)}
@@ -170,6 +172,10 @@ ToggleSwitch.displayName = 'ToggleSwitch'
170
172
 
171
173
  ToggleSwitch.propTypes = {
172
174
  ...selectedSystemPropTypes,
175
+ /**
176
+ * Whether the English or French copy will be used (e.g. for accessibility labels).
177
+ */
178
+ copy: PropTypes.oneOf(['en', 'fr']),
173
179
  tokens: getTokensPropType('ToggleSwitch'),
174
180
  variant: variantProp.propType,
175
181
  /**
@@ -28,6 +28,7 @@ const [selectItemProps, selectedItemPropTypes] = selectSystemProps([
28
28
  const ToggleSwitchGroup = forwardRef(
29
29
  (
30
30
  {
31
+ copy = 'en',
31
32
  variant,
32
33
  tokens,
33
34
  items = [],
@@ -105,6 +106,7 @@ const ToggleSwitchGroup = forwardRef(
105
106
 
106
107
  return (
107
108
  <ToggleSwitch
109
+ copy={copy}
108
110
  id={id}
109
111
  ref={itemRef}
110
112
  key={id}
@@ -143,6 +145,10 @@ ToggleSwitchGroup.displayName = 'ToggleSwitchGroup'
143
145
 
144
146
  ToggleSwitchGroup.propTypes = {
145
147
  ...selectedSystemPropTypes,
148
+ /**
149
+ * Whether the English or French copy will be used (e.g. for accessibility labels).
150
+ */
151
+ copy: PropTypes.oneOf(['en', 'fr']),
146
152
  tokens: getTokensPropType('ToggleSwitchGroup'),
147
153
  variant: variantProp.propType,
148
154
  /**
@@ -276,7 +276,7 @@ Tooltip.propTypes = {
276
276
  */
277
277
  content: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
278
278
  /**
279
- * Select english or french copy for the accessible label.
279
+ * Select English or French copy for the accessible label.
280
280
  */
281
281
  copy: PropTypes.oneOf(['en', 'fr']),
282
282
  /**
@@ -13,6 +13,7 @@ import {
13
13
  headingTags,
14
14
  selectSystemProps,
15
15
  textTags,
16
+ textProps,
16
17
  viewProps,
17
18
  getA11yPropsFromHtmlTag
18
19
  } from '../utils'
@@ -20,7 +21,8 @@ import {
20
21
  * @typedef {import('../utils/a11y/semantics').TextTag} TextTag
21
22
  */
22
23
 
23
- const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
24
+ const [selectContainerProps, selectedContainerPropTypes] = selectSystemProps([a11yProps, viewProps])
25
+ const [selectTextProps, selectedTextPropTypes] = selectSystemProps([textProps])
24
26
 
25
27
  const selectTextStyles = ({
26
28
  fontWeight,
@@ -62,23 +64,24 @@ const Typography = forwardRef(
62
64
  ) => {
63
65
  const viewport = useViewport()
64
66
  const themeTokens = useThemeTokens('Typography', tokens, variant, { viewport })
65
- const textProps = {
67
+ const resolvedTextProps = {
68
+ ...selectTextProps(rest),
66
69
  style: selectTextStyles(align ? { ...themeTokens, textAlign: align } : themeTokens),
67
70
  dataSet,
68
71
  maxFontSizeMultiplier: getMaxFontMultiplier(themeTokens)
69
72
  }
70
73
 
71
- const selectedProps = selectProps({
74
+ const containerProps = {
72
75
  ...getA11yPropsFromHtmlTag(tag, accessibilityRole),
73
- ...rest
74
- })
76
+ ...selectContainerProps(rest)
77
+ }
75
78
 
76
79
  return block ? (
77
- <View ref={ref} {...selectedProps}>
78
- <Text {...textProps}>{children}</Text>
80
+ <View ref={ref} {...containerProps}>
81
+ <Text {...resolvedTextProps}>{children}</Text>
79
82
  </View>
80
83
  ) : (
81
- <Text ref={ref} {...textProps} {...selectedProps}>
84
+ <Text ref={ref} {...containerProps} {...resolvedTextProps}>
82
85
  {children}
83
86
  </Text>
84
87
  )
@@ -87,7 +90,8 @@ const Typography = forwardRef(
87
90
  Typography.displayName = 'Typography'
88
91
 
89
92
  Typography.propTypes = {
90
- ...selectedSystemPropTypes,
93
+ ...selectedContainerPropTypes,
94
+ ...selectedTextPropTypes,
91
95
  tokens: getTokensPropType('Typography'),
92
96
  variant: variantProp.propType,
93
97
  /**
package/src/index.js CHANGED
@@ -50,7 +50,10 @@ export {
50
50
  useTheme,
51
51
  useSetTheme,
52
52
  useThemeTokens,
53
- getThemeTokens
53
+ getThemeTokens,
54
+ applyOuterBorder,
55
+ applyTextStyles,
56
+ applyShadowToken
54
57
  } from './ThemeProvider'
55
58
 
56
59
  export * from './utils'
@@ -1,16 +1,30 @@
1
- import { useEffect, useRef } from 'react'
1
+ import { useEffect, useRef, useState } from 'react'
2
2
  import { Animated, Easing, Platform } from 'react-native'
3
3
 
4
4
  // TODO: systematise animations
5
5
  // https://github.com/telus/universal-design-system/issues/487
6
6
  function useVerticalExpandAnimation({ containerHeight, isExpanded, tokens }) {
7
+ const [isAnimating, setIsAnimating] = useState(false)
8
+
7
9
  const expandAnimatedValue = useRef(new Animated.Value(0)).current
10
+ const elementRef = useRef(null)
8
11
 
9
12
  const { expandDuration, collapseDuration } = tokens
10
13
 
14
+ // Treat as animating from when expanded state changes, until animation completes
15
+ useEffect(() => setIsAnimating(true), [isExpanded])
16
+
11
17
  useEffect(() => {
18
+ const onComplete = () => !isExpanded && setIsAnimating(false)
19
+
12
20
  if (Platform.OS === 'web') {
13
- return
21
+ if (!elementRef.current) return () => {}
22
+
23
+ // React Native Web does not pass `onTransitionEnd` through, must attach manually.
24
+ // https://github.com/necolas/react-native-web/pull/1713
25
+ const element = elementRef.current
26
+ element.addEventListener('transitionend', onComplete)
27
+ return () => element.removeEventListener('transitionend', onComplete)
14
28
  }
15
29
 
16
30
  const animationConfig = {
@@ -20,28 +34,27 @@ function useVerticalExpandAnimation({ containerHeight, isExpanded, tokens }) {
20
34
  useNativeDriver: false
21
35
  }
22
36
 
23
- Animated.timing(expandAnimatedValue, animationConfig).start()
37
+ const animation = Animated.timing(expandAnimatedValue, animationConfig)
38
+ animation.start(onComplete)
39
+ return () => animation.stop()
24
40
  }, [isExpanded, expandAnimatedValue, containerHeight, expandDuration, collapseDuration])
25
41
 
26
- let containerStyles
42
+ // Without `visibility: 'hidden', descendents are focusable on web even when collapsed
43
+ const containerStyles = !isExpanded && !isAnimating ? { visibility: 'hidden' } : {}
27
44
 
28
45
  // don't visually collapse the container until we have it measured
29
46
  if (containerHeight !== null) {
30
47
  if (Platform.OS === 'web') {
31
48
  const transitionDuration = isExpanded ? expandDuration : collapseDuration
49
+ containerStyles.transition = `height ${transitionDuration}ms ease-in-out`
32
50
 
33
- containerStyles = {
34
- transition: `height ${transitionDuration}ms ease-in-out`,
35
- height: isExpanded ? containerHeight : 0
36
- }
51
+ containerStyles.height = isExpanded ? containerHeight : 0
37
52
  } else {
38
- containerStyles = {
39
- height: expandAnimatedValue
40
- }
53
+ containerStyles.height = expandAnimatedValue
41
54
  }
42
55
  }
43
56
 
44
- return containerStyles
57
+ return [containerStyles, elementRef]
45
58
  }
46
59
 
47
60
  export default useVerticalExpandAnimation