@telus-uds/components-base 0.0.2-prerelease.6 → 0.0.2-prerelease.7

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 (155) hide show
  1. package/.ultra.cache.json +1 -1
  2. package/CHANGELOG.md +20 -0
  3. package/__fixtures__/testTheme.js +424 -37
  4. package/__tests__/Button/ButtonBase.test.jsx +2 -31
  5. package/__tests__/Checkbox/Checkbox.test.jsx +94 -0
  6. package/__tests__/InputSupports/InputSupports.test.jsx +50 -0
  7. package/__tests__/List/List.test.jsx +60 -0
  8. package/__tests__/Radio/Radio.test.jsx +87 -0
  9. package/__tests__/Select/Select.test.jsx +93 -0
  10. package/__tests__/Skeleton/Skeleton.test.jsx +61 -0
  11. package/__tests__/Tags/Tags.test.jsx +328 -0
  12. package/__tests__/TextInput/TextArea.test.jsx +34 -0
  13. package/__tests__/TextInput/{TextInput.test.jsx → TextInputBase.test.jsx} +20 -46
  14. package/jest.config.js +3 -1
  15. package/lib/Button/Button.js +10 -3
  16. package/lib/Button/ButtonBase.js +73 -59
  17. package/lib/Button/ButtonGroup.js +11 -27
  18. package/lib/Button/ButtonLink.js +5 -0
  19. package/lib/Checkbox/Checkbox.js +308 -0
  20. package/lib/Checkbox/CheckboxInput.native.js +6 -0
  21. package/lib/Checkbox/CheckboxInput.web.js +57 -0
  22. package/lib/Checkbox/index.js +2 -0
  23. package/lib/Feedback/Feedback.js +20 -3
  24. package/lib/Icon/Icon.js +8 -5
  25. package/lib/Icon/IconText.js +72 -0
  26. package/lib/Icon/index.js +2 -1
  27. package/lib/InputSupports/InputSupports.js +90 -0
  28. package/lib/InputSupports/index.js +2 -0
  29. package/lib/InputSupports/propTypes.js +55 -0
  30. package/lib/Link/ChevronLink.js +23 -20
  31. package/lib/Link/InlinePressable.native.js +78 -0
  32. package/lib/Link/InlinePressable.web.js +32 -0
  33. package/lib/Link/Link.js +11 -10
  34. package/lib/Link/LinkBase.js +62 -123
  35. package/lib/Link/TextButton.js +20 -9
  36. package/lib/Link/index.js +2 -1
  37. package/lib/List/List.js +52 -0
  38. package/lib/List/ListItem.js +207 -0
  39. package/lib/List/index.js +2 -0
  40. package/lib/Pagination/PageButton.js +2 -25
  41. package/lib/Pagination/SideButton.js +27 -37
  42. package/lib/Radio/Radio.js +291 -0
  43. package/lib/Radio/RadioInput.native.js +6 -0
  44. package/lib/Radio/RadioInput.web.js +59 -0
  45. package/lib/Radio/index.js +2 -0
  46. package/lib/Select/Group.native.js +14 -0
  47. package/lib/Select/Group.web.js +18 -0
  48. package/lib/Select/Item.native.js +9 -0
  49. package/lib/Select/Item.web.js +15 -0
  50. package/lib/Select/Picker.native.js +87 -0
  51. package/lib/Select/Picker.web.js +63 -0
  52. package/lib/Select/Select.js +272 -0
  53. package/lib/Select/index.js +6 -0
  54. package/lib/Skeleton/Skeleton.js +119 -0
  55. package/lib/Skeleton/index.js +2 -0
  56. package/lib/Tags/Tags.js +217 -0
  57. package/lib/Tags/index.js +2 -0
  58. package/lib/TextInput/TextArea.js +82 -0
  59. package/lib/TextInput/TextInput.js +23 -304
  60. package/lib/TextInput/TextInputBase.js +229 -0
  61. package/lib/TextInput/index.js +2 -1
  62. package/lib/TextInput/propTypes.js +31 -0
  63. package/lib/ThemeProvider/useThemeTokens.js +54 -3
  64. package/lib/ToggleSwitch/ToggleSwitch.js +1 -1
  65. package/lib/Typography/Typography.js +4 -19
  66. package/lib/index.js +8 -1
  67. package/lib/utils/a11y/index.js +1 -0
  68. package/lib/utils/a11y/textSize.js +33 -0
  69. package/lib/utils/index.js +3 -0
  70. package/lib/utils/info/index.js +7 -0
  71. package/lib/utils/info/platform/index.js +11 -0
  72. package/lib/utils/info/platform/platform.android.js +1 -0
  73. package/lib/utils/info/platform/platform.ios.js +1 -0
  74. package/lib/utils/info/platform/platform.native.js +4 -0
  75. package/lib/utils/info/platform/platform.web.js +1 -0
  76. package/lib/utils/info/versions.js +5 -0
  77. package/lib/utils/pressability.js +92 -0
  78. package/lib/utils/propTypes.js +78 -17
  79. package/package.json +5 -4
  80. package/release-context.json +4 -4
  81. package/src/Button/Button.jsx +6 -3
  82. package/src/Button/ButtonBase.jsx +66 -57
  83. package/src/Button/ButtonGroup.jsx +9 -22
  84. package/src/Button/ButtonLink.jsx +11 -2
  85. package/src/Checkbox/Checkbox.jsx +275 -0
  86. package/src/Checkbox/CheckboxInput.native.jsx +6 -0
  87. package/src/Checkbox/CheckboxInput.web.jsx +55 -0
  88. package/src/Checkbox/index.js +3 -0
  89. package/src/Feedback/Feedback.jsx +13 -4
  90. package/src/Icon/Icon.jsx +9 -5
  91. package/src/Icon/IconText.jsx +63 -0
  92. package/src/Icon/index.js +2 -1
  93. package/src/InputSupports/InputSupports.jsx +86 -0
  94. package/src/InputSupports/index.js +3 -0
  95. package/src/InputSupports/propTypes.js +44 -0
  96. package/src/Link/ChevronLink.jsx +20 -17
  97. package/src/Link/InlinePressable.native.jsx +73 -0
  98. package/src/Link/InlinePressable.web.jsx +37 -0
  99. package/src/Link/Link.jsx +17 -13
  100. package/src/Link/LinkBase.jsx +57 -140
  101. package/src/Link/TextButton.jsx +25 -11
  102. package/src/Link/index.js +2 -1
  103. package/src/List/List.jsx +47 -0
  104. package/src/List/ListItem.jsx +187 -0
  105. package/src/List/index.js +3 -0
  106. package/src/Pagination/PageButton.jsx +2 -16
  107. package/src/Pagination/SideButton.jsx +23 -34
  108. package/src/Radio/Radio.jsx +270 -0
  109. package/src/Radio/RadioInput.native.jsx +6 -0
  110. package/src/Radio/RadioInput.web.jsx +57 -0
  111. package/src/Radio/index.js +3 -0
  112. package/src/Select/Group.native.jsx +14 -0
  113. package/src/Select/Group.web.jsx +15 -0
  114. package/src/Select/Item.native.jsx +10 -0
  115. package/src/Select/Item.web.jsx +11 -0
  116. package/src/Select/Picker.native.jsx +95 -0
  117. package/src/Select/Picker.web.jsx +67 -0
  118. package/src/Select/Select.jsx +265 -0
  119. package/src/Select/index.js +8 -0
  120. package/src/Skeleton/Skeleton.jsx +101 -0
  121. package/src/Skeleton/index.js +3 -0
  122. package/src/Tags/Tags.jsx +206 -0
  123. package/src/Tags/index.js +3 -0
  124. package/src/TextInput/TextArea.jsx +78 -0
  125. package/src/TextInput/TextInput.jsx +17 -284
  126. package/src/TextInput/TextInputBase.jsx +220 -0
  127. package/src/TextInput/index.js +2 -1
  128. package/src/TextInput/propTypes.js +29 -0
  129. package/src/ThemeProvider/useThemeTokens.js +54 -3
  130. package/src/ToggleSwitch/ToggleSwitch.jsx +1 -1
  131. package/src/Typography/Typography.jsx +4 -15
  132. package/src/index.js +8 -1
  133. package/src/utils/a11y/index.js +1 -0
  134. package/src/utils/a11y/textSize.js +30 -0
  135. package/src/utils/index.js +3 -0
  136. package/src/utils/info/index.js +8 -0
  137. package/src/utils/info/platform/index.js +11 -0
  138. package/src/utils/info/platform/platform.android.js +1 -0
  139. package/src/utils/info/platform/platform.ios.js +1 -0
  140. package/src/utils/info/platform/platform.native.js +4 -0
  141. package/src/utils/info/platform/platform.web.js +1 -0
  142. package/src/utils/info/versions.js +6 -0
  143. package/src/utils/pressability.js +92 -0
  144. package/src/utils/propTypes.js +97 -22
  145. package/stories/Button/Button.stories.jsx +5 -0
  146. package/stories/Checkbox/Checkbox.stories.jsx +71 -0
  147. package/stories/Feedback/Feedback.stories.jsx +5 -6
  148. package/stories/Link/Link.stories.jsx +15 -1
  149. package/stories/List/List.stories.jsx +117 -0
  150. package/stories/Radio/Radio.stories.jsx +113 -0
  151. package/stories/Select/Select.stories.jsx +55 -0
  152. package/stories/Skeleton/Skeleton.stories.jsx +36 -0
  153. package/stories/Tags/Tags.stories.jsx +69 -0
  154. package/stories/TextInput/TextArea.stories.jsx +100 -0
  155. package/stories/supports.jsx +1 -1
