@telus-uds/components-base 1.5.0 → 1.7.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 (125) hide show
  1. package/.turbo/turbo-build.log +8 -8
  2. package/.turbo/turbo-lint.log +13 -0
  3. package/CHANGELOG.json +154 -1
  4. package/CHANGELOG.md +47 -2
  5. package/__tests__/FlexGrid/Row.test.jsx +100 -25
  6. package/__tests__/utils/containUniqueFields.test.js +25 -0
  7. package/component-docs.json +78 -26
  8. package/generate-component-docs.js +20 -7
  9. package/lib/Button/ButtonBase.js +1 -1
  10. package/lib/Button/ButtonGroup.js +20 -12
  11. package/lib/Card/PressableCardBase.js +1 -1
  12. package/lib/Checkbox/Checkbox.js +27 -16
  13. package/lib/Checkbox/CheckboxGroup.js +19 -5
  14. package/lib/ExpandCollapse/Panel.js +10 -10
  15. package/lib/FlexGrid/Col/Col.js +13 -3
  16. package/lib/FlexGrid/Row/Row.js +8 -2
  17. package/lib/InputLabel/InputLabel.js +27 -25
  18. package/lib/Link/LinkBase.js +19 -6
  19. package/lib/Link/TextButton.js +1 -10
  20. package/lib/List/ListItem.js +22 -12
  21. package/lib/Modal/Modal.js +18 -18
  22. package/lib/Radio/Radio.js +23 -12
  23. package/lib/Radio/RadioGroup.js +12 -3
  24. package/lib/RadioCard/RadioCard.js +1 -1
  25. package/lib/RadioCard/RadioCardGroup.js +11 -2
  26. package/lib/Search/Search.js +27 -19
  27. package/lib/Select/Select.js +2 -3
  28. package/lib/Tags/Tags.js +23 -17
  29. package/lib/TextInput/TextArea.js +2 -2
  30. package/lib/TextInput/TextInput.js +12 -2
  31. package/lib/TextInput/TextInputBase.js +1 -1
  32. package/lib/TextInput/propTypes.js +8 -1
  33. package/lib/ToggleSwitch/ToggleSwitch.js +5 -2
  34. package/lib/ToggleSwitch/ToggleSwitchGroup.js +20 -12
  35. package/lib/Typography/Typography.js +12 -10
  36. package/lib/index.js +22 -1
  37. package/lib/utils/containUniqueFields.js +34 -0
  38. package/lib/utils/index.js +10 -1
  39. package/lib/utils/input.js +5 -6
  40. package/lib/utils/props/handlerProps.js +72 -0
  41. package/lib/utils/props/index.js +32 -0
  42. package/lib/utils/props/inputSupportsProps.js +3 -5
  43. package/lib/utils/props/linkProps.js +3 -7
  44. package/lib/utils/props/textInputProps.js +206 -0
  45. package/lib/utils/props/textProps.js +72 -0
  46. package/lib-module/Button/ButtonBase.js +2 -2
  47. package/lib-module/Button/ButtonGroup.js +15 -6
  48. package/lib-module/Card/PressableCardBase.js +2 -2
  49. package/lib-module/Checkbox/Checkbox.js +28 -17
  50. package/lib-module/Checkbox/CheckboxGroup.js +20 -7
  51. package/lib-module/ExpandCollapse/Panel.js +10 -10
  52. package/lib-module/FlexGrid/Col/Col.js +13 -3
  53. package/lib-module/FlexGrid/Row/Row.js +8 -2
  54. package/lib-module/InputLabel/InputLabel.js +28 -25
  55. package/lib-module/Link/LinkBase.js +19 -6
  56. package/lib-module/Link/TextButton.js +1 -10
  57. package/lib-module/List/ListItem.js +22 -12
  58. package/lib-module/Modal/Modal.js +19 -19
  59. package/lib-module/Radio/Radio.js +24 -13
  60. package/lib-module/Radio/RadioGroup.js +13 -4
  61. package/lib-module/RadioCard/RadioCard.js +2 -2
  62. package/lib-module/RadioCard/RadioCardGroup.js +12 -3
  63. package/lib-module/Search/Search.js +29 -21
  64. package/lib-module/Select/Select.js +2 -3
  65. package/lib-module/Tags/Tags.js +18 -11
  66. package/lib-module/TextInput/TextArea.js +3 -3
  67. package/lib-module/TextInput/TextInput.js +11 -3
  68. package/lib-module/TextInput/TextInputBase.js +2 -2
  69. package/lib-module/TextInput/propTypes.js +7 -1
  70. package/lib-module/ToggleSwitch/ToggleSwitch.js +6 -3
  71. package/lib-module/ToggleSwitch/ToggleSwitchGroup.js +15 -6
  72. package/lib-module/Typography/Typography.js +13 -11
  73. package/lib-module/index.js +1 -1
  74. package/lib-module/utils/containUniqueFields.js +26 -0
  75. package/lib-module/utils/index.js +2 -1
  76. package/lib-module/utils/input.js +6 -6
  77. package/lib-module/utils/props/handlerProps.js +59 -0
  78. package/lib-module/utils/props/index.js +3 -0
  79. package/lib-module/utils/props/inputSupportsProps.js +3 -5
  80. package/lib-module/utils/props/linkProps.js +3 -7
  81. package/lib-module/utils/props/textInputProps.js +193 -0
  82. package/lib-module/utils/props/textProps.js +59 -0
  83. package/package.json +5 -5
  84. package/src/Button/ButtonBase.jsx +8 -2
  85. package/src/Button/ButtonGroup.jsx +51 -34
  86. package/src/Card/PressableCardBase.jsx +6 -1
  87. package/src/Checkbox/Checkbox.jsx +35 -23
  88. package/src/Checkbox/CheckboxGroup.jsx +52 -22
  89. package/src/ExpandCollapse/Panel.jsx +9 -9
  90. package/src/FlexGrid/Col/Col.jsx +11 -2
  91. package/src/FlexGrid/Row/Row.jsx +8 -2
  92. package/src/InputLabel/InputLabel.jsx +36 -27
  93. package/src/Link/LinkBase.jsx +20 -4
  94. package/src/Link/TextButton.jsx +1 -19
  95. package/src/List/ListItem.jsx +17 -9
  96. package/src/Modal/Modal.jsx +30 -26
  97. package/src/Radio/Radio.jsx +26 -14
  98. package/src/Radio/RadioGroup.jsx +39 -21
  99. package/src/RadioCard/RadioCard.jsx +6 -1
  100. package/src/RadioCard/RadioCardGroup.jsx +17 -1
  101. package/src/Search/Search.jsx +33 -21
  102. package/src/Select/Select.jsx +2 -2
  103. package/src/Tags/Tags.jsx +23 -9
  104. package/src/TextInput/TextArea.jsx +7 -1
  105. package/src/TextInput/TextInput.jsx +15 -3
  106. package/src/TextInput/TextInputBase.jsx +8 -1
  107. package/src/TextInput/propTypes.js +7 -1
  108. package/src/ToggleSwitch/ToggleSwitch.jsx +11 -2
  109. package/src/ToggleSwitch/ToggleSwitchGroup.jsx +19 -6
  110. package/src/Typography/Typography.jsx +13 -9
  111. package/src/index.js +4 -1
  112. package/src/utils/containUniqueFields.js +32 -0
  113. package/src/utils/index.js +1 -0
  114. package/src/utils/input.js +5 -7
  115. package/src/utils/props/handlerProps.js +47 -0
  116. package/src/utils/props/index.js +3 -0
  117. package/src/utils/props/inputSupportsProps.js +3 -4
  118. package/src/utils/props/linkProps.js +3 -6
  119. package/src/utils/props/textInputProps.js +177 -0
  120. package/src/utils/props/textProps.js +58 -0
  121. package/stories/InputLabel/InputLabel.stories.jsx +25 -28
  122. package/stories/Modal/Modal.stories.jsx +25 -0
  123. package/stories/Search/Search.stories.jsx +53 -3
  124. package/stories/TextInput/TextInput.stories.jsx +40 -2
  125. package/__tests__/Link/LinkBase.test.jsx +0 -22
