@telus-uds/components-base 0.0.2-prerelease.9 → 1.0.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 (282) hide show
  1. package/.eslintrc.js +9 -0
  2. package/.ultra.cache.json +1 -1
  3. package/CHANGELOG.md +32 -0
  4. package/README.md +4 -2
  5. package/__fixtures__/test-utils.js +25 -0
  6. package/__fixtures__/testTheme.js +4 -2
  7. package/__tests__/Button/ButtonGroup.test.jsx +4 -5
  8. package/__tests__/Checkbox/Checkbox.test.jsx +2 -2
  9. package/__tests__/Checkbox/CheckboxGroup.test.jsx +4 -5
  10. package/__tests__/ExpandCollapse/ExpandCollapse.test.jsx +2 -2
  11. package/__tests__/HorizontalScroll/HorizontalScroll.test.jsx +164 -0
  12. package/__tests__/Link/LinkBase.test.jsx +0 -14
  13. package/__tests__/Radio/Radio.test.jsx +2 -2
  14. package/__tests__/Radio/RadioGroup.test.jsx +4 -5
  15. package/__tests__/RadioCard/RadioCard.test.jsx +2 -2
  16. package/__tests__/RadioCard/RadioCardGroup.test.jsx +4 -5
  17. package/__tests__/Search/Search.test.jsx +9 -8
  18. package/__tests__/Select/Select.test.jsx +3 -2
  19. package/__tests__/Tabs/Tabs.test.jsx +1 -161
  20. package/__tests__/Tags/Tags.test.jsx +4 -5
  21. package/__tests__/TextInput/TextArea.test.jsx +3 -2
  22. package/__tests__/TextInput/TextInputBase.test.jsx +10 -5
  23. package/__tests__/ThemeProvider/ThemeProvider.test.jsx +77 -0
  24. package/__tests__/ThemeProvider/useThemeTokens.test.jsx +9 -5
  25. package/__tests__/ThemeProvider/utils/theme-tokens.test.js +41 -0
  26. package/__tests__/ToggleSwitch/ToggleSwitch.test.jsx +3 -2
  27. package/__tests__/utils/children.test.jsx +128 -0
  28. package/__tests__/utils/input.test.js +1 -1
  29. package/__tests__/utils/semantics.test.jsx +43 -0
  30. package/lib/A11yText/index.js +10 -5
  31. package/lib/ActivityIndicator/Spinner.js +16 -13
  32. package/lib/ActivityIndicator/Spinner.native.js +12 -8
  33. package/lib/Box/Box.js +102 -8
  34. package/lib/Button/Button.js +9 -8
  35. package/lib/Button/ButtonBase.js +14 -7
  36. package/lib/Button/ButtonGroup.js +25 -10
  37. package/lib/Button/ButtonLink.js +10 -7
  38. package/lib/Card/Card.js +2 -0
  39. package/lib/Card/CardBase.js +12 -5
  40. package/lib/Card/PressableCardBase.js +12 -8
  41. package/lib/Checkbox/Checkbox.js +25 -14
  42. package/lib/Checkbox/CheckboxGroup.js +22 -12
  43. package/lib/Divider/Divider.js +12 -7
  44. package/lib/ExpandCollapse/Accordion.js +10 -4
  45. package/lib/ExpandCollapse/Control.js +12 -6
  46. package/lib/ExpandCollapse/ExpandCollapse.js +10 -5
  47. package/lib/ExpandCollapse/Panel.js +8 -7
  48. package/lib/Feedback/Feedback.js +10 -5
  49. package/lib/Fieldset/Fieldset.js +10 -5
  50. package/lib/Fieldset/FieldsetContainer.js +10 -5
  51. package/lib/Fieldset/FieldsetContainer.native.js +10 -5
  52. package/lib/Fieldset/Legend.js +10 -5
  53. package/lib/Fieldset/Legend.native.js +10 -5
  54. package/lib/FlexGrid/Col/Col.js +8 -5
  55. package/lib/FlexGrid/FlexGrid.js +31 -6
  56. package/lib/FlexGrid/Row/Row.js +12 -5
  57. package/lib/{Tabs → HorizontalScroll}/HorizontalScroll.js +5 -4
  58. package/lib/{Tabs/TabsScrollButton.js → HorizontalScroll/HorizontalScrollButton.js} +14 -8
  59. package/lib/{Tabs → HorizontalScroll}/ScrollViewEnd.js +0 -0
  60. package/lib/{Tabs → HorizontalScroll}/ScrollViewEnd.native.js +0 -0
  61. package/lib/{Tabs → HorizontalScroll}/dictionary.js +0 -0
  62. package/lib/HorizontalScroll/index.js +35 -0
  63. package/lib/{Tabs → HorizontalScroll}/itemPositions.js +0 -0
  64. package/lib/Icon/Icon.js +16 -9
  65. package/lib/Icon/IconText.js +8 -7
  66. package/lib/IconButton/IconButton.js +10 -5
  67. package/lib/InputLabel/InputLabel.js +33 -5
  68. package/lib/InputLabel/LabelContent.js +22 -12
  69. package/lib/InputLabel/LabelContent.native.js +23 -5
  70. package/lib/InputSupports/InputSupports.js +10 -5
  71. package/lib/Link/ChevronLink.js +12 -5
  72. package/lib/Link/InlinePressable.js +10 -4
  73. package/lib/Link/InlinePressable.native.js +5 -4
  74. package/lib/Link/Link.js +12 -5
  75. package/lib/Link/LinkBase.js +12 -5
  76. package/lib/Link/TextButton.js +10 -5
  77. package/lib/List/List.js +5 -4
  78. package/lib/List/ListItem.js +16 -8
  79. package/lib/Modal/Modal.js +10 -5
  80. package/lib/Notification/Notification.js +21 -5
  81. package/lib/Pagination/PageButton.js +21 -10
  82. package/lib/Pagination/Pagination.js +12 -7
  83. package/lib/Pagination/SideButton.js +12 -7
  84. package/lib/Pagination/usePagination.js +2 -2
  85. package/lib/Progress/Progress.js +10 -5
  86. package/lib/Progress/ProgressBar.js +21 -10
  87. package/lib/Progress/ProgressBarBackground.js +12 -8
  88. package/lib/Radio/Radio.js +14 -13
  89. package/lib/Radio/RadioButton.js +20 -9
  90. package/lib/Radio/RadioGroup.js +24 -13
  91. package/lib/RadioCard/RadioCard.js +14 -10
  92. package/lib/RadioCard/RadioCardGroup.js +13 -12
  93. package/lib/Search/Search.js +29 -18
  94. package/lib/Select/Picker.js +11 -6
  95. package/lib/Select/Picker.native.js +21 -6
  96. package/lib/Select/Select.js +46 -4
  97. package/lib/SideNav/Item.js +10 -5
  98. package/lib/SideNav/ItemsGroup.js +10 -5
  99. package/lib/SideNav/SideNav.js +11 -7
  100. package/lib/Skeleton/Skeleton.js +15 -15
  101. package/lib/Skeleton/skeletonWebAnimation.js +1 -1
  102. package/lib/Spacer/Spacer.js +19 -7
  103. package/lib/StackView/StackView.js +25 -7
  104. package/lib/StackView/StackWrap.js +16 -9
  105. package/lib/StackView/StackWrapBox.js +33 -8
  106. package/lib/StackView/StackWrapGap.js +16 -7
  107. package/lib/StackView/common.js +4 -2
  108. package/lib/StackView/getStackedContent.js +2 -2
  109. package/lib/StepTracker/StepTracker.js +10 -5
  110. package/lib/Tabs/Tabs.js +26 -19
  111. package/lib/Tabs/TabsItem.js +16 -12
  112. package/lib/Tags/Tags.js +27 -11
  113. package/lib/TextInput/TextArea.js +7 -5
  114. package/lib/TextInput/TextInput.js +12 -6
  115. package/lib/TextInput/TextInputBase.js +12 -8
  116. package/lib/ThemeProvider/ThemeProvider.js +14 -10
  117. package/lib/ThemeProvider/useSetTheme.js +6 -1
  118. package/lib/ThemeProvider/utils/styles.js +2 -2
  119. package/lib/ThemeProvider/utils/theme-tokens.js +39 -8
  120. package/lib/ToggleSwitch/ToggleSwitch.js +11 -6
  121. package/lib/Tooltip/Backdrop.js +10 -2
  122. package/lib/Tooltip/Tooltip.js +5 -4
  123. package/lib/Typography/Typography.js +40 -24
  124. package/lib/index.js +21 -0
  125. package/lib/utils/a11y/index.js +13 -0
  126. package/lib/utils/a11y/semantics.js +173 -0
  127. package/lib/utils/animation/useVerticalExpandAnimation.js +1 -1
  128. package/lib/utils/children.js +55 -8
  129. package/lib/utils/input.js +27 -17
  130. package/lib/utils/propTypes.js +82 -29
  131. package/lib/utils/useCopy.js +1 -1
  132. package/lib/utils/useHash.js +8 -4
  133. package/lib/utils/useSpacingScale.js +1 -3
  134. package/lib/utils/useUniqueId.js +1 -1
  135. package/package.json +9 -5
  136. package/release-context.json +4 -4
  137. package/src/A11yText/index.jsx +6 -4
  138. package/src/ActivityIndicator/Spinner.jsx +5 -3
  139. package/src/ActivityIndicator/Spinner.native.jsx +5 -3
  140. package/src/Box/Box.jsx +124 -39
  141. package/src/Button/Button.jsx +7 -4
  142. package/src/Button/ButtonBase.jsx +86 -77
  143. package/src/Button/ButtonGroup.jsx +81 -69
  144. package/src/Button/ButtonLink.jsx +18 -13
  145. package/src/Card/Card.jsx +2 -2
  146. package/src/Card/CardBase.jsx +5 -4
  147. package/src/Card/PressableCardBase.jsx +71 -64
  148. package/src/Checkbox/Checkbox.jsx +118 -108
  149. package/src/Checkbox/CheckboxGroup.jsx +72 -62
  150. package/src/Divider/Divider.jsx +7 -4
  151. package/src/ExpandCollapse/Accordion.jsx +3 -2
  152. package/src/ExpandCollapse/Control.jsx +40 -43
  153. package/src/ExpandCollapse/ExpandCollapse.jsx +26 -23
  154. package/src/ExpandCollapse/Panel.jsx +69 -63
  155. package/src/Feedback/Feedback.jsx +36 -33
  156. package/src/Fieldset/Fieldset.jsx +63 -56
  157. package/src/Fieldset/FieldsetContainer.jsx +14 -5
  158. package/src/Fieldset/FieldsetContainer.native.jsx +7 -4
  159. package/src/Fieldset/Legend.jsx +7 -2
  160. package/src/Fieldset/Legend.native.jsx +7 -2
  161. package/src/FlexGrid/Col/Col.jsx +139 -132
  162. package/src/FlexGrid/FlexGrid.jsx +79 -51
  163. package/src/FlexGrid/Row/Row.jsx +55 -48
  164. package/src/HorizontalScroll/HorizontalScroll.jsx +168 -0
  165. package/src/HorizontalScroll/HorizontalScrollButton.jsx +105 -0
  166. package/src/{Tabs → HorizontalScroll}/ScrollViewEnd.jsx +0 -0
  167. package/src/{Tabs → HorizontalScroll}/ScrollViewEnd.native.jsx +0 -0
  168. package/src/{Tabs → HorizontalScroll}/dictionary.js +0 -0
  169. package/src/HorizontalScroll/index.js +17 -0
  170. package/src/{Tabs → HorizontalScroll}/itemPositions.js +0 -0
  171. package/src/Icon/Icon.jsx +37 -35
  172. package/src/Icon/IconText.jsx +22 -17
  173. package/src/IconButton/IconButton.jsx +49 -42
  174. package/src/InputLabel/InputLabel.jsx +53 -38
  175. package/src/InputLabel/LabelContent.jsx +14 -6
  176. package/src/InputLabel/LabelContent.native.jsx +11 -2
  177. package/src/InputSupports/InputSupports.jsx +29 -34
  178. package/src/Link/ChevronLink.jsx +26 -16
  179. package/src/Link/InlinePressable.jsx +5 -3
  180. package/src/Link/InlinePressable.native.jsx +5 -3
  181. package/src/Link/Link.jsx +22 -16
  182. package/src/Link/LinkBase.jsx +67 -58
  183. package/src/Link/TextButton.jsx +30 -23
  184. package/src/List/List.jsx +5 -4
  185. package/src/List/ListItem.jsx +77 -82
  186. package/src/Modal/Modal.jsx +9 -4
  187. package/src/Notification/Notification.jsx +58 -43
  188. package/src/Pagination/PageButton.jsx +42 -35
  189. package/src/Pagination/Pagination.jsx +88 -92
  190. package/src/Pagination/SideButton.jsx +44 -41
  191. package/src/Progress/Progress.jsx +5 -4
  192. package/src/Progress/ProgressBar.jsx +42 -29
  193. package/src/Progress/ProgressBarBackground.jsx +5 -3
  194. package/src/Radio/Radio.jsx +85 -78
  195. package/src/Radio/RadioButton.jsx +54 -43
  196. package/src/Radio/RadioGroup.jsx +74 -63
  197. package/src/RadioCard/RadioCard.jsx +75 -68
  198. package/src/RadioCard/RadioCardGroup.jsx +82 -75
  199. package/src/Search/Search.jsx +127 -106
  200. package/src/Select/Picker.jsx +49 -42
  201. package/src/Select/Picker.native.jsx +56 -49
  202. package/src/Select/Select.jsx +115 -72
  203. package/src/SideNav/Item.jsx +53 -46
  204. package/src/SideNav/ItemsGroup.jsx +50 -43
  205. package/src/SideNav/SideNav.jsx +68 -60
  206. package/src/Skeleton/Skeleton.jsx +9 -13
  207. package/src/Spacer/Spacer.jsx +11 -4
  208. package/src/StackView/StackView.jsx +46 -23
  209. package/src/StackView/StackWrap.jsx +7 -6
  210. package/src/StackView/StackWrapBox.jsx +61 -28
  211. package/src/StackView/StackWrapGap.jsx +46 -24
  212. package/src/StackView/common.jsx +3 -2
  213. package/src/StepTracker/StepTracker.jsx +73 -62
  214. package/src/Tabs/Tabs.jsx +70 -62
  215. package/src/Tabs/TabsItem.jsx +111 -103
  216. package/src/Tags/Tags.jsx +114 -102
  217. package/src/TextInput/TextArea.jsx +5 -4
  218. package/src/TextInput/TextInput.jsx +5 -4
  219. package/src/TextInput/TextInputBase.jsx +84 -77
  220. package/src/ThemeProvider/ThemeProvider.jsx +11 -7
  221. package/src/ThemeProvider/useSetTheme.js +4 -0
  222. package/src/ThemeProvider/utils/theme-tokens.js +28 -0
  223. package/src/ToggleSwitch/ToggleSwitch.jsx +49 -50
  224. package/src/Tooltip/Tooltip.jsx +134 -130
  225. package/src/Typography/Typography.jsx +67 -44
  226. package/src/index.js +2 -0
  227. package/src/utils/a11y/index.js +1 -0
  228. package/src/utils/a11y/semantics.js +162 -0
  229. package/src/utils/children.jsx +60 -7
  230. package/src/utils/input.js +20 -17
  231. package/src/utils/propTypes.js +101 -39
  232. package/src/utils/useCopy.js +1 -1
  233. package/src/utils/useHash.js +8 -3
  234. package/stories/A11yText/A11yText.stories.jsx +2 -2
  235. package/stories/ActivityIndicator/ActivityIndicator.stories.jsx +1 -1
  236. package/stories/Box/Box.stories.jsx +1 -1
  237. package/stories/Button/Button.stories.jsx +2 -2
  238. package/stories/Button/ButtonGroup.stories.jsx +1 -1
  239. package/stories/Button/ButtonLink.stories.jsx +1 -1
  240. package/stories/Card/Card.stories.jsx +1 -1
  241. package/stories/Checkbox/Checkbox.stories.jsx +1 -1
  242. package/stories/Divider/Divider.stories.jsx +1 -1
  243. package/stories/ExpandCollapse/ExpandCollapse.stories.jsx +2 -2
  244. package/stories/Feedback/Feedback.stories.jsx +1 -1
  245. package/stories/FlexGrid/01 FlexGrid.stories.jsx +1 -1
  246. package/stories/FlexGrid/02 Row.stories.jsx +1 -1
  247. package/stories/FlexGrid/03 Col.stories.jsx +1 -1
  248. package/stories/Icon/Icon.stories.jsx +1 -1
  249. package/stories/IconButton/IconButton.stories.jsx +1 -1
  250. package/stories/InputLabel/InputLabel.stories.jsx +1 -1
  251. package/stories/Link/ChevronLink.stories.jsx +1 -1
  252. package/stories/Link/Link.stories.jsx +1 -1
  253. package/stories/Link/TextButton.stories.jsx +1 -1
  254. package/stories/List/List.stories.jsx +1 -1
  255. package/stories/Modal/Modal.stories.jsx +1 -1
  256. package/stories/Notification/Notification.stories.jsx +1 -1
  257. package/stories/Pagination/Pagination.stories.jsx +1 -1
  258. package/stories/Progress/Progress.stories.jsx +1 -1
  259. package/stories/Radio/Radio.stories.jsx +1 -1
  260. package/stories/RadioCard/RadioCard.stories.jsx +1 -1
  261. package/stories/Search/Search.stories.jsx +1 -1
  262. package/stories/Select/Select.stories.jsx +1 -1
  263. package/stories/SideNav/SideNav.stories.jsx +1 -1
  264. package/stories/SideNav/SideNavItem.stories.jsx +1 -1
  265. package/stories/SideNav/SideNavItemsGroup.stories.jsx +1 -1
  266. package/stories/Skeleton/Skeleton.stories.jsx +2 -2
  267. package/stories/Spacer/Spacer.stories.jsx +1 -1
  268. package/stories/StackView/StackView.stories.jsx +1 -1
  269. package/stories/StackView/StackWrap.stories.jsx +1 -1
  270. package/stories/StepTracker/StepTracker.stories.jsx +1 -1
  271. package/stories/Tabs/Tabs.stories.jsx +1 -1
  272. package/stories/Tags/Tags.stories.jsx +1 -1
  273. package/stories/TextInput/TextArea.stories.jsx +1 -1
  274. package/stories/TextInput/TextInput.stories.jsx +1 -1
  275. package/stories/ToggleSwitch/ToggleSwitch.stories.jsx +1 -1
  276. package/stories/Tooltip/Tooltip.stories.jsx +1 -1
  277. package/stories/TooltipButton/TooltipButton.stories.jsx +1 -1
  278. package/stories/Typography/Typography.stories.jsx +1 -1
  279. package/stories/platform-supports.jsx +1 -1
  280. package/stories/supports.jsx +2 -2
  281. package/src/Tabs/HorizontalScroll.jsx +0 -165
  282. package/src/Tabs/TabsScrollButton.jsx +0 -100
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useRef, useState } from 'react'
1
+ import React, { forwardRef, useEffect, useRef, useState } from 'react'
2
2
  import { Dimensions, Platform, Pressable, StyleSheet, Text, View } from 'react-native'