@@ -1,18 +1,16 @@
1
1
  import React from 'react'
2
2
  import { Pressable, Text, View, StyleSheet, Platform } from 'react-native'
3
3
 
4
- import { useThemeTokensCallback } from '../ThemeProvider'
5
4
  import { applyTextStyles, applyShadowToken } from '../ThemeProvider/utils'
6
5
  import buttonPropTypes from './propTypes'
7
- import { a11yProps, hrefAttrsProp, linkProps } from '../utils/propTypes'
8
-
9
- const getCursorStyle = (inactive, accessibilityRole) => {
10
- if (inactive) return 'not-allowed'
11
- // These roles should result in cursor: pointer but don't in current RNW releases
12
- if (['checkbox', 'radio', 'switch'].includes(accessibilityRole)) return 'pointer'
13
- // For everything else, let React Native Web figure it out internally
14
- return undefined
15
- }
6
+ import {
7
+ a11yProps,
8
+ getCursorStyle,
9
+ hrefAttrsProp,
10
+ linkProps,
11
+ resolvePressableState,
12
+ resolvePressableTokens
13
+ } from '../utils'
16
14
 
17
15
  const getOuterBorderOffset = ({ outerBorderGap = 0, outerBorderWidth = 0 }) =>
18
16
  outerBorderGap + outerBorderWidth