@@ -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
 
@@ -16,10 +16,11 @@ import {
16
16
  selectSystemProps,
17
17
  useCopy,
18
18
  variantProp,
19
- viewProps
19
+ viewProps,
20
+ selectTokens
20
21
  } from '../utils'
21
22
  import { useViewport } from '../ViewportProvider'
22
- import ButtonBase from '../Button/ButtonBase'
23
+ import IconButton from '../IconButton'
23
24
  import dictionary from './dictionary'
24
25
 
25
26
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
@@ -70,11 +71,6 @@ const selectCloseButtonContainerStyles = ({ paddingRight, paddingTop }) => ({
70
71
  paddingTop
71
72
  })
72
73
 
73
- const selectCloseIconProps = ({ closeIconSize, closeIconColor }) => ({
74
- size: closeIconSize,
75
- color: closeIconColor
76
- })
77
-
78
74
  /**
79
75
  * A modal window is a secondary window that opens on top of the main one.
80
76
  * Users have to interact with it before they can carry out their task and return to the main window.
@@ -90,7 +86,7 @@ const selectCloseIconProps = ({ closeIconSize, closeIconColor }) => ({
90
86
  * - Don’t use modals consecutively
91
87
  */
92
88
  const Modal = forwardRef(
93
- ({ children, isOpen, onClose, maxWidth, tokens, variant, copy, ...rest }, ref) => {
89
+ ({ children, isOpen, onClose, maxWidth, tokens, variant, copy, closeButton, ...rest }, ref) => {
94
90
  const viewport = useViewport()
95
91
  const themeTokens = useThemeTokens('Modal', tokens, variant, { viewport, maxWidth })
96
92
 
@@ -107,11 +103,14 @@ const Modal = forwardRef(
107
103
  if (event.key === 'Escape') onClose()
108
104
  }
109
105
 
106
+ // Show the custom react node passed to `closedButton` or the default close button if `closeButton` is `undefined`.
107
+ // Hide the close button if `closeButton` is `null`.
108
+ const showCloseButton = closeButton !== null
109
+
110
110
  if (!isOpen) {
111
111
  return null
112
112
  }
113
113
 
114
- // TODO: replace the close button with IconButton when implemented (https://github.com/telus/universal-design-system/issues/281)
115
114
  return (
116
115
  <NativeModal transparent {...selectProps(rest)}>
117
116
  <View style={[staticStyles.positioningContainer]}>
@@ -124,23 +123,24 @@ const Modal = forwardRef(
124
123
  style={[staticStyles.modal, selectModalStyles(themeTokens)]}
125
124
  onKeyUp={handleKeyUp}
126
125
  >
127
- <View
128
- style={[
129
- staticStyles.closeButtonContainer,
130
- selectCloseButtonContainerStyles(themeTokens)
131
- ]}
132
- >
133
- <ButtonBase
134
- onPress={handleClose}
135
- accessibilityRole="button"
136
- accessibilityLabel={closeLabel}
126
+ {showCloseButton && (
127
+ <View
128
+ style={[
129
+ staticStyles.closeButtonContainer,
130
+ selectCloseButtonContainerStyles(themeTokens)
131
+ ]}
137
132
  >
138
- {
139
- // TODO: add close button interactive states after IconButton is done
140
- () => <CloseIconComponent {...selectCloseIconProps(themeTokens)} />
141
- }
142
- </ButtonBase>
143
- </View>
133
+ {closeButton || (
134
+ <IconButton
135
+ onPress={handleClose}
136
+ icon={CloseIconComponent}
137
+ accessibilityRole="button"
138
+ accessibilityLabel={closeLabel}
139
+ tokens={selectTokens('IconButton', themeTokens, 'close')}
140
+ />
141
+ )}
142
+ </View>
143
+ )}
144
144
  {children}
145
145
  </View>
146
146
  </View>
@@ -164,7 +164,11 @@ Modal.propTypes = {
164
164
  onClose: PropTypes.func,
165
165
  maxWidth: PropTypes.bool,
166
166
  tokens: getTokensPropType('Modal'),
167
- variant: variantProp.propType
167
+ variant: variantProp.propType,
168
+ /**
169
+ * Pass a react node to override the default close button or pass `null` to hide the close button.
170
+ */
171
+ closeButton: PropTypes.node
168
172
  }
169
173
 
170
174
  export default Modal
@@ -7,6 +7,7 @@ import RadioButton, { selectRadioButtonTokens } from './RadioButton'
7
7
  import { applyShadowToken, applyTextStyles, useThemeTokensCallback } from '../ThemeProvider'
8
8
  import {
9
9
  a11yProps,
10
+ focusHandlerProps,
10
11
  getTokensPropType,
11
12
  selectSystemProps,
12
13
  useInputValue,
@@ -16,7 +17,11 @@ import {
16
17
  } from '../utils'
17
18
  import StackView from '../StackView'
18
19
 
19
- const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
20
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([
21
+ a11yProps,
22
+ focusHandlerProps,
23
+ viewProps
24
+ ])
20
25
 
21
26
  const selectContainerStyles = ({
22
27
  containerBackgroundColor,
@@ -154,23 +159,29 @@ const Radio = forwardRef(
154
159
  >
155
160
  {({ focused: focus, hovered: hover, pressed }) => {
156
161
  const stateTokens = getTokens({ focus, hover, pressed })
162
+ const labelStyles = selectLabelStyles(stateTokens)
163
+ const alignWithLabel = label
164
+ ? [staticStyles.alignWithLabel, { height: labelStyles.lineHeight }]
165
+ : null
157
166
 
158
167
  return (
159
168
  <StackView space={0}>
160
169
  <View style={[staticStyles.container, selectContainerStyles(stateTokens)]}>
161
- <RadioButton
162
- tokens={selectRadioButtonTokens(stateTokens)}
163
- isControlled={isControlled}
164
- isChecked={isChecked}
165
- inactive={inactive}
166
- defaultChecked={defaultChecked}
167
- inputId={inputId}
168
- handleChange={handleChange}
169
- name={inputName}
170
- value={value}
171
- />
170
+ <View style={alignWithLabel}>
171
+ <RadioButton
172
+ tokens={selectRadioButtonTokens(stateTokens)}
173
+ isControlled={isControlled}
174
+ isChecked={isChecked}
175
+ inactive={inactive}
176
+ defaultChecked={defaultChecked}
177
+ inputId={inputId}
178
+ handleChange={handleChange}
179
+ name={inputName}
180
+ value={value}
181
+ />
182
+ </View>
172
183
  {Boolean(label) && (
173
- <Text style={selectLabelStyles(stateTokens)}>
184
+ <Text style={labelStyles}>
174
185
  <RadioLabel forId={inputId}>{label}</RadioLabel>
175
186
  </Text>
176
187
  )}
@@ -242,5 +253,6 @@ Radio.propTypes = {
242
253
  export default Radio
243
254
 
244
255
  const staticStyles = StyleSheet.create({
245
- container: { flexDirection: 'row', alignItems: 'center' }
256
+ container: { flexDirection: 'row', alignItems: 'center' },
257
+ alignWithLabel: { alignSelf: 'flex-start', justifyContent: 'center' }
246
258
  })
@@ -6,6 +6,8 @@ import { useViewport } from '../ViewportProvider'
6
6
  import { useThemeTokens } from '../ThemeProvider'
7
7
  import {
8
8
  a11yProps,
9
+ containUniqueFields,
10
+ focusHandlerProps,
9
11
  getTokensPropType,
10
12
  selectSystemProps,
11
13
  useInputValue,
@@ -17,6 +19,11 @@ import Radio from './Radio'
17
19
  import Fieldset from '../Fieldset'
18
20
 
19
21
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
22
+ const [selectItemProps, selectedItemPropTypes] = selectSystemProps([
23
+ a11yProps,
24
+ focusHandlerProps,
25
+ viewProps
26
+ ])
20
27
 
21
28
  /**
22
29
  * A group of Radios that behave as a radio group. Use when users select a single choice from mutually
@@ -100,28 +107,38 @@ const RadioGroup = forwardRef(
100
107
  onChange,
101
108
  readOnly: readOnly || inactive
102
109
  })
103
- const radios = items.map(({ label, id, onChange: itemOnChange, ref: itemRef }, index) => {
104
- const radioId = id || `Radio[${index}]`
105
- const isChecked = currentValue === radioId
106
- const handleChange = (newCheckedState, event) => {
107
- if (typeof itemOnChange === 'function') itemOnChange(newCheckedState, event)
108
- if (newCheckedState) setValue(radioId, event)
110
+
111
+ const uniqueFields = ['id', 'label']
112
+ if (!containUniqueFields(items, uniqueFields)) {
113
+ throw new Error(`RadioGroup items must have unique ${uniqueFields.join(', ')}`)
114
+ }
115
+
116
+ const radios = items.map(
117
+ ({ label, id, onChange: itemOnChange, ref: itemRef, ...itemRest }, index) => {
118
+ const radioId = id || `Radio[${index}]`
119
+ const isChecked = currentValue === radioId
120
+ const handleChange = (newCheckedState, event) => {
121
+ if (typeof itemOnChange === 'function') itemOnChange(newCheckedState, event)
122
+ if (newCheckedState) setValue(radioId, event)
123
+ }
124
+
125
+ return (
126
+ <Radio
127
+ ref={itemRef}
128
+ key={radioId}
129
+ id={radioId}
130
+ checked={isChecked}
131
+ onChange={handleChange}
132
+ inactive={inactive}
133
+ label={label}
134
+ name={inputGroupName}
135
+ tokens={radioTokens}
136
+ variant={variant}
137
+ {...selectItemProps(itemRest)}
138
+ />
139
+ )
109
140
  }
110
- return (
111
- <Radio
112
- ref={itemRef}
113
- key={radioId}
114
- id={radioId}
115
- checked={isChecked}
116
- onChange={handleChange}
117
- inactive={inactive}
118
- label={label}
119
- name={inputGroupName}
120
- tokens={radioTokens}
121
- variant={variant}
122
- />
123
- )
124
- })
141
+ )
125
142
 
126
143
  return (
127
144
  <Fieldset
@@ -163,6 +180,7 @@ RadioGroup.propTypes = {
163
180
  */
164
181
  items: PropTypes.arrayOf(
165
182
  PropTypes.exact({
183
+ ...selectedItemPropTypes,
166
184
  label: PropTypes.string,
167
185
  id: PropTypes.string,
168
186
  onChange: PropTypes.func,
@@ -5,6 +5,7 @@ import { StyleSheet, Text, View } from 'react-native'
5
5
  import { useThemeTokensCallback, applyTextStyles } from '../ThemeProvider'
6
6
  import {
7
7
  a11yProps,
8
+ focusHandlerProps,
8
9
  getTokensPropType,
9
10
  selectSystemProps,
10
11
  selectTokens,
@@ -17,7 +18,11 @@ import { PressableCardBase, selectPressableCardTokens } from '../Card'
17
18
  import StackView from '../StackView'
18
19
  import RadioButton, { selectRadioButtonTokens } from '../Radio/RadioButton'
19
20
 
20
- const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
21
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([
22
+ a11yProps,
23
+ focusHandlerProps,
24
+ viewProps
25
+ ])
21
26
 
22
27
  /**
23
28
  * A Card that behaves like a radio button. Use when users select a single choice from mutually exclusive options
@@ -5,6 +5,8 @@ import { useViewport } from '../ViewportProvider'
5
5
  import { useThemeTokens } from '../ThemeProvider'
6
6
  import {
7
7
  a11yProps,
8
+ containUniqueFields,
9
+ focusHandlerProps,
8
10
  getTokensPropType,
9
11
  selectSystemProps,
10
12
  useInputValue,
@@ -16,6 +18,11 @@ import RadioCard from './RadioCard'
16
18
  import Fieldset from '../Fieldset'
17
19
 
18
20
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
21
+ const [selectItemProps, selectedItemPropTypes] = selectSystemProps([
22
+ a11yProps,
23
+ focusHandlerProps,
24
+ viewProps
25
+ ])
19
26
 
20
27
  /**
21
28
  * A group of Cards that behave as a radio button group. Use when users select a single choice from mutually
@@ -105,6 +112,12 @@ const RadioCardGroup = forwardRef(
105
112
  // Needs 'radiogroup' role on direct parent of radios for MacOS Voiceover's numbering to work,
106
113
  // and also needs 'radiogroup' role on fieldset for correct description on focusing the set.
107
114
  // TODO: test this on more web screen readers.
115
+
116
+ const uniqueFields = ['id']
117
+ if (!containUniqueFields(items, uniqueFields)) {
118
+ throw new Error(`RadioCardGroup items must have unique ${uniqueFields.join(', ')}`)
119
+ }
120
+
108
121
  return (
109
122
  <Fieldset
110
123
  ref={ref}
@@ -121,12 +134,13 @@ const RadioCardGroup = forwardRef(
121
134
  >
122
135
  {(props) => (
123
136
  <StackContainer space={space} accessibilityRole="radiogroup">
124
- {items.map(({ title, content, id, onChange: itemOnChange }, index) => {
137
+ {items.map(({ title, content, id, onChange: itemOnChange, ...itemRest }, index) => {
125
138
  const cardId = id || `RadioCard[${index}]`
126
139
  const handleChange = (newCheckedState, event) => {
127
140
  if (typeof itemOnChange === 'function') itemOnChange(newCheckedState, event)
128
141
  if (newCheckedState) setValue(cardId, event)
129
142
  }
143
+
130
144
  return (
131
145
  <RadioCard
132
146
  key={cardId}
@@ -139,6 +153,7 @@ const RadioCardGroup = forwardRef(
139
153
  tokens={radioCardTokens}
140
154
  variant={variant}
141
155
  readOnly={readOnly}
156
+ {...selectItemProps(itemRest)}
142
157
  {...props}
143
158
  >
144
159
  {content}
@@ -172,6 +187,7 @@ RadioCardGroup.propTypes = {
172
187
  */
173
188
  items: PropTypes.arrayOf(
174
189
  PropTypes.exact({
190
+ ...selectedItemPropTypes,
175
191
  title: PropTypes.string,
176
192
  content: PropTypes.node,
177
193
  id: PropTypes.string,
@@ -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.
@@ -194,7 +194,7 @@ const Select = forwardRef(
194
194
  const handleMouseOver = () => setIsHovered(true)
195
195
  const handleMouseOut = () => setIsHovered(false)
196
196
 
197
- const { props: supportsProps, ...selectedProps } = selectProps(rest)
197
+ const { supportsProps, ...selectedProps } = selectProps(rest)
198
198
 
199
199
  const themeTokens = useThemeTokens('Select', tokens, variant, {
200
200
  focus: isFocused,
@@ -236,7 +236,7 @@ const Select = forwardRef(
236
236
  selectValidationIconContainerStyles(themeTokens)
237
237
  ]}
238
238
  >
239
- <ValidationIconComponent tokens={selectValidationIconTokens(themeTokens)} />
239
+ <ValidationIconComponent {...selectValidationIconTokens(themeTokens)} />
240
240
  </View>
241
241
  )}
242
242
  {IconComponent &&
package/src/Tags/Tags.jsx CHANGED
@@ -10,17 +10,25 @@ import { useViewport } from '../ViewportProvider'
10
10
  import { useThemeTokens, useThemeTokensCallback } from '../ThemeProvider'
11
11
  import {
12
12
  a11yProps,
13
+ containUniqueFields,
14
+ focusHandlerProps,
13
15
  getTokensPropType,
14
16
  pressProps,
15
17
  selectSystemProps,
16
18
  selectTokens,
19
+ useMultipleInputValues,
17
20
  variantProp,
18
21
  viewProps
19
- } from '../utils/props'
20
- import { useMultipleInputValues } from '../utils/input'
22
+ } from '../utils'
21
23
  import { getPressHandlersWithArgs } from '../utils/pressability'
22
24
 
23
- const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, pressProps, viewProps])
25
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
26
+ const [selectItemProps, selectedItemPropTypes] = selectSystemProps([
27
+ a11yProps,
28
+ focusHandlerProps,
29
+ pressProps,
30
+ viewProps
31
+ ])
24
32
 
25
33
  const selectIconTextTokens = ({
26
34
  icon,
@@ -95,6 +103,11 @@ const Tags = forwardRef(
95
103
  })
96
104
  const itemA11yRole = 'checkbox'
97
105
 
106
+ const uniqueFields = ['id', 'label']
107
+ if (!containUniqueFields(items, uniqueFields)) {
108
+ throw new Error(`Tags items must have unique ${uniqueFields.join(', ')}`)
109
+ }
110
+
98
111
  return (
99
112
  <StackWrap
100
113
  ref={ref}
@@ -103,22 +116,22 @@ const Tags = forwardRef(
103
116
  direction={direction}
104
117
  tokens={stackTokens}
105
118
  >
106
- {items.map(({ label, id = label, accessibilityLabel, ref: itemRef }, index) => {
119
+ {items.map(({ label, id = label, ref: itemRef, ...itemRest }, index) => {
107
120
  const isSelected = currentValues.includes(id)
108
121
 
109
122
  // Pass an object of relevant component state as first argument for any passed-in press handlers
110
123
  const pressHandlers = getPressHandlersWithArgs(rest, [{ id, label, currentValues }])
111
124
 
112
125
  const handlePress = (event) => {
113
- if (pressHandlers.onPress) pressHandlers.onPress()
126
+ if (pressHandlers.onPress) pressHandlers.onPress(event)
114
127
  toggleOneValue(id, event)
115
128
  }
116
129
 
117
- const itemA11y = {
130
+ const itemProps = {
118
131
  accessibilityState: { checked: isSelected },
119
132
  accessibilityRole: itemA11yRole,
120
- accessibilityLabel,
121
- ...a11yProps.getPositionInSet(items.length, index)
133
+ ...a11yProps.getPositionInSet(items.length, index),
134
+ ...selectItemProps(itemRest)
122
135
  }
123
136
 
124
137
  return (
@@ -130,7 +143,7 @@ const Tags = forwardRef(
130
143
  tokens={getButtonTokens}
131
144
  selected={isSelected}
132
145
  inactive={inactive}
133
- {...itemA11y}
146
+ {...itemProps}
134
147
  >
135
148
  {({ textStyles, ...buttonState }) => {
136
149
  // TODO: once Icon/IconButton designs are stable, see if this sort of styling around
@@ -183,6 +196,7 @@ Tags.propTypes = {
183
196
  */
184
197
  items: PropTypes.arrayOf(
185
198
  PropTypes.shape({
199
+ ...selectedItemPropTypes,
186
200
  /**
187
201
  * The text displayed to the user in the button, describing this option,
188
202
  * passed to the button as its child.
@@ -3,9 +3,12 @@ import React, { forwardRef, useState } from 'react'
3
3
  import { Platform } from 'react-native'
4
4
  import {
5
5
  a11yProps,
6
+ focusHandlerProps,
6
7
  getTokensPropType,
7
8
  inputSupportsProps,
8
9
  selectSystemProps,
10
+ textInputHandlerProps,
11
+ textInputProps,
9
12
  variantProp,
10
13
  viewProps
11
14
  } from '../utils'
@@ -16,7 +19,10 @@ import textInputPropTypes from './propTypes'
16
19
 
17
20
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([
18
21
  a11yProps,
22
+ focusHandlerProps,
19
23
  inputSupportsProps,
24
+ textInputHandlerProps,
25
+ textInputProps,
20
26
  viewProps
21
27
  ])
22
28
 
@@ -60,7 +66,7 @@ const TextArea = forwardRef(({ tokens, variant = {}, ...rest }, ref) => {
60
66
  }
61
67
  }
62
68
 
63
- const { props: supportsProps, ...selectedProps } = selectProps(rest)
69
+ const { supportsProps, ...selectedProps } = selectProps(rest)
64
70
 
65
71
  const inputProps = {
66
72
  ...selectedProps,