@telus-uds/components-base 1.24.1 → 1.24.2

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 (50) hide show
  1. package/CHANGELOG.md +13 -2
  2. package/lib/Box/Box.js +3 -1
  3. package/lib/Divider/Divider.js +3 -0
  4. package/lib/FlexGrid/FlexGrid.js +3 -1
  5. package/lib/Link/InlinePressable.js +8 -1
  6. package/lib/Link/LinkBase.js +6 -8
  7. package/lib/StackView/StackView.js +3 -1
  8. package/lib/StackView/StackWrapBox.js +3 -1
  9. package/lib/StackView/StackWrapGap.js +3 -1
  10. package/lib/ThemeProvider/utils/styles.js +1 -3
  11. package/lib/Tooltip/Tooltip.js +85 -158
  12. package/lib/Tooltip/Tooltip.native.js +357 -0
  13. package/lib/Tooltip/index.js +6 -1
  14. package/lib/Tooltip/shared.js +39 -0
  15. package/lib/Typography/Typography.js +3 -1
  16. package/lib/utils/floating-ui/index.js +43 -0
  17. package/lib/utils/floating-ui/index.native.js +43 -0
  18. package/lib-module/Box/Box.js +3 -1
  19. package/lib-module/Divider/Divider.js +3 -0
  20. package/lib-module/FlexGrid/FlexGrid.js +3 -1
  21. package/lib-module/Link/InlinePressable.js +8 -1
  22. package/lib-module/Link/LinkBase.js +6 -8
  23. package/lib-module/StackView/StackView.js +3 -1
  24. package/lib-module/StackView/StackWrapBox.js +3 -1
  25. package/lib-module/StackView/StackWrapGap.js +3 -1
  26. package/lib-module/ThemeProvider/utils/styles.js +1 -3
  27. package/lib-module/Tooltip/Tooltip.js +85 -155
  28. package/lib-module/Tooltip/Tooltip.native.js +326 -0
  29. package/lib-module/Tooltip/index.js +4 -1
  30. package/lib-module/Tooltip/shared.js +27 -0
  31. package/lib-module/Typography/Typography.js +3 -1
  32. package/lib-module/utils/floating-ui/index.js +1 -0
  33. package/lib-module/utils/floating-ui/index.native.js +1 -0
  34. package/package.json +4 -2
  35. package/src/Box/Box.jsx +1 -0
  36. package/src/Divider/Divider.jsx +3 -0
  37. package/src/FlexGrid/FlexGrid.jsx +1 -0
  38. package/src/Link/InlinePressable.jsx +9 -3
  39. package/src/Link/LinkBase.jsx +17 -10
  40. package/src/StackView/StackView.jsx +1 -0
  41. package/src/StackView/StackWrapBox.jsx +1 -0
  42. package/src/StackView/StackWrapGap.jsx +1 -0
  43. package/src/ThemeProvider/utils/styles.js +1 -3
  44. package/src/Tooltip/Tooltip.jsx +79 -156
  45. package/src/Tooltip/Tooltip.native.jsx +283 -0
  46. package/src/Tooltip/index.js +5 -1
  47. package/src/Tooltip/shared.js +27 -0
  48. package/src/Typography/Typography.jsx +1 -0
  49. package/src/utils/floating-ui/index.js +1 -0
  50. package/src/utils/floating-ui/index.native.js +1 -0