@@ -39,6 +37,7 @@ const selectOuterWidthStyles = ({ outerBorderGap, outerBorderWidth, width }) =>
39
37
  // The inner container's bounding box is the bounding box of the button overall
40
38
  // so this many device pixels will sit outside of the overall bounding box
41
39
  const outerBorderOffset = getOuterBorderOffset({ outerBorderGap, outerBorderWidth })
40
+
42
41
  const widthStyles = {
43
42
  margin: 0 - outerBorderOffset
44
43
  }
@@ -83,7 +82,8 @@ const selectInnerContainerStyles = ({
83
82
  paddingTop,
84
83
  paddingBottom,
85
84
  shadow,
86
- borderWidth
85
+ borderWidth,
86
+ minWidth
87
87
  }) => {
88
88
  // Subtract border width from padding so overall button width/height doesn't
89
89
  // jump around if the border width changes (avoiding NaN and negative padding)
@@ -98,6 +98,7 @@ const selectInnerContainerStyles = ({
98
98
  paddingTop: offsetBorder(paddingTop),
99
99
  paddingBottom: offsetBorder(paddingBottom),
100
100
  backgroundColor,
101
+ minWidth,
101
102
  ...applyShadowToken(shadow)
102
103
  }
103
104
  }
@@ -118,76 +119,76 @@ const selectTextStyles = ({ fontSize, color, lineHeight, fontName, fontWeight, t
118
119
  textAlign
119
120
  })
120
121
 
121
- const selectWebOnlyStyles = (inactive, themeTokens, { accessibilityRole }) =>
122
- Platform.OS === 'web'
123
- ? {
124
- // if it would overflow the container, wraps instead
125
- maxWidth: `calc(100% + ${getOuterBorderOffset(themeTokens) * 2}px)`,
126
- cursor: getCursorStyle(inactive, accessibilityRole),
127
- outline: 'none' // removes the default browser :focus outline
128
- }
129
- : {}
122
+ const selectWebOnlyStyles = (inactive, themeTokens, { accessibilityRole }) => {
123
+ return Platform.select({
124
+ web: {
125
+ // if it would overflow the container, wraps instead
126
+ maxWidth: `calc(100% + ${getOuterBorderOffset(themeTokens) * 2}px)`,
127
+ outline: 'none', // removes the default browser :focus outline
128
+ ...getCursorStyle(inactive, accessibilityRole)
129
+ },
130
+ default: {}
131
+ })
132
+ }
133
+
134
+ // TODO: see if this can be made into a generalised utility, ideally when
135
+ // there is a stable, generic, generalised approach to within-component text
136
+ const resolveChildren = (children, { textStyles, state }) => {
137
+ switch (typeof children) {
138
+ case 'function':
139
+ return children(state)
140
+ case 'string':
141
+ return <Text style={textStyles}>{children}</Text>
142
+ default:
143
+ return children
144
+ }
145
+ }
130
146
 
131
147
  const ButtonBase = ({
132
148
  href,
133
149
  hrefAttrs,
134
150
  children,
135
- variant,
136
151
  onPress,
137
- tokens,
152
+ tokens = {},
138
153
  disabled = false, // alias for inactive
139
154
  inactive = disabled,
140
155
  selected = false,
141
156
  ...rest
142
157
  }) => {
143
- const getTokens = useThemeTokensCallback('Button', tokens, variant)
144
- const getButtonState = ({ pressed, focused, hovered }) => ({
145
- pressed,
146
- focus: focused,
147
- hover: hovered,
148
- inactive,
149
- selected
150
- })
151
- const getTokensByPressableState = (pressableState) => getTokens(getButtonState(pressableState))
158
+ const extraButtonState = { inactive, selected }
159
+ const resolveTokens = (pressableState) =>
160
+ resolvePressableTokens(tokens, pressableState, extraButtonState)
152
161
 
153
162
  const a11y = a11yProps.select(rest)
154
163
 
155
164
  const getPressableStyle = (pressableState) => {
156
- const themeTokens = getTokensByPressableState(pressableState)
165
+ const themeTokens = resolveTokens(pressableState)
157
166
  return [
158
- staticStyles.wrapper,
167
+ staticStyles.row,
159
168
  selectWebOnlyStyles(inactive, themeTokens, a11y),
160
169
  selectOuterContainerStyles(themeTokens),
161
170
  selectOuterWidthStyles(themeTokens)
162
171
  ]
163
172
  }
164
173
 
165
- const handlePress = linkProps.handleHref({ href, onPress })
166
-
167
174
  return (
168
175
  <Pressable
169
- onPress={handlePress}
176
+ href={href}
177
+ onPress={linkProps.handleHref({ href, onPress })}
170
178
  style={getPressableStyle}
171
179
  disabled={inactive}
172
- href={href}
173
180
  {...hrefAttrsProp.spread(hrefAttrs)}
174
181
  {...a11y}
175
182
  >
176
183
  {(pressableState) => {
177
- const themeTokens = getTokensByPressableState(pressableState)
184
+ const themeTokens = resolveTokens(pressableState)
178
185
  const containerStyles = selectInnerContainerStyles(themeTokens)
179
186
  const borderStyles = selectBorderStyles(themeTokens)
180
- const textStyles = {
181
- ...selectTextStyles(themeTokens),
182
- ...Platform.select({
183
- // TODO: https://github.com/telus/universal-design-system/issues/487
184
- web: { transition: 'color 200ms' }
185
- })
186
- }
187
+ const textStyles = [selectTextStyles(themeTokens), staticStyles.text]
187
188
 
188
189
  // If the container has a width set, fill it instead of sizing from content.
189
190
  // If in future we support text alignments other than center, add here.
190
- const stretchStyles = !!themeTokens.width && staticStyles.center
191
+ const stretchStyles = themeTokens.width ? staticStyles.stretch : staticStyles.align
191
192
 
192
193
  return (
193
194
  <View
@@ -195,6 +196,7 @@ const ButtonBase = ({
195
196
  containerStyles,
196
197
  borderStyles,
197
198
  stretchStyles,
199
+ staticStyles.row,
198
200
  Platform.select({
199
201
  web: {
200
202
  maxWidth: '100%', // ensure overflowing content wraps
@@ -204,14 +206,10 @@ const ButtonBase = ({
204
206
  })
205
207
  ]}
206
208
  >
207
- {typeof children === 'function' ? (
208
- children({
209
- ...getButtonState(pressableState),
210
- textStyles
211
- })
212
- ) : (
213
- <Text style={textStyles}>{children}</Text>
214
- )}
209
+ {resolveChildren(children, {
210
+ state: { ...resolvePressableState(pressableState, extraButtonState), textStyles },
211
+ textStyles
212
+ })}
215
213
  </View>
216
214
  )
217
215
  }}
@@ -226,10 +224,21 @@ ButtonBase.propTypes = {
226
224
  }
227
225
 
228
226
  const staticStyles = StyleSheet.create({
229
- wrapper: {
230
- flexDirection: 'row' // ensures alignSelf is horizontal
227
+ row: {
228
+ // Apply all button alignment horizontally; no vertical stacking within a button
229
+ flexDirection: 'row'
230
+ },
231
+ text: {
232
+ flexGrow: 1, // On native but not web, flexShrink here wraps text prematurely
233
+ ...Platform.select({
234
+ // TODO: https://github.com/telus/universal-design-system/issues/487
235
+ web: { transition: 'color 200ms' }
236
+ })
237
+ },
238
+ align: {
239
+ alignItems: 'center'
231
240
  },
232
- center: {
241
+ stretch: {
233
242
  flex: 1,
234
243
  alignItems: 'center',
235
244
  justifyContent: 'center'
@@ -5,7 +5,7 @@ import { Platform } from 'react-native'
5
5
  import ButtonBase from './ButtonBase'
6
6
  import { StackWrap } from '../StackView'
7
7
  import { useViewport } from '../ViewportProvider'
8
- import { useThemeTokens } from '../ThemeProvider'
8
+ import { useThemeTokens, useThemeTokensCallback } from '../ThemeProvider'
9
9
  import {
10
10
  a11yProps,
11
11
  pressProps,
@@ -14,10 +14,10 @@ import {
14
14
  selectTokens
15
15
  } from '../utils/propTypes'
16
16
  import { useMultipleInputValues } from '../utils/input'
17
+ import { getPressHandlersWithArgs } from '../utils/pressability'
17
18
 
18
19
  const ButtonGroup = ({
19
20
  variant,
20
- buttonVariant = {},
21
21
  tokens,
22
22
  items = [],
23
23
  values,
@@ -36,6 +36,8 @@ const ButtonGroup = ({
36
36
  const stackTokens = selectTokens('StackView', themeTokens)
37
37
  const { direction, space } = themeTokens
38
38
 
39
+ const getButtonTokens = useThemeTokensCallback('ButtonGroupItem', tokens, variant)
40
+
39
41
  const { currentValues, toggleOneValue } = useMultipleInputValues({
40
42
  initialValues,
41
43
  values,
@@ -55,15 +57,9 @@ const ButtonGroup = ({
55
57
  {items.map(({ label, id = label, accessibilityLabel }, index) => {
56
58
  const isSelected = currentValues.includes(id)
57
59
 
58
- // Allow handlers to be passed down for blur, hover, focus, pressIn, etc
59
- const pressHandlers = Object.fromEntries(
60
- Object.entries(pressProps.select(rest)).map(([key, handler]) => ({
61
- [key]: (args) => {
62
- // Pass each handler data on this button and current selection
63
- handler({ id, label, currentValues }, ...args)
64
- }
65
- }))
66
- )
60
+ // Pass an object of relevant component state as first argument for any passed-in press handlers
61
+ const pressHandlers = getPressHandlersWithArgs(rest, [{ id, label, currentValues }])
62
+
67
63
  const handlePress = () => {
68
64
  if (pressHandlers.onPress) pressHandlers.onPress()
69
65
  toggleOneValue(id)
@@ -73,15 +69,7 @@ const ButtonGroup = ({
73
69
  accessibilityState: { checked: isSelected },
74
70
  accessibilityRole: itemA11yRole,
75
71
  accessibilityLabel,
76
- ...Platform.select({
77
- web: {
78
- // accessibilityPosInSet etc exists in React Native Web main branch
79
- // but not in a release compatible with Expo etc; just use `aria-*`
80
- 'aria-setsize': items.length,
81
- 'aria-posinset': index + 1
82
- },
83
- default: {}
84
- })
72
+ ...a11yProps.getPositionInSet(items.length, index)
85
73
  }
86
74
 
87
75
  // Ensure button is direct child of group as MacOS voiceover only applies "X of Y" to
@@ -91,7 +79,7 @@ const ButtonGroup = ({
91
79
  key={id}
92
80
  {...pressHandlers}
93
81
  onPress={handlePress}
94
- variant={{ component: 'ButtonGroup', ...buttonVariant }}
82
+ tokens={getButtonTokens}
95
83
  selected={isSelected}
96
84
  inactive={inactive}
97
85
  {...itemA11y}
@@ -109,7 +97,6 @@ ButtonGroup.propTypes = {
109
97
  ...pressProps.propTypes,
110
98
  tokens: getTokensPropType('ButtonGroup'),
111
99
  variant: variantProp.propType,
112
- buttonVariant: variantProp.propType,
113
100
  /**
114
101
  * The maximum number of items a user may select at once. Defaults to 1 and behaves
115
102
  * like radio buttons. To have no limit and allow any number of selections, pass `null`.
@@ -2,14 +2,23 @@ import React from 'react'
2
2
  import ButtonBase from './ButtonBase'
3
3
  import buttonPropTypes from './propTypes'
4
4
  import { a11yProps, hrefAttrsProp, linkProps } from '../utils/propTypes'
5
+ import { useThemeTokensCallback } from '../ThemeProvider'
5
6
 
6
7
  /**
7
8
  * `ButtonLink` is a component with the semantics and behaviour of a link, but with the visual appearance of a button.
8
9
  * ButtonLink is a block-level component and should not be used inline.
9
10
  */
10
- const ButtonLink = ({ accessibilityRole = 'link', ...props }) => {
11
+ const ButtonLink = ({ accessibilityRole = 'link', tokens, variant, ...props }) => {
11
12
  const { hrefAttrs, rest } = hrefAttrsProp.bundle(props)
12
- return <ButtonBase accessibilityRole={accessibilityRole} hrefAttrs={hrefAttrs} {...rest} />
13
+ const getTokens = useThemeTokensCallback('Button', tokens, variant)
14
+ return (
15
+ <ButtonBase
16
+ accessibilityRole={accessibilityRole}
17
+ tokens={getTokens}
18
+ hrefAttrs={hrefAttrs}
19
+ {...rest}
20
+ />
21
+ )
13
22
  }
14
23
 
15
24
  ButtonLink.propTypes = {
@@ -0,0 +1,275 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { Pressable, StyleSheet, Text, View } from 'react-native'
4
+
5
+ import CheckboxInput from './CheckboxInput'
6
+ // @todo move `LabelContent` outside of the `InputLabel` and fix
7
+ // the issue with the cursor not being pointer on Web
8
+ import CheckboxLabel from '../InputLabel/LabelContent'
9
+ import Feedback from '../Feedback'
10
+ import StackView from '../StackView'
11
+ import { applyShadowToken, applyTextStyles, useThemeTokensCallback } from '../ThemeProvider'
12
+ import { getTokensPropType, useInputValue, variantProp } from '../utils'
13
+ import { a11yProps } from '../utils/propTypes'
14
+ import useUniqueId from '../utils/useUniqueId'
15
+
16
+ const selectInputStyles = (
17
+ {
18
+ iconBackgroundColor,
19
+ inputBorderColor,
20
+ inputBorderRadius,
21
+ inputBorderWidth,
22
+ inputBackgroundColor,
23
+ inputHeight,
24
+ inputOutlineColor,
25
+ inputOutlineWidth,
26
+ inputShadow,
27
+ inputWidth
28
+ },
29
+ isChecked
30
+ ) => ({
31
+ borderColor: inputBorderColor,
32
+ borderWidth: inputBorderWidth,
33
+ borderRadius: inputBorderRadius,
34
+ backgroundColor: isChecked ? iconBackgroundColor : inputBackgroundColor,
35
+ outlineStyle: 'solid',
36
+ outlineColor: inputOutlineColor,
37
+ outlineWidth: inputOutlineWidth,
38
+ height: inputHeight,
39
+ width: inputWidth,
40
+ ...applyShadowToken(inputShadow)
41
+ })
42
+ const selectLabelStyles = ({
43
+ labelColor,
44
+ labelFontName,
45
+ labelFontSize,
46
+ labelFontWeight,
47
+ labelMarginLeft,
48
+ labelLineHeight
49
+ }) => ({
50
+ marginLeft: labelMarginLeft,
51
+ ...applyTextStyles({
52
+ color: labelColor,
53
+ fontName: labelFontName,
54
+ fontWeight: labelFontWeight,
55
+ fontSize: labelFontSize,
56
+ lineHeight: labelLineHeight
57
+ })
58
+ })
59
+ const selectContainerStyles = ({ containerMarginBottom }) => ({
60
+ marginBottom: containerMarginBottom
61
+ })
62
+ const selectIconTokens = ({ iconColor, iconSize }) => ({
63
+ color: iconColor,
64
+ size: iconSize
65
+ })
66
+ const selectFeedbackTokens = ({ feedbackMarginBottom, feedbackMarginTop, feedbackPosition }) => ({
67
+ feedbackPosition,
68
+ feedbackMarginBottom,
69
+ feedbackMarginTop
70
+ })
71
+
72
+ /**
73
+ * Basic Checkbox component.
74
+ *
75
+ * ## Component API
76
+ *
77
+ * Use `label` prop to provide a label for the checkbox.
78
+ * For a disabled `Checkbox` set the `inactive` prop to `true`.
79
+ *
80
+ * ### Controlled version
81
+ *
82
+ * If the checkbox is controlled from outside, it needs to receive `checked` and `onChange` props.
83
+ *
84
+ * ### Uncontrolled version
85
+ *
86
+ * In case of uncontrolled checkbox you can use `defaultChecked` prop to provide the initial value.
87
+ * Whenever the checkbox gets toggled, it calls the `onChange` callback with the new value (boolean).
88
+ *
89
+ * ### Using within forms
90
+ *
91
+ * You can pass `name` and `value` props if you need a particular checkbox to be a part of a group of
92
+ * checkboxes on a form.
93
+ *
94
+ * ### Validation and feedback
95
+ *
96
+ * You can use the `feedback` prop to provide a comment related to the checkbox element. If the comment
97
+ * is related to a validation error, the checkbox and the feedback can be styled accordingly by setting
98
+ * the `error` prop to `true` (can also be done without feedback).
99
+ *
100
+ * ## A11y guidelines
101
+ *
102
+ * Checkbox component accepts all the common accessibility props, but also sets some defaults, including
103
+ * accessibility role `'checkbox'` and accessibility state that depends on the other props (`checked`, `inactive`)
104
+ * or the internal state in case of uncontrolled checkbox.
105
+ *
106
+ */
107
+ const Checkbox = ({
108
+ checked,
109
+ defaultChecked,
110
+ error = false,
111
+ feedback,
112
+ id,
113
+ inactive,
114
+ label,
115
+ name,
116
+ onChange,
117
+ tokens,
118
+ value,
119
+ variant,
120
+ ...rest
121
+ }) => {
122
+ const { currentValue: isChecked, setValue: setIsChecked, isControlled } = useInputValue(
123
+ {
124
+ value: checked,
125
+ initialValue: defaultChecked,
126
+ onChange
127
+ },
128
+ 'useCheckboxValue'
129
+ )
130
+ const getTokens = useThemeTokensCallback('Checkbox', tokens, {
131
+ checked: isChecked,
132
+ inactive,
133
+ error,
134
+ ...variant
135
+ })
136
+ const defaultTokens = getTokens()
137
+ const { feedbackMarginBottom, feedbackMarginTop, feedbackPosition } = selectFeedbackTokens(
138
+ defaultTokens
139
+ )
140
+ const styles = StyleSheet.create({
141
+ feedbackContainer: { marginTop: feedbackMarginTop, marginBottom: feedbackMarginBottom }
142
+ })
143
+ const handleChange = () => {
144
+ if (!inactive) {
145
+ setIsChecked(!isChecked)
146
+ }
147
+ }
148
+ const accessibilityProps = a11yProps.select(rest)
149
+ const uniqueId = useUniqueId('checkbox')
150
+ const inputId = id ?? uniqueId
151
+
152
+ // @todo our current version of React Native Web doesn't include
153
+ // keyboard support on `Pressable` (which starts with 0.15.3), so
154
+ // the complete accessibility of the `Checkbox` component on Web
155
+ // (namely, change on key pressed) is pending RNW upgrade
156
+ // (see https://github.com/necolas/react-native-web/issues/1950)
157
+ return (
158
+ <View style={staticStyles.wrapper}>
159
+ <StackView direction={feedbackPosition === 'top' ? 'column-reverse' : 'column'} space={0}>
160
+ <Pressable
161
+ disabled={inactive}
162
+ onPress={handleChange}
163
+ accessibilityRole="checkbox"
164
+ accessibilityState={{ checked: isChecked, disabled: inactive }}
165
+ {...accessibilityProps}
166
+ >
167
+ {({ focused: focus, hovered: hover, pressed }) => {
168
+ const { icon: IconComponent, ...stateTokens } = getTokens({ focus, hover, pressed })
169
+ const iconTokens = selectIconTokens(stateTokens)
170
+
171
+ return (
172
+ <View style={[staticStyles.container, selectContainerStyles(stateTokens)]}>
173
+ <View
174
+ style={StyleSheet.flatten([
175
+ staticStyles.defaultInputStyles,
176
+ selectInputStyles(stateTokens, isChecked)
177
+ ])}
178
+ testID="Checkbox-Input"
179
+ >
180
+ {/* Add a real input on Web, skip on native platforms */}
181
+ <CheckboxInput
182
+ checked={isChecked}
183
+ defaultChecked={defaultChecked}
184
+ disabled={inactive}
185
+ id={inputId}
186
+ isControlled={isControlled}
187
+ name={name}
188
+ value={value}
189
+ />
190
+ {isChecked && IconComponent && (
191
+ <IconComponent tokens={iconTokens} testID="Checkbox-Icon" />
192
+ )}
193
+ </View>
194
+ {Boolean(label) && (
195
+ <Text style={selectLabelStyles(stateTokens)}>
196
+ <CheckboxLabel forId={inputId}>{label}</CheckboxLabel>
197
+ </Text>
198
+ )}
199
+ </View>
200
+ )
201
+ }}
202
+ </Pressable>
203
+ {Boolean(feedback) && (
204
+ <View style={styles.feedbackContainer}>
205
+ <Feedback
206
+ title={feedback}
207
+ variant={{ icon: error === true }}
208
+ validation={error === true ? 'error' : undefined}
209
+ />
210
+ </View>
211
+ )}
212
+ </StackView>
213
+ </View>
214
+ )
215
+ }
216
+
217
+ Checkbox.propTypes = {
218
+ ...a11yProps.propTypes,
219
+ /**
220
+ * Use `checked` for controlled Checkbox. For uncontrolled Checkbox, use the `defaultChecked` prop.
221
+ */
222
+ checked: PropTypes.bool,
223
+ /**
224
+ * Use `defaultChecked` to provide the initial value for an uncontrolled Checkbox.
225
+ */
226
+ defaultChecked: PropTypes.bool,
227
+ /**
228
+ * A detailed description of related validation error or additional instructions (depending on the `error` prop).
229
+ */
230
+ feedback: PropTypes.string,
231
+ /**
232
+ * Checkbox ID.
233
+ */
234
+ id: PropTypes.string,
235
+ /**
236
+ * Whether the corresponding input is disabled or active.
237
+ */
238
+ inactive: PropTypes.bool,
239
+ /**
240
+ * The label.
241
+ */
242
+ label: PropTypes.string,
243
+ /**
244
+ * Associate this checkbox with a group (set as the name attribute).
245
+ */
246
+ name: PropTypes.string,
247
+ /**
248
+ * Whether the underlying input triggered a validation error or not.
249
+ */
250
+ error: PropTypes.bool,
251
+ /**
252
+ * The value. Must be unique within the group.
253
+ */
254
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
255
+ /**
256
+ * Callback called when a controlled checkbox gets interacted with.
257
+ */
258
+ onChange: PropTypes.func,
259
+ /**
260
+ * Checkbox tokens.
261
+ */
262
+ tokens: getTokensPropType('Checkbox'),
263
+ /**
264
+ * Checkbox variant.
265
+ */
266
+ variant: variantProp.propType
267
+ }
268
+
269
+ export default Checkbox
270
+
271
+ const staticStyles = StyleSheet.create({
272
+ wrapper: { backgroundColor: 'transparent' },
273
+ container: { flexDirection: 'row', alignItems: 'center' },
274
+ defaultInputStyles: { alignItems: 'center', justifyContent: 'center' }
275
+ })
@@ -0,0 +1,6 @@
1
+ /**
2
+ * There's no checkbox input on native platforms, so this is a noop.
3
+ */
4
+ const CheckboxInput = () => null
5
+
6
+ export default CheckboxInput
@@ -0,0 +1,55 @@
1
+ import React, { forwardRef } from 'react'
2
+ import PropTypes from 'prop-types'
3
+
4
+ /**
5
+ * On Web we need to include an actual input but hide it.
6
+ */
7
+ const CheckboxInput = forwardRef(
8
+ ({ checked, defaultChecked, disabled, id, isControlled, name, onChange, value }, ref) => {
9
+ const handleClick = (event) => {
10
+ // Cancel the click dispatched via the label tag, since it's already wrapped
11
+ // in <Pressable>
12
+ event.preventDefault()
13
+ event.stopPropagation()
14
+ }
15
+
16
+ return (
17
+ <input
18
+ checked={isControlled ? checked : undefined}
19
+ defaultChecked={isControlled ? undefined : defaultChecked}
20
+ disabled={disabled}
21
+ hidden
22
+ id={id}
23
+ name={name}
24
+ onChange={onChange}
25
+ onClick={handleClick}
26
+ ref={ref}
27
+ type="checkbox"
28
+ value={value}
29
+ />
30
+ )
31
+ }
32
+ )
33
+ CheckboxInput.displayName = 'CheckboxInput'
34
+
35
+ CheckboxInput.propTypes = {
36
+ checked: PropTypes.bool,
37
+ defaultChecked: PropTypes.bool,
38
+ disabled: PropTypes.bool,
39
+ id: PropTypes.string.isRequired,
40
+ isControlled: PropTypes.bool.isRequired,
41
+ name: PropTypes.string,
42
+ onChange: PropTypes.func,
43
+ value: PropTypes.string
44
+ }
45
+
46
+ CheckboxInput.defaultProps = {
47
+ checked: undefined,
48
+ defaultChecked: undefined,
49
+ disabled: false,
50
+ name: undefined,
51
+ onChange: () => {},
52
+ value: undefined
53
+ }
54
+
55
+ export default CheckboxInput
@@ -0,0 +1,3 @@
1
+ import Checkbox from './Checkbox'
2
+
3
+ export default Checkbox