@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.
- package/.eslintrc.js +9 -0
- package/.ultra.cache.json +1 -1
- package/CHANGELOG.md +32 -0
- package/README.md +4 -2
- package/__fixtures__/test-utils.js +25 -0
- package/__fixtures__/testTheme.js +4 -2
- package/__tests__/Button/ButtonGroup.test.jsx +4 -5
- package/__tests__/Checkbox/Checkbox.test.jsx +2 -2
- package/__tests__/Checkbox/CheckboxGroup.test.jsx +4 -5
- package/__tests__/ExpandCollapse/ExpandCollapse.test.jsx +2 -2
- package/__tests__/HorizontalScroll/HorizontalScroll.test.jsx +164 -0
- package/__tests__/Link/LinkBase.test.jsx +0 -14
- package/__tests__/Radio/Radio.test.jsx +2 -2
- package/__tests__/Radio/RadioGroup.test.jsx +4 -5
- package/__tests__/RadioCard/RadioCard.test.jsx +2 -2
- package/__tests__/RadioCard/RadioCardGroup.test.jsx +4 -5
- package/__tests__/Search/Search.test.jsx +9 -8
- package/__tests__/Select/Select.test.jsx +3 -2
- package/__tests__/Tabs/Tabs.test.jsx +1 -161
- package/__tests__/Tags/Tags.test.jsx +4 -5
- package/__tests__/TextInput/TextArea.test.jsx +3 -2
- package/__tests__/TextInput/TextInputBase.test.jsx +10 -5
- package/__tests__/ThemeProvider/ThemeProvider.test.jsx +77 -0
- package/__tests__/ThemeProvider/useThemeTokens.test.jsx +9 -5
- package/__tests__/ThemeProvider/utils/theme-tokens.test.js +41 -0
- package/__tests__/ToggleSwitch/ToggleSwitch.test.jsx +3 -2
- package/__tests__/utils/children.test.jsx +128 -0
- package/__tests__/utils/input.test.js +1 -1
- package/__tests__/utils/semantics.test.jsx +43 -0
- package/lib/A11yText/index.js +10 -5
- package/lib/ActivityIndicator/Spinner.js +16 -13
- package/lib/ActivityIndicator/Spinner.native.js +12 -8
- package/lib/Box/Box.js +102 -8
- package/lib/Button/Button.js +9 -8
- package/lib/Button/ButtonBase.js +14 -7
- package/lib/Button/ButtonGroup.js +25 -10
- package/lib/Button/ButtonLink.js +10 -7
- package/lib/Card/Card.js +2 -0
- package/lib/Card/CardBase.js +12 -5
- package/lib/Card/PressableCardBase.js +12 -8
- package/lib/Checkbox/Checkbox.js +25 -14
- package/lib/Checkbox/CheckboxGroup.js +22 -12
- package/lib/Divider/Divider.js +12 -7
- package/lib/ExpandCollapse/Accordion.js +10 -4
- package/lib/ExpandCollapse/Control.js +12 -6
- package/lib/ExpandCollapse/ExpandCollapse.js +10 -5
- package/lib/ExpandCollapse/Panel.js +8 -7
- package/lib/Feedback/Feedback.js +10 -5
- package/lib/Fieldset/Fieldset.js +10 -5
- package/lib/Fieldset/FieldsetContainer.js +10 -5
- package/lib/Fieldset/FieldsetContainer.native.js +10 -5
- package/lib/Fieldset/Legend.js +10 -5
- package/lib/Fieldset/Legend.native.js +10 -5
- package/lib/FlexGrid/Col/Col.js +8 -5
- package/lib/FlexGrid/FlexGrid.js +31 -6
- package/lib/FlexGrid/Row/Row.js +12 -5
- package/lib/{Tabs → HorizontalScroll}/HorizontalScroll.js +5 -4
- package/lib/{Tabs/TabsScrollButton.js → HorizontalScroll/HorizontalScrollButton.js} +14 -8
- package/lib/{Tabs → HorizontalScroll}/ScrollViewEnd.js +0 -0
- package/lib/{Tabs → HorizontalScroll}/ScrollViewEnd.native.js +0 -0
- package/lib/{Tabs → HorizontalScroll}/dictionary.js +0 -0
- package/lib/HorizontalScroll/index.js +35 -0
- package/lib/{Tabs → HorizontalScroll}/itemPositions.js +0 -0
- package/lib/Icon/Icon.js +16 -9
- package/lib/Icon/IconText.js +8 -7
- package/lib/IconButton/IconButton.js +10 -5
- package/lib/InputLabel/InputLabel.js +33 -5
- package/lib/InputLabel/LabelContent.js +22 -12
- package/lib/InputLabel/LabelContent.native.js +23 -5
- package/lib/InputSupports/InputSupports.js +10 -5
- package/lib/Link/ChevronLink.js +12 -5
- package/lib/Link/InlinePressable.js +10 -4
- package/lib/Link/InlinePressable.native.js +5 -4
- package/lib/Link/Link.js +12 -5
- package/lib/Link/LinkBase.js +12 -5
- package/lib/Link/TextButton.js +10 -5
- package/lib/List/List.js +5 -4
- package/lib/List/ListItem.js +16 -8
- package/lib/Modal/Modal.js +10 -5
- package/lib/Notification/Notification.js +21 -5
- package/lib/Pagination/PageButton.js +21 -10
- package/lib/Pagination/Pagination.js +12 -7
- package/lib/Pagination/SideButton.js +12 -7
- package/lib/Pagination/usePagination.js +2 -2
- package/lib/Progress/Progress.js +10 -5
- package/lib/Progress/ProgressBar.js +21 -10
- package/lib/Progress/ProgressBarBackground.js +12 -8
- package/lib/Radio/Radio.js +14 -13
- package/lib/Radio/RadioButton.js +20 -9
- package/lib/Radio/RadioGroup.js +24 -13
- package/lib/RadioCard/RadioCard.js +14 -10
- package/lib/RadioCard/RadioCardGroup.js +13 -12
- package/lib/Search/Search.js +29 -18
- package/lib/Select/Picker.js +11 -6
- package/lib/Select/Picker.native.js +21 -6
- package/lib/Select/Select.js +46 -4
- package/lib/SideNav/Item.js +10 -5
- package/lib/SideNav/ItemsGroup.js +10 -5
- package/lib/SideNav/SideNav.js +11 -7
- package/lib/Skeleton/Skeleton.js +15 -15
- package/lib/Skeleton/skeletonWebAnimation.js +1 -1
- package/lib/Spacer/Spacer.js +19 -7
- package/lib/StackView/StackView.js +25 -7
- package/lib/StackView/StackWrap.js +16 -9
- package/lib/StackView/StackWrapBox.js +33 -8
- package/lib/StackView/StackWrapGap.js +16 -7
- package/lib/StackView/common.js +4 -2
- package/lib/StackView/getStackedContent.js +2 -2
- package/lib/StepTracker/StepTracker.js +10 -5
- package/lib/Tabs/Tabs.js +26 -19
- package/lib/Tabs/TabsItem.js +16 -12
- package/lib/Tags/Tags.js +27 -11
- package/lib/TextInput/TextArea.js +7 -5
- package/lib/TextInput/TextInput.js +12 -6
- package/lib/TextInput/TextInputBase.js +12 -8
- package/lib/ThemeProvider/ThemeProvider.js +14 -10
- package/lib/ThemeProvider/useSetTheme.js +6 -1
- package/lib/ThemeProvider/utils/styles.js +2 -2
- package/lib/ThemeProvider/utils/theme-tokens.js +39 -8
- package/lib/ToggleSwitch/ToggleSwitch.js +11 -6
- package/lib/Tooltip/Backdrop.js +10 -2
- package/lib/Tooltip/Tooltip.js +5 -4
- package/lib/Typography/Typography.js +40 -24
- package/lib/index.js +21 -0
- package/lib/utils/a11y/index.js +13 -0
- package/lib/utils/a11y/semantics.js +173 -0
- package/lib/utils/animation/useVerticalExpandAnimation.js +1 -1
- package/lib/utils/children.js +55 -8
- package/lib/utils/input.js +27 -17
- package/lib/utils/propTypes.js +82 -29
- package/lib/utils/useCopy.js +1 -1
- package/lib/utils/useHash.js +8 -4
- package/lib/utils/useSpacingScale.js +1 -3
- package/lib/utils/useUniqueId.js +1 -1
- package/package.json +9 -5
- package/release-context.json +4 -4
- package/src/A11yText/index.jsx +6 -4
- package/src/ActivityIndicator/Spinner.jsx +5 -3
- package/src/ActivityIndicator/Spinner.native.jsx +5 -3
- package/src/Box/Box.jsx +124 -39
- package/src/Button/Button.jsx +7 -4
- package/src/Button/ButtonBase.jsx +86 -77
- package/src/Button/ButtonGroup.jsx +81 -69
- package/src/Button/ButtonLink.jsx +18 -13
- package/src/Card/Card.jsx +2 -2
- package/src/Card/CardBase.jsx +5 -4
- package/src/Card/PressableCardBase.jsx +71 -64
- package/src/Checkbox/Checkbox.jsx +118 -108
- package/src/Checkbox/CheckboxGroup.jsx +72 -62
- package/src/Divider/Divider.jsx +7 -4
- package/src/ExpandCollapse/Accordion.jsx +3 -2
- package/src/ExpandCollapse/Control.jsx +40 -43
- package/src/ExpandCollapse/ExpandCollapse.jsx +26 -23
- package/src/ExpandCollapse/Panel.jsx +69 -63
- package/src/Feedback/Feedback.jsx +36 -33
- package/src/Fieldset/Fieldset.jsx +63 -56
- package/src/Fieldset/FieldsetContainer.jsx +14 -5
- package/src/Fieldset/FieldsetContainer.native.jsx +7 -4
- package/src/Fieldset/Legend.jsx +7 -2
- package/src/Fieldset/Legend.native.jsx +7 -2
- package/src/FlexGrid/Col/Col.jsx +139 -132
- package/src/FlexGrid/FlexGrid.jsx +79 -51
- package/src/FlexGrid/Row/Row.jsx +55 -48
- package/src/HorizontalScroll/HorizontalScroll.jsx +168 -0
- package/src/HorizontalScroll/HorizontalScrollButton.jsx +105 -0
- package/src/{Tabs → HorizontalScroll}/ScrollViewEnd.jsx +0 -0
- package/src/{Tabs → HorizontalScroll}/ScrollViewEnd.native.jsx +0 -0
- package/src/{Tabs → HorizontalScroll}/dictionary.js +0 -0
- package/src/HorizontalScroll/index.js +17 -0
- package/src/{Tabs → HorizontalScroll}/itemPositions.js +0 -0
- package/src/Icon/Icon.jsx +37 -35
- package/src/Icon/IconText.jsx +22 -17
- package/src/IconButton/IconButton.jsx +49 -42
- package/src/InputLabel/InputLabel.jsx +53 -38
- package/src/InputLabel/LabelContent.jsx +14 -6
- package/src/InputLabel/LabelContent.native.jsx +11 -2
- package/src/InputSupports/InputSupports.jsx +29 -34
- package/src/Link/ChevronLink.jsx +26 -16
- package/src/Link/InlinePressable.jsx +5 -3
- package/src/Link/InlinePressable.native.jsx +5 -3
- package/src/Link/Link.jsx +22 -16
- package/src/Link/LinkBase.jsx +67 -58
- package/src/Link/TextButton.jsx +30 -23
- package/src/List/List.jsx +5 -4
- package/src/List/ListItem.jsx +77 -82
- package/src/Modal/Modal.jsx +9 -4
- package/src/Notification/Notification.jsx +58 -43
- package/src/Pagination/PageButton.jsx +42 -35
- package/src/Pagination/Pagination.jsx +88 -92
- package/src/Pagination/SideButton.jsx +44 -41
- package/src/Progress/Progress.jsx +5 -4
- package/src/Progress/ProgressBar.jsx +42 -29
- package/src/Progress/ProgressBarBackground.jsx +5 -3
- package/src/Radio/Radio.jsx +85 -78
- package/src/Radio/RadioButton.jsx +54 -43
- package/src/Radio/RadioGroup.jsx +74 -63
- package/src/RadioCard/RadioCard.jsx +75 -68
- package/src/RadioCard/RadioCardGroup.jsx +82 -75
- package/src/Search/Search.jsx +127 -106
- package/src/Select/Picker.jsx +49 -42
- package/src/Select/Picker.native.jsx +56 -49
- package/src/Select/Select.jsx +115 -72
- package/src/SideNav/Item.jsx +53 -46
- package/src/SideNav/ItemsGroup.jsx +50 -43
- package/src/SideNav/SideNav.jsx +68 -60
- package/src/Skeleton/Skeleton.jsx +9 -13
- package/src/Spacer/Spacer.jsx +11 -4
- package/src/StackView/StackView.jsx +46 -23
- package/src/StackView/StackWrap.jsx +7 -6
- package/src/StackView/StackWrapBox.jsx +61 -28
- package/src/StackView/StackWrapGap.jsx +46 -24
- package/src/StackView/common.jsx +3 -2
- package/src/StepTracker/StepTracker.jsx +73 -62
- package/src/Tabs/Tabs.jsx +70 -62
- package/src/Tabs/TabsItem.jsx +111 -103
- package/src/Tags/Tags.jsx +114 -102
- package/src/TextInput/TextArea.jsx +5 -4
- package/src/TextInput/TextInput.jsx +5 -4
- package/src/TextInput/TextInputBase.jsx +84 -77
- package/src/ThemeProvider/ThemeProvider.jsx +11 -7
- package/src/ThemeProvider/useSetTheme.js +4 -0
- package/src/ThemeProvider/utils/theme-tokens.js +28 -0
- package/src/ToggleSwitch/ToggleSwitch.jsx +49 -50
- package/src/Tooltip/Tooltip.jsx +134 -130
- package/src/Typography/Typography.jsx +67 -44
- package/src/index.js +2 -0
- package/src/utils/a11y/index.js +1 -0
- package/src/utils/a11y/semantics.js +162 -0
- package/src/utils/children.jsx +60 -7
- package/src/utils/input.js +20 -17
- package/src/utils/propTypes.js +101 -39
- package/src/utils/useCopy.js +1 -1
- package/src/utils/useHash.js +8 -3
- package/stories/A11yText/A11yText.stories.jsx +2 -2
- package/stories/ActivityIndicator/ActivityIndicator.stories.jsx +1 -1
- package/stories/Box/Box.stories.jsx +1 -1
- package/stories/Button/Button.stories.jsx +2 -2
- package/stories/Button/ButtonGroup.stories.jsx +1 -1
- package/stories/Button/ButtonLink.stories.jsx +1 -1
- package/stories/Card/Card.stories.jsx +1 -1
- package/stories/Checkbox/Checkbox.stories.jsx +1 -1
- package/stories/Divider/Divider.stories.jsx +1 -1
- package/stories/ExpandCollapse/ExpandCollapse.stories.jsx +2 -2
- package/stories/Feedback/Feedback.stories.jsx +1 -1
- package/stories/FlexGrid/01 FlexGrid.stories.jsx +1 -1
- package/stories/FlexGrid/02 Row.stories.jsx +1 -1
- package/stories/FlexGrid/03 Col.stories.jsx +1 -1
- package/stories/Icon/Icon.stories.jsx +1 -1
- package/stories/IconButton/IconButton.stories.jsx +1 -1
- package/stories/InputLabel/InputLabel.stories.jsx +1 -1
- package/stories/Link/ChevronLink.stories.jsx +1 -1
- package/stories/Link/Link.stories.jsx +1 -1
- package/stories/Link/TextButton.stories.jsx +1 -1
- package/stories/List/List.stories.jsx +1 -1
- package/stories/Modal/Modal.stories.jsx +1 -1
- package/stories/Notification/Notification.stories.jsx +1 -1
- package/stories/Pagination/Pagination.stories.jsx +1 -1
- package/stories/Progress/Progress.stories.jsx +1 -1
- package/stories/Radio/Radio.stories.jsx +1 -1
- package/stories/RadioCard/RadioCard.stories.jsx +1 -1
- package/stories/Search/Search.stories.jsx +1 -1
- package/stories/Select/Select.stories.jsx +1 -1
- package/stories/SideNav/SideNav.stories.jsx +1 -1
- package/stories/SideNav/SideNavItem.stories.jsx +1 -1
- package/stories/SideNav/SideNavItemsGroup.stories.jsx +1 -1
- package/stories/Skeleton/Skeleton.stories.jsx +2 -2
- package/stories/Spacer/Spacer.stories.jsx +1 -1
- package/stories/StackView/StackView.stories.jsx +1 -1
- package/stories/StackView/StackWrap.stories.jsx +1 -1
- package/stories/StepTracker/StepTracker.stories.jsx +1 -1
- package/stories/Tabs/Tabs.stories.jsx +1 -1
- package/stories/Tags/Tags.stories.jsx +1 -1
- package/stories/TextInput/TextArea.stories.jsx +1 -1
- package/stories/TextInput/TextInput.stories.jsx +1 -1
- package/stories/ToggleSwitch/ToggleSwitch.stories.jsx +1 -1
- package/stories/Tooltip/Tooltip.stories.jsx +1 -1
- package/stories/TooltipButton/TooltipButton.stories.jsx +1 -1
- package/stories/Typography/Typography.stories.jsx +1 -1
- package/stories/platform-supports.jsx +1 -1
- package/stories/supports.jsx +2 -2
- package/src/Tabs/HorizontalScroll.jsx +0 -165
- package/src/Tabs/TabsScrollButton.jsx +0 -100
package/src/Tooltip/Tooltip.jsx
CHANGED
|
@@ -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 = (
|
|
103
|
-
|
|
102
|
+
const Tooltip = forwardRef(
|
|
103
|
+
({ children, content, position = 'auto', copy = 'en', tokens, variant }, ref) => {
|
|
104
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
|
|
112
|
+
const getCopy = useCopy({ dictionary, copy })
|
|
113
|
+
const themeTokens = useThemeTokens('Tooltip', tokens, variant)
|
|
113
114
|
|
|
114
|
-
|
|
115
|
+
const { arrowWidth, arrowOffset } = themeTokens
|
|
115
116
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
const subscription = Dimensions.addEventListener('change', ({ window }) => {
|
|
119
|
+
setWindowDimensions(window)
|
|
120
|
+
})
|
|
120
121
|
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
return () => subscription?.remove()
|
|
123
|
+
})
|
|
123
124
|
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
const toggleIsOpen = () => setIsOpen(!isOpen)
|
|
126
|
+
const close = () => setIsOpen(false)
|
|
126
127
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
128
|
+
const getPressableState = ({ pressed, hovered, focused }) => ({
|
|
129
|
+
pressed,
|
|
130
|
+
hover: hovered,
|
|
131
|
+
focus: focused
|
|
132
|
+
})
|
|
132
133
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
setIsOpen(false)
|
|
168
|
+
}, [windowDimensions])
|
|
168
169
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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.
|
|
241
|
-
|
|
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
|
-
|
|
245
|
-
|
|
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
|
-
</
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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 {
|
|
9
|
-
|
|
8
|
+
import {
|
|
9
|
+
a11yProps,
|
|
10
|
+
variantProp,
|
|
11
|
+
getTokensPropType,
|
|
12
|
+
getMaxFontMultiplier,
|
|
13
|
+
headingTags,
|
|
14
|
+
textTags,
|
|
15
|
+
getA11yPropsFromHtmlTag
|
|
16
|
+
} from '../utils'
|
|
10
17
|
/**
|
|
11
|
-
*
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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'
|
package/src/utils/a11y/index.js
CHANGED
|
@@ -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
|
+
}
|