@@ -0,0 +1,283 @@
1
+ import React, { forwardRef, useEffect, useRef, useState } from 'react'
2
+ import { Dimensions, Platform, Pressable, StyleSheet, Text, View } from 'react-native'
3
+ import propTypes from './shared'
4
+
5
+ import { applyShadowToken, applyTextStyles, useThemeTokens } from '../ThemeProvider'
6
+ import { a11yProps, selectSystemProps, selectTokens, viewProps } from '../utils'
7
+ import Backdrop from './Backdrop'
8
+ import getTooltipPosition from './getTooltipPosition'
9
+ import TooltipButton from '../TooltipButton'
10
+ import useCopy from '../utils/useCopy'
11
+ import dictionary from './dictionary'
12
+
13
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
14
+
15
+ const selectTooltipStyles = ({
16
+ backgroundColor,
17
+ paddingTop,
18
+ paddingBottom,
19
+ paddingLeft,
20
+ paddingRight,
21
+ borderRadius
22
+ }) => ({
23
+ backgroundColor,
24
+ paddingTop,
25
+ paddingBottom,
26
+ paddingLeft,
27
+ paddingRight,
28
+ borderRadius
29
+ })
30
+ const selectTooltipShadowStyles = ({ shadow, borderRadius }) => ({
31
+ borderRadius,
32
+ ...applyShadowToken(shadow)
33
+ })
34
+ const selectTooltipPositionStyles = ({ top, left, width }) => {
35
+ return { top, left, width }
36
+ }
37
+ const selectArrowStyles = (
38
+ { backgroundColor, arrowWidth, arrowBorderRadius, shadow },
39
+ { position, width: tooltipWidth, height: tooltipHeight }
40
+ ) => {
41
+ // the arrow width is actually a diagonal of the rectangle that we'll use as a tip
42
+ const rectangleSide = Math.sqrt((arrowWidth * arrowWidth) / 2)
43
+
44
+ // position the arrow at the side and center of the tooltip - this happens before rotation
45
+ // so we use the rectangle size as basis
46
+ const verticalOffset = (-1 * rectangleSide) / 2
47
+ const horizontalOffset = rectangleSide / 2
48
+
49
+ // percentage-based absolute positioning doesn't act well on native, so we have to
50
+ // calculate the pixel values
51
+ const directionalStyles = {
52
+ above: {
53
+ bottom: verticalOffset,
54
+ left: tooltipWidth / 2 - horizontalOffset,
55
+ transform: [{ rotateZ: '45deg' }]
56
+ },
57
+ below: {
58
+ top: verticalOffset,
59
+ left: tooltipWidth / 2 - horizontalOffset,
60
+ transform: [{ rotateZ: '-135deg' }]
61
+ },
62
+ left: {
63
+ right: verticalOffset,
64
+ top: tooltipHeight / 2 - horizontalOffset,
65
+ transform: [{ rotateZ: '-45deg' }]
66
+ },
67
+ right: {
68
+ left: verticalOffset,
69
+ top: tooltipHeight / 2 - horizontalOffset,
70
+ transform: [{ rotateZ: '135deg' }]
71
+ }
72
+ }
73
+
74
+ return {
75
+ backgroundColor,
76
+ width: rectangleSide,
77
+ height: rectangleSide,
78
+ borderBottomRightRadius: arrowBorderRadius, // this corner will be the arrow tip after rotation
79
+ ...applyShadowToken(shadow),
80
+ ...directionalStyles[position]
81
+ }
82
+ }
83
+
84
+ const selectTextStyles = (tokens) => applyTextStyles(selectTokens('Typography', tokens))
85
+ const defaultControl = (pressableState, variant) => (
86
+ <TooltipButton pressableState={pressableState} variant={variant} />
87
+ )
88
+
89
+ /**
90
+ * Tooltip provides a descriptive and detailed explanation or instructions. It can be used next to an input label
91
+ * to help a user fill it in, or as a standalone component.
92
+ *
93
+ * By default the TooltipButton component will be used as a control for triggering the tooltip, but you may attach
94
+ * a tooltip to any other component. A render function can be used to adjust the control's styling on state changes (hover, focus, etc.).
95
+ *
96
+ * ### Positioning
97
+ * By default a Tooltip will be automatically positioned in a way that ensures it fits within the viewport.
98
+ * You may suggest a position with a prop - it will be used, unless the tooltip would end up outside the viewport.
99
+ *
100
+ * ### Usage criteria
101
+ * - 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).
102
+ * - Tooltips may also be useful when vertical space is an issue.
103
+ */
104
+ const Tooltip = forwardRef(
105
+ ({ children, content, position = 'auto', copy = 'en', tokens, variant, ...rest }, ref) => {
106
+ const [isOpen, setIsOpen] = useState(false)
107
+
108
+ const controlRef = useRef()
109
+ const [controlLayout, setControlLayout] = useState(null)
110
+ const [tooltipDimensions, setTooltipDimensions] = useState(null)
111
+ const [windowDimensions, setWindowDimensions] = useState(Dimensions.get('window'))
112
+ const [tooltipPosition, setTooltipPosition] = useState(null)
113
+
114
+ const getCopy = useCopy({ dictionary, copy })
115
+ const themeTokens = useThemeTokens('Tooltip', tokens, variant)
116
+
117
+ const { arrowWidth, arrowOffset } = themeTokens
118
+
119
+ useEffect(() => {
120
+ const subscription = Dimensions.addEventListener('change', ({ window }) => {
121
+ setWindowDimensions(window)
122
+ })
123
+
124
+ return () => subscription?.remove()
125
+ })
126
+
127
+ const toggleIsOpen = () => setIsOpen(!isOpen)
128
+ const close = () => setIsOpen(false)
129
+
130
+ const getPressableState = ({ pressed, hovered, focused }) => ({
131
+ pressed,
132
+ hover: hovered,
133
+ focus: focused
134
+ })
135
+
136
+ const onTooltipLayout = ({
137
+ nativeEvent: {
138
+ layout: { width, height }
139
+ }
140
+ }) => {
141
+ if (
142
+ tooltipDimensions === null ||
143
+ tooltipDimensions.width !== width ||
144
+ tooltipDimensions.height !== height
145
+ ) {
146
+ setTooltipDimensions({
147
+ width: Platform.select({
148
+ web: width + 0.3, // avoids often unnecessary line breaks due to subpixel rendering of fonts
149
+ native: width
150
+ }),
151
+ height
152
+ })
153
+ }
154
+ }
155
+
156
+ useEffect(() => {
157
+ if (isOpen) {
158
+ controlRef.current.measureInWindow((x, y, width, height) => {
159
+ setControlLayout({ x, y, width, height })
160
+ })
161
+ } else {
162
+ setControlLayout(null)
163
+ setTooltipDimensions(null)
164
+ setTooltipPosition(null)
165
+ }
166
+ }, [isOpen])
167
+
168
+ useEffect(() => {
169
+ setIsOpen(false)
170
+ }, [windowDimensions])
171
+
172
+ useEffect(() => {
173
+ if (
174
+ (tooltipPosition !== null && !tooltipPosition?.isNormalized) ||
175
+ !isOpen ||
176
+ controlLayout === null ||
177
+ tooltipDimensions == null
178
+ ) {
179
+ return
180
+ }
181
+
182
+ const updatedPosition = getTooltipPosition(position, {
183
+ controlLayout,
184
+ tooltipDimensions,
185
+ windowDimensions,
186
+ arrowWidth,
187
+ arrowOffset
188
+ })
189
+
190
+ // avoid ending up in an infinite normalization loop
191
+ if (tooltipPosition?.isNormalized && updatedPosition.isNormalized) {
192
+ return
193
+ }
194
+
195
+ setTooltipPosition(updatedPosition)
196
+ }, [
197
+ isOpen,
198
+ position,
199
+ tooltipDimensions,
200
+ controlLayout,
201
+ windowDimensions,
202
+ arrowWidth,
203
+ arrowOffset,
204
+ tooltipPosition
205
+ ])
206
+
207
+ const control = children !== undefined ? children : defaultControl
208
+ const pressableStyles =
209
+ control === defaultControl ? Platform.select({ web: { outline: 'none' } }) : undefined
210
+ const pressableHitSlop =
211
+ control === defaultControl ? { top: 10, bottom: 10, left: 10, right: 10 } : undefined
212
+
213
+ return (
214
+ <View style={staticStyles.container} {...selectProps(rest)}>
215
+ <Pressable
216
+ onPress={toggleIsOpen}
217
+ ref={controlRef}
218
+ onBlur={close}
219
+ style={pressableStyles}
220
+ hitSlop={pressableHitSlop}
221
+ accessibilityLabel={getCopy('a11yText')}
222
+ accessibilityRole="button"
223
+ >
224
+ {typeof control === 'function'
225
+ ? (pressableState) => control(getPressableState(pressableState), variant)
226
+ : control}
227
+ </Pressable>
228
+ {isOpen && (
229
+ <Backdrop onPress={close}>
230
+ <View
231
+ ref={ref}
232
+ style={[
233
+ staticStyles.tooltip,
234
+ selectTooltipShadowStyles(themeTokens), // applied separately so that it doesn't cover the arrow
235
+ tooltipPosition && selectTooltipPositionStyles(tooltipPosition),
236
+ (tooltipPosition === null || tooltipPosition?.isNormalized) &&
237
+ staticStyles.tooltipHidden // visually hide the tooltip until we have a final measurement
238
+ ]}
239
+ onLayout={onTooltipLayout}
240
+ accessibilityRole="alert"
241
+ >
242
+ <View
243
+ style={[
244
+ staticStyles.arrow,
245
+ tooltipPosition && selectArrowStyles(themeTokens, tooltipPosition)
246
+ ]}
247
+ />
248
+ <View style={selectTooltipStyles(themeTokens)}>
249
+ <Text style={selectTextStyles(themeTokens)}>{content}</Text>
250
+ </View>
251
+ </View>
252
+ </Backdrop>
253
+ )}
254
+ </View>
255
+ )
256
+ }
257
+ )
258
+ Tooltip.displayName = 'NativeTooltip'
259
+
260
+ Tooltip.propTypes = {
261
+ ...selectedSystemPropTypes,
262
+ ...propTypes
263
+ }
264
+
265
+ export default Tooltip
266
+
267
+ const staticStyles = StyleSheet.create({
268
+ container: {
269
+ alignItems: 'flex-start'
270
+ },
271
+ tooltip: {
272
+ position: 'absolute',
273
+ maxWidth: 240,
274
+ top: 0,
275
+ left: 0
276
+ },
277
+ tooltipHidden: {
278
+ opacity: 0
279
+ },
280
+ arrow: {
281
+ position: 'absolute'
282
+ }
283
+ })
@@ -1,3 +1,7 @@
1
- import Tooltip from './Tooltip'
1
+ import { Dimensions } from 'react-native'
2
+ import NonNativeTooltip from './Tooltip'
3
+ import NativeToolTip from './Tooltip.native'
4
+
5
+ const Tooltip = !Dimensions.get('screen').width <= 340 ? NativeToolTip : NonNativeTooltip
2
6
 