3
3
 
4
4
  import PropTypes from 'prop-types'
@@ -99,157 +99,161 @@ const defaultControl = (pressableState, variant) => (
99
99
  * - You may use one when the information is useful only to a small percentage of users (ie. tech savvy people wouldn't need this info).
100
100
  * - Tooltips may also be useful when vertical space is an issue.
101
101
  */
102
- const Tooltip = ({ children, content, position = 'auto', copy = 'en', tokens, variant }) => {
103
- const [isOpen, setIsOpen] = useState(false)
102
+ const Tooltip = forwardRef(
103
+ ({ children, content, position = 'auto', copy = 'en', tokens, variant }, ref) => {
104
+ const [isOpen, setIsOpen] = useState(false)
104
105
 
105
- const controlRef = useRef()
106
- const [controlLayout, setControlLayout] = useState(null)
107
- const [tooltipDimensions, setTooltipDimensions] = useState(null)
108
- const [windowDimensions, setWindowDimensions] = useState(Dimensions.get('window'))
109
- const [tooltipPosition, setTooltipPosition] = useState(null)
106
+ const controlRef = useRef()
107
+ const [controlLayout, setControlLayout] = useState(null)
108
+ const [tooltipDimensions, setTooltipDimensions] = useState(null)
109
+ const [windowDimensions, setWindowDimensions] = useState(Dimensions.get('window'))
110
+ const [tooltipPosition, setTooltipPosition] = useState(null)
110
111
 
111
- const getCopy = useCopy({ dictionary, copy })
112
- const themeTokens = useThemeTokens('Tooltip', tokens, variant)
112
+ const getCopy = useCopy({ dictionary, copy })
113
+ const themeTokens = useThemeTokens('Tooltip', tokens, variant)
113
114
 
114
- const { arrowWidth, arrowOffset } = themeTokens
115
+ const { arrowWidth, arrowOffset } = themeTokens
115
116
 
116
- useEffect(() => {
117
- const subscription = Dimensions.addEventListener('change', ({ window }) => {
118
- setWindowDimensions(window)
119
- })
117
+ useEffect(() => {
118
+ const subscription = Dimensions.addEventListener('change', ({ window }) => {
119
+ setWindowDimensions(window)
120
+ })
120
121
 
121
- return () => subscription?.remove()
122
- })
122
+ return () => subscription?.remove()
123
+ })
123
124
 
124
- const toggleIsOpen = () => setIsOpen(!isOpen)
125
- const close = () => setIsOpen(false)
125
+ const toggleIsOpen = () => setIsOpen(!isOpen)
126
+ const close = () => setIsOpen(false)
126
127
 
127
- const getPressableState = ({ pressed, hovered, focused }) => ({
128
- pressed,
129
- hover: hovered,
130
- focus: focused
131
- })
128
+ const getPressableState = ({ pressed, hovered, focused }) => ({
129
+ pressed,
130
+ hover: hovered,
131
+ focus: focused
132
+ })
132
133
 
133
- const onTooltipLayout = ({
134
- nativeEvent: {
135
- layout: { width, height }
136
- }
137
- }) => {
138
- if (
139
- tooltipDimensions === null ||
140
- tooltipDimensions.width !== width ||
141
- tooltipDimensions.height !== height
142
- ) {
143
- setTooltipDimensions({
144
- width: Platform.select({
145
- web: width + 0.3, // avoids often unnecessary line breaks due to subpixel rendering of fonts
146
- native: width
147
- }),
148
- height
149
- })
134
+ const onTooltipLayout = ({
135
+ nativeEvent: {
136
+ layout: { width, height }
137
+ }
138
+ }) => {
139
+ if (
140
+ tooltipDimensions === null ||
141
+ tooltipDimensions.width !== width ||
142
+ tooltipDimensions.height !== height
143
+ ) {
144
+ setTooltipDimensions({
145
+ width: Platform.select({
146
+ web: width + 0.3, // avoids often unnecessary line breaks due to subpixel rendering of fonts
147
+ native: width
148
+ }),
149
+ height
150
+ })
151
+ }
150
152
  }
151
- }
152
153
 
153
- useEffect(() => {
154
- if (isOpen) {
155
- controlRef.current.measureInWindow((x, y, width, height) => {
156
- setControlLayout({ x, y, width, height })
157
- })
158
- } else {
159
- setControlLayout(null)
160
- setTooltipDimensions(null)
161
- setTooltipPosition(null)
162
- }
163
- }, [isOpen])
154
+ useEffect(() => {
155
+ if (isOpen) {
156
+ controlRef.current.measureInWindow((x, y, width, height) => {
157
+ setControlLayout({ x, y, width, height })
158
+ })
159
+ } else {
160
+ setControlLayout(null)
161
+ setTooltipDimensions(null)
162
+ setTooltipPosition(null)
163
+ }
164
+ }, [isOpen])
164
165
 
165
- useEffect(() => {
166
- setIsOpen(false)
167
- }, [windowDimensions])
166
+ useEffect(() => {
167
+ setIsOpen(false)
168
+ }, [windowDimensions])
168
169
 
169
- useEffect(() => {
170
- if (
171
- (tooltipPosition !== null && !tooltipPosition?.isNormalized) ||
172
- !isOpen ||
173
- controlLayout === null ||
174
- tooltipDimensions == null
175
- ) {
176
- return
177
- }
170
+ useEffect(() => {
171
+ if (
172
+ (tooltipPosition !== null && !tooltipPosition?.isNormalized) ||
173
+ !isOpen ||
174
+ controlLayout === null ||
175
+ tooltipDimensions == null
176
+ ) {
177
+ return
178
+ }
178
179
 
179
- const updatedPosition = getTooltipPosition(position, {
180
- controlLayout,
180
+ const updatedPosition = getTooltipPosition(position, {
181
+ controlLayout,
182
+ tooltipDimensions,
183
+ windowDimensions,
184
+ arrowWidth,
185
+ arrowOffset
186
+ })
187
+
188
+ // avoid ending up in an infinite normalization loop
189
+ if (tooltipPosition?.isNormalized && updatedPosition.isNormalized) {
190
+ return
191
+ }
192
+
193
+ setTooltipPosition(updatedPosition)
194
+ }, [
195
+ isOpen,
196
+ position,
181
197
  tooltipDimensions,
198
+ controlLayout,
182
199
  windowDimensions,
183
200
  arrowWidth,
184
- arrowOffset
185
- })
201
+ arrowOffset,
202
+ tooltipPosition
203
+ ])
186
204
 
187
- // avoid ending up in an infinite normalization loop
188
- if (tooltipPosition?.isNormalized && updatedPosition.isNormalized) {
189
- return
190
- }
205
+ const control = children !== undefined ? children : defaultControl
206
+ const pressableStyles =
207
+ control === defaultControl ? Platform.select({ web: { outline: 'none' } }) : undefined
208
+ const pressableHitSlop =
209
+ control === defaultControl ? { top: 10, bottom: 10, left: 10, right: 10 } : undefined
191
210
 
192
- setTooltipPosition(updatedPosition)
193
- }, [
194
- isOpen,
195
- position,
196
- tooltipDimensions,
197
- controlLayout,
198
- windowDimensions,
199
- arrowWidth,
200
- arrowOffset,
201
- tooltipPosition
202
- ])
203
-
204
- const control = children !== undefined ? children : defaultControl
205
- const pressableStyles =
206
- control === defaultControl ? Platform.select({ web: { outline: 'none' } }) : undefined
207
- const pressableHitSlop =
208
- control === defaultControl ? { top: 10, bottom: 10, left: 10, right: 10 } : undefined
209
-
210
- return (
211
- <View style={staticStyles.container}>
212
- <Pressable
213
- onPress={toggleIsOpen}
214
- ref={controlRef}
215
- onBlur={close}
216
- style={pressableStyles}
217
- hitSlop={pressableHitSlop}
218
- accessibilityLabel={getCopy('a11yText')}
219
- accessibilityRole="button"
220
- >
221
- {typeof control === 'function'
222
- ? (pressableState) => control(getPressableState(pressableState), variant)
223
- : control}
224
- </Pressable>
225
- {isOpen && (
226
- <Backdrop onPress={close}>
227
- <View
228
- style={[
229
- staticStyles.tooltip,
230
- selectTooltipShadowStyles(themeTokens), // applied separately so that it doesn't cover the arrow
231
- tooltipPosition && selectTooltipPositionStyles(tooltipPosition),
232
- (tooltipPosition === null || tooltipPosition?.isNormalized) &&
233
- staticStyles.tooltipHidden // visually hide the tooltip until we have a final measurement
234
- ]}
235
- onLayout={onTooltipLayout}
236
- accessibilityRole="alert"
237
- >
211
+ return (
212
+ <View style={staticStyles.container}>
213
+ <Pressable
214
+ onPress={toggleIsOpen}
215
+ ref={controlRef}
216
+ onBlur={close}
217
+ style={pressableStyles}
218
+ hitSlop={pressableHitSlop}
219
+ accessibilityLabel={getCopy('a11yText')}
220
+ accessibilityRole="button"
221
+ >
222
+ {typeof control === 'function'
223
+ ? (pressableState) => control(getPressableState(pressableState), variant)
224
+ : control}
225
+ </Pressable>
226
+ {isOpen && (
227
+ <Backdrop onPress={close}>
238
228
  <View
229
+ ref={ref}
239
230
  style={[
240
- staticStyles.arrow,
241
- tooltipPosition && selectArrowStyles(themeTokens, tooltipPosition)
231
+ staticStyles.tooltip,
232
+ selectTooltipShadowStyles(themeTokens), // applied separately so that it doesn't cover the arrow
233
+ tooltipPosition && selectTooltipPositionStyles(tooltipPosition),
234
+ (tooltipPosition === null || tooltipPosition?.isNormalized) &&
235
+ staticStyles.tooltipHidden // visually hide the tooltip until we have a final measurement
242
236
  ]}
243
- />
244
- <View style={selectTooltipStyles(themeTokens)}>
245
- <Text style={selectTextStyles(themeTokens)}>{content}</Text>
237
+ onLayout={onTooltipLayout}
238
+ accessibilityRole="alert"
239
+ >
240
+ <View
241
+ style={[
242
+ staticStyles.arrow,
243
+ tooltipPosition && selectArrowStyles(themeTokens, tooltipPosition)
244
+ ]}
245
+ />
246
+ <View style={selectTooltipStyles(themeTokens)}>
247
+ <Text style={selectTextStyles(themeTokens)}>{content}</Text>
248
+ </View>
246
249
  </View>
247
- </View>
248
- </Backdrop>
249
- )}
250
- </View>
251
- )
252
- }
250
+ </Backdrop>
251
+ )}
252
+ </View>
253
+ )
254
+ }
255
+ )
256
+ Tooltip.displayName = 'Tooltip'
253
257
 
254
258
  Tooltip.propTypes = {
255
259
  /**
@@ -1,20 +1,22 @@
1
- import React from 'react'
1
+ import React, { forwardRef } from 'react'
2
2
  import PropTypes from 'prop-types'
3
3
  import { Text, View } from 'react-native'
4
4
 
5
5
  import { useThemeTokens } from '../ThemeProvider'
6
6
  import { useViewport } from '../ViewportProvider'
7
7
  import { applyTextStyles } from '../ThemeProvider/utils'
8
- import { a11yProps, variantProp, getTokensPropType, getMaxFontMultiplier } from '../utils'
9
-
8
+ import {
9
+ a11yProps,
10
+ variantProp,
11
+ getTokensPropType,
12
+ getMaxFontMultiplier,
13
+ headingTags,
14
+ textTags,
15
+ getA11yPropsFromHtmlTag
16
+ } from '../utils'
10
17
  /**
11
- * If passed a string like 'h1', 'h2' etc, returns the heading number as a string,
12
- * else returns false
18
+ * @typedef {import('../utils/a11y/semantics').TextTag} TextTag
13
19
  */
14
- function getHeadingLevel(heading) {
15
- const match = typeof heading === 'string' && heading.match(/^h(\d)$/)
16
- return match && match[1]
17
- }
18
20
 
19
21
  const selectTextStyles = ({
20
22
  fontWeight,
@@ -38,50 +40,71 @@ const selectTextStyles = ({
38
40
  })
39
41
 
40
42
  // General-purpose flexible theme-neutral base component for text
41
- const Typography = ({
42
- children,
43
- variant,
44
- heading,
45
- block = false,
46
- align = undefined,
47
- accessibilityRole = heading ? 'header' : 'text',
48
- tokens,
49
- ...rest
50
- }) => {
51
- const viewport = useViewport()
52
- const themeTokens = useThemeTokens('Typography', tokens, variant, { viewport })
53
- const textProps = {
54
- style: selectTextStyles(align ? { ...themeTokens, textAlign: align } : themeTokens),
55
- maxFontSizeMultiplier: getMaxFontMultiplier(themeTokens)
56
- }
43
+ const Typography = forwardRef(
44
+ (
45
+ {
46
+ children,
47
+ variant,
48
+ heading,
49
+ tag = typeof heading === 'string' ? heading : undefined,
50
+ accessibilityRole = heading === true ? 'header' : undefined,
51
+ block = false,
52
+ align = undefined,
53
+ tokens,
54
+ dataSet,
55
+ ...rest
56
+ },
57
+ ref
58
+ ) => {
59
+ const viewport = useViewport()
60
+ const themeTokens = useThemeTokens('Typography', tokens, variant, { viewport })
61
+ const textProps = {
62
+ style: selectTextStyles(align ? { ...themeTokens, textAlign: align } : themeTokens),
63
+ dataSet,
64
+ maxFontSizeMultiplier: getMaxFontMultiplier(themeTokens)
65
+ }
57
66
 
58
- const headingLevel = getHeadingLevel(heading)
59
- const a11y = {
60
- ...a11yProps.select(rest),
61
- accessibilityRole,
62
- // On React Native Web, `accessibilityLevel` controls which heading tag (h1, h2 etc) is used.
63
- ...(headingLevel && { accessibilityLevel: headingLevel })
64
- }
67
+ const a11y = {
68
+ ...getA11yPropsFromHtmlTag(tag, accessibilityRole),
69
+ ...a11yProps.select(rest)
70
+ }
65
71
 
66
- return block ? (
67
- <View {...a11y}>
68
- <Text {...textProps}>{children}</Text>
69
- </View>
70
- ) : (
71
- <Text {...textProps} {...a11y}>
72
- {children}
73
- </Text>
74
- )
75
- }
72
+ return block ? (
73
+ <View ref={ref} {...a11y}>
74
+ <Text {...textProps}>{children}</Text>
75
+ </View>
76
+ ) : (
77
+ <Text ref={ref} {...textProps} {...a11y}>
78
+ {children}
79
+ </Text>
80
+ )
81
+ }
82
+ )
83
+ Typography.displayName = 'Typography'
76
84
 
77
85
  Typography.propTypes = {
78
86
  ...a11yProps.types,
79
87
  tokens: getTokensPropType('Typography'),
80
88
  variant: variantProp.propType,
81
89
  /**
82
- * If truthy, will render a heading; if a html heading tag is provided, uses that tag on Web
90
+ * Renders the text as a semantic heading. If a html heading tag is provided, uses that tag on Web.
91
+ *
92
+ * Does not affect styling: <Typography heading="h2"> will render a <h2> that looks like plain text.
93
+ * Use both `heading` and `variant` props to render semantic headings that look like headings.
94
+ *
95
+ * In native apps, if this is `true` or any html heading tag string and accessibilityRole prop
96
+ * is not defined, the accessibilityRole will default to "heading".
97
+ */
98
+ heading: PropTypes.oneOf([...headingTags, true]),
99
+ /**
100
+ * Optional semantic HTML tag, to apply to the text on web. Does not change styling.
101
+ *
102
+ * `tag` and `heading` props set the same thing, so shouldn't be used together (`tag` overrides `heading`).
103
+ *
104
+ * In native apps, if a header tag is provided ("h1", "h2" etc) and accessibilityRole prop
105
+ * is not defined, the accessibilityRole will default to "heading".
83
106
  */
84
- heading: PropTypes.oneOf([true, 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']),
107
+ tag: PropTypes.oneOf(textTags),
85
108
  /**
86
109
  * If true, forces the element to behave as a View block even if nested in other text
87
110
  */
package/src/index.js CHANGED
@@ -10,6 +10,8 @@ export { default as ExpandCollapse, Accordion } from './ExpandCollapse'
10
10
  export { default as Feedback } from './Feedback'
11
11
  export { default as Fieldset } from './Fieldset'
12
12
  export { default as FlexGrid } from './FlexGrid'
13
+ export { default as HorizontalScroll } from './HorizontalScroll'
14
+ export * from './HorizontalScroll'
13
15
  export { default as Icon } from './Icon'
14
16
  export * from './Icon'
15
17
  export { default as IconButton } from './IconButton'
@@ -1 +1,2 @@
1
1
  export * from './textSize'
2
+ export * from './semantics'
@@ -0,0 +1,162 @@
1
+ import { Platform } from 'react-native'
2
+ /**
3
+ * @typedef {import('react-native').AccessibilityRole} AccessibilityRole
4
+ */
5
+
6
+ /**
7
+ * This is based on the role-to-tag mapping that React Native Web uses to set HTML tags.
8
+ * It's not exported in any way from RNW, so we need to keep this up-to-date manually.
9
+ * https://github.com/necolas/react-native-web/blob/master/packages/react-native-web/src/modules/AccessibilityUtil/propsToAccessibilityComponent.js
10
+ *
11
+ * Note: every role in this list is a web-only aria-role. There is no overlap between _these_ web tags
12
+ * or roles and native accessibilityRoles. Only h1, h2, h3 etc map to an RN equivalent ("heading").
13
+ *
14
+ * - RN "summary" native role maps to "region" aria-role, but setting `<section>`/"region" does not
15
+ * set React Native's "summary" role, which has a much narrower use case.
16
+ * - `<Header>`/"Banner" also do not map to RN's "heading". Only h1 / h2 etc map to RN "heading".
17
+ *
18
+ * Therefore, all of these tags / roles default to no accessibilityRole in native apps. This is not wrong:
19
+ * in general, RN accessibilityRoles tend to be more about interaction and less about semantics than web roles.
20
+ *
21
+ * RNW's one-way mapping of React Native accessibilityRoles to web aria-roles:
22
+ * https://github.com/necolas/react-native-web/blob/master/packages/react-native-web/src/modules/AccessibilityUtil/propsToAriaRole.js
23
+ */
24
+ // Adding `/** @type {const} */ ({...})` denotes object content as `readonly` in many IDEs
25
+ // eslint-disable-next-line prettier/prettier
26
+ const rolesToTags = /** @type {const} */ ({
27
+ article: 'article',
28
+ banner: 'header',
29
+ blockquote: 'blockquote',
30
+ code: 'code',
31
+ complementary: 'aside',
32
+ contentinfo: 'footer',
33
+ deletion: 'del',
34
+ emphasis: 'em',
35
+ figure: 'figure',
36
+ insertion: 'ins',
37
+ form: 'form',
38
+ list: 'ul',
39
+ listitem: 'li',
40
+ main: 'main',
41
+ navigation: 'nav',
42
+ region: 'section',
43
+ strong: 'strong',
44
+
45
+ // Add special cases that are in RNW's function logic but not in its mapping object
46
+ label: 'label'
47
+ // eslint-disable-next-line prettier/prettier
48
+ })
49
+
50
+ // Invert React Native Web's mapping, so a tag gets the role that gets that tag
51
+ export const tagsToRoles = Object.fromEntries(
52
+ Object.entries(rolesToTags).map(([key, value]) => [value, key])
53
+ )
54
+
55
+ /**
56
+ * Heading HTML tags map to the "heading" accessibilityRole in native apps, which is similar
57
+ * to headings on web but without the expectation of a hierarchy of headings within one screen.
58
+ */
59
+ // eslint-disable-next-line prettier/prettier
60
+ export const headingTags = /** @type {const} */ (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
61
+ /**
62
+ * @typedef {typeof headingTags[number]} HeadingTag
63
+ */
64
+
65
+ /**
66
+ * All HTML tags that may be set via RNW accesibility props alone and therefore may be set using
67
+ * the getA11yPropsFromHtmlTag function without changing other behaviour.
68
+ *
69
+ * Of these, only headers (h1, h2, ...h5, h6) set a corresponding accessibilityRole in native apps ("heading").
70
+ */
71
+ export const supportedTags = [...Object.keys(tagsToRoles), ...headingTags]
72
+ /**
73
+ * Uses readonly mapping keys/values to generate static types for IDEs that support TS in JSDoc.
74
+ * @typedef {keyof rolesToTags} RoleWithTag
75
+ * @typedef {typeof rolesToTags[RoleWithTag] | typeof headingTags[number]} SupportedTag
76
+ */
77
+
78
+ /**
79
+ * Subset of supported HTML tags that may be used with layout containers like Box.
80
+ *
81
+ * Of these, only headers (h1, h2, ...h5, h6) set a corresponding accessibilityRole in native apps ("heading").
82
+ */
83
+ // eslint-disable-next-line prettier/prettier
84
+ export const layoutTags = /** @type {const} */ ([
85
+ ...headingTags,
86
+ 'article',
87
+ 'aside',
88
+ 'blockquote',
89
+ 'footer',
90
+ 'figure',
91
+ 'form',
92
+ 'header',
93
+ 'ul',
94
+ 'li',
95
+ 'main',
96
+ 'nav',
97
+ 'section',
98
+ 'label'
99
+ // eslint-disable-next-line prettier/prettier
100
+ ])
101
+ /**
102
+ * @typedef {typeof layoutTags[number]} LayoutTag
103
+ */
104
+
105
+ /**
106
+ * Subset of supported HTML tags that may be used with text elements like Typography.
107
+ *
108
+ * Of these, only headers (h1, h2, ...h5, h6) set a corresponding accessibilityRole in native apps ("heading").
109
+ */
110
+ // eslint-disable-next-line prettier/prettier
111
+ export const textTags = /** @type {const} */ ([
112
+ ...headingTags,
113
+ 'blockquote',
114
+ 'code',
115
+ 'del',
116
+ 'em',
117
+ 'ins',
118
+ 'li',
119
+ 'strong',
120
+ 'label'
121
+ // eslint-disable-next-line prettier/prettier
122
+ ])
123
+ /**
124
+ * @typedef {typeof layoutTags[number]} TextTag
125
+ */
126
+
127
+ /**
128
+ * If passed a heading tag string like 'h1', 'h2' etc, returns the heading number as a number
129
+ * ready for use in `accessibilityLevel` props and similar.
130
+ *
131
+ * @param {string} [tag] - HTML tag string; returns undefined if not a {@link HeadingTag}
132
+ * @returns {'1' | '2' | '3' | '4' | '5' | '6' | undefined}
133
+ */
134
+ export const getHeadingLevel = (tag) => (headingTags.includes(tag) ? Number(tag[1]) : undefined)
135
+
136
+ /**
137
+ * Takes a supported HTML tag, and returns the accessibility props that, on web, make React Native Web
138
+ * render that tag.
139
+ *
140
+ * For cross-platform apps, a second argument may be passed with an [accessibilityRole](https://reactnative.dev/docs/accessibility#accessibilityrole)
141
+ * to use in native apps. Heading tags (h1, h2, ...h5, h6) map to "heading" role by default;
142
+ * no other supported semantic HTML tags have an equivalent native accessibilityRole.
143
+ *
144
+ * @param {SupportedTag} tag - HTML tag to use on web
145
+ * @param {AccessibilityRole | null} [nativeRole] - optional accessibilityRole for native apps
146
+ * @returns {{ accessibilityRole: string, accessibilityLevel?: string } | undefined}
147
+ */
148
+ export const getA11yPropsFromHtmlTag = (tag, nativeRole) => {
149
+ // Allow cross-platform apps to set accessibilityRoles alongside a web tag without conflict
150
+ if (nativeRole !== undefined && Platform.OS !== 'web') return { accessibilityRole: nativeRole }
151
+
152
+ if (tag) {
153
+ const accessibilityRole = tagsToRoles[tag]
154
+ if (accessibilityRole) return { accessibilityRole }
155
+
156
+ const accessibilityLevel = getHeadingLevel(tag)
157
+ if (accessibilityLevel) return { accessibilityRole: 'header', accessibilityLevel }
158
+ }
159
+ // If nothing matches or no tag supplied, return undefined and let component decide how to fall back.
160
+ // Note that return value may always be spread in objects (it is safe to spread undefined like { ...undefined })
161
+ return undefined
162
+ }