3
7
  export default Tooltip
@@ -0,0 +1,27 @@
1
+ import PropTypes from 'prop-types'
2
+
3
+ import { getTokensPropType, variantProp } from '../utils'
4
+
5
+ const propTypes = {
6
+ /**
7
+ * Used to render the control (i.e. tooltip trigger). If a render function is used it will receive the
8
+ * pressable state and tooltip variant as an argument.
9
+ */
10
+ children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
11
+ /**
12
+ * The message. Can be raw text or text components.
13
+ */
14
+ content: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
15
+ /**
16
+ * Select English or French copy for the accessible label.
17
+ */
18
+ copy: PropTypes.oneOf(['en', 'fr']),
19
+ /**
20
+ * Use to place the tooltip in a specific location (only if it fits within viewport).
21
+ */
22
+ position: PropTypes.oneOf(['auto', 'above', 'right', 'below', 'left']),
23
+ tokens: getTokensPropType('Tooltip'),
24
+ variant: variantProp.propType
25
+ }
26
+
27
+ export default propTypes
@@ -71,6 +71,7 @@ const Typography = forwardRef(
71
71
  }
72
72
 
73
73
  const containerProps = {
74
+ accessibilityRole,
74
75
  ...getA11yPropsFromHtmlTag(tag, accessibilityRole),
75
76
  ...selectContainerProps(rest)
76
77
  }
@@ -0,0 +1 @@
1
+ export { useFloating, arrow, offset, shift, flip, autoPlacement } from '@floating-ui/react-dom'
@@ -0,0 +1 @@
1
+ export { useFloating, arrow, offset, shift, flip, autoPlacement } from '@floating-ui/react-native'