@telus-uds/components-base 0.0.2-prerelease.3 → 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 (266) hide show
  1. package/.ultra.cache.json +1 -0
  2. package/CHANGELOG.md +55 -0
  3. package/__fixtures__/testTheme.js +528 -42
  4. package/__tests__/Button/ButtonBase.test.jsx +3 -32
  5. package/__tests__/Checkbox/Checkbox.test.jsx +94 -0
  6. package/__tests__/Divider/Divider.test.jsx +26 -5
  7. package/__tests__/Feedback/Feedback.test.jsx +42 -0
  8. package/__tests__/FlexGrid/Col.test.jsx +5 -0
  9. package/__tests__/InputSupports/InputSupports.test.jsx +50 -0
  10. package/__tests__/List/List.test.jsx +60 -0
  11. package/__tests__/Radio/Radio.test.jsx +87 -0
  12. package/__tests__/Select/Select.test.jsx +93 -0
  13. package/__tests__/Skeleton/Skeleton.test.jsx +61 -0
  14. package/__tests__/Spacer/Spacer.test.jsx +63 -0
  15. package/__tests__/StackView/StackView.test.jsx +216 -0
  16. package/__tests__/StackView/StackWrap.test.jsx +47 -0
  17. package/__tests__/StackView/getStackedContent.test.jsx +295 -0
  18. package/__tests__/Tags/Tags.test.jsx +328 -0
  19. package/__tests__/TextInput/TextArea.test.jsx +34 -0
  20. package/__tests__/TextInput/TextInputBase.test.jsx +120 -0
  21. package/__tests__/Tooltip/Tooltip.test.jsx +65 -0
  22. package/__tests__/Tooltip/getTooltipPosition.test.js +79 -0
  23. package/__tests__/utils/useCopy.test.js +31 -0
  24. package/__tests__/utils/useResponsiveProp.test.jsx +202 -0
  25. package/__tests__/utils/{spacing.test.jsx → useSpacingScale.test.jsx} +1 -1
  26. package/__tests__/utils/useUniqueId.test.js +31 -0
  27. package/jest.config.js +8 -2
  28. package/lib/Box/Box.js +7 -2
  29. package/lib/Button/Button.js +10 -3
  30. package/lib/Button/ButtonBase.js +79 -75
  31. package/lib/Button/ButtonGroup.js +24 -49
  32. package/lib/Button/ButtonLink.js +5 -0
  33. package/lib/Checkbox/Checkbox.js +308 -0
  34. package/lib/Checkbox/CheckboxInput.native.js +6 -0
  35. package/lib/Checkbox/CheckboxInput.web.js +57 -0
  36. package/lib/Checkbox/index.js +2 -0
  37. package/lib/Divider/Divider.js +40 -2
  38. package/lib/Feedback/Feedback.js +132 -0
  39. package/lib/Feedback/index.js +2 -0
  40. package/lib/Icon/Icon.js +9 -6
  41. package/lib/Icon/IconText.js +72 -0
  42. package/lib/Icon/index.js +2 -1
  43. package/lib/InputLabel/InputLabel.js +88 -0
  44. package/lib/InputLabel/LabelContent.native.js +8 -0
  45. package/lib/InputLabel/LabelContent.web.js +17 -0
  46. package/lib/InputLabel/index.js +2 -0
  47. package/lib/InputSupports/InputSupports.js +90 -0
  48. package/lib/InputSupports/index.js +2 -0
  49. package/lib/InputSupports/propTypes.js +55 -0
  50. package/lib/Link/ChevronLink.js +35 -10
  51. package/lib/Link/InlinePressable.native.js +78 -0
  52. package/lib/Link/InlinePressable.web.js +32 -0
  53. package/lib/Link/Link.js +11 -10
  54. package/lib/Link/LinkBase.js +69 -124
  55. package/lib/Link/TextButton.js +20 -9
  56. package/lib/Link/index.js +2 -1
  57. package/lib/List/List.js +52 -0
  58. package/lib/List/ListItem.js +207 -0
  59. package/lib/List/index.js +2 -0
  60. package/lib/Pagination/PageButton.js +3 -26
  61. package/lib/Pagination/SideButton.js +32 -42
  62. package/lib/Radio/Radio.js +291 -0
  63. package/lib/Radio/RadioInput.native.js +6 -0
  64. package/lib/Radio/RadioInput.web.js +59 -0
  65. package/lib/Radio/index.js +2 -0
  66. package/lib/Select/Group.native.js +14 -0
  67. package/lib/Select/Group.web.js +18 -0
  68. package/lib/Select/Item.native.js +9 -0
  69. package/lib/Select/Item.web.js +15 -0
  70. package/lib/Select/Picker.native.js +87 -0
  71. package/lib/Select/Picker.web.js +63 -0
  72. package/lib/Select/Select.js +272 -0
  73. package/lib/Select/index.js +6 -0
  74. package/lib/Skeleton/Skeleton.js +119 -0
  75. package/lib/Skeleton/index.js +2 -0
  76. package/lib/Spacer/Spacer.js +98 -0
  77. package/lib/Spacer/index.js +2 -0
  78. package/lib/StackView/StackView.js +107 -0
  79. package/lib/StackView/StackWrap.js +32 -0
  80. package/lib/StackView/StackWrap.native.js +3 -0
  81. package/lib/StackView/StackWrapBox.js +90 -0
  82. package/lib/StackView/StackWrapGap.js +50 -0
  83. package/lib/StackView/common.js +30 -0
  84. package/lib/StackView/getStackedContent.js +111 -0
  85. package/lib/StackView/index.js +5 -0
  86. package/lib/Tags/Tags.js +217 -0
  87. package/lib/Tags/index.js +2 -0
  88. package/lib/TextInput/TextArea.js +82 -0
  89. package/lib/TextInput/TextInput.js +54 -0
  90. package/lib/TextInput/TextInputBase.js +229 -0
  91. package/lib/TextInput/index.js +3 -0
  92. package/lib/TextInput/propTypes.js +31 -0
  93. package/lib/ThemeProvider/useThemeTokens.js +54 -3
  94. package/lib/ToggleSwitch/ToggleSwitch.js +1 -1
  95. package/lib/Tooltip/Backdrop.native.js +35 -0
  96. package/lib/Tooltip/Backdrop.web.js +52 -0
  97. package/lib/Tooltip/Tooltip.js +315 -0
  98. package/lib/Tooltip/dictionary.js +8 -0
  99. package/lib/Tooltip/getTooltipPosition.js +164 -0
  100. package/lib/Tooltip/index.js +2 -0
  101. package/lib/TooltipButton/TooltipButton.js +64 -0
  102. package/lib/TooltipButton/index.js +2 -0
  103. package/lib/Typography/Typography.js +4 -23
  104. package/lib/ViewportProvider/ViewportProvider.js +25 -0
  105. package/lib/ViewportProvider/index.js +2 -43
  106. package/lib/ViewportProvider/useViewport.js +3 -0
  107. package/lib/ViewportProvider/useViewportListener.js +43 -0
  108. package/lib/index.js +15 -1
  109. package/lib/utils/a11y/index.js +1 -0
  110. package/lib/utils/a11y/textSize.js +33 -0
  111. package/lib/utils/index.js +7 -1
  112. package/lib/utils/info/index.js +7 -0
  113. package/lib/utils/info/platform/index.js +11 -0
  114. package/lib/utils/info/platform/platform.android.js +1 -0
  115. package/lib/utils/info/platform/platform.ios.js +1 -0
  116. package/lib/utils/info/platform/platform.native.js +4 -0
  117. package/lib/utils/info/platform/platform.web.js +1 -0
  118. package/lib/utils/info/versions.js +5 -0
  119. package/lib/utils/input.js +3 -1
  120. package/lib/utils/pressability.js +92 -0
  121. package/lib/utils/propTypes.js +77 -8
  122. package/lib/utils/useCopy.js +16 -0
  123. package/lib/utils/useResponsiveProp.js +47 -0
  124. package/lib/utils/{spacing/useSpacingScale.js → useSpacingScale.js} +30 -9
  125. package/lib/utils/useUniqueId.js +12 -0
  126. package/package.json +7 -5
  127. package/release-context.json +4 -4
  128. package/src/Box/Box.jsx +4 -2
  129. package/src/Button/Button.jsx +6 -3
  130. package/src/Button/ButtonBase.jsx +72 -75
  131. package/src/Button/ButtonGroup.jsx +22 -39
  132. package/src/Button/ButtonLink.jsx +11 -2
  133. package/src/Checkbox/Checkbox.jsx +275 -0
  134. package/src/Checkbox/CheckboxInput.native.jsx +6 -0
  135. package/src/Checkbox/CheckboxInput.web.jsx +55 -0
  136. package/src/Checkbox/index.js +3 -0
  137. package/src/Divider/Divider.jsx +38 -3
  138. package/src/Feedback/Feedback.jsx +108 -0
  139. package/src/Feedback/index.js +3 -0
  140. package/src/Icon/Icon.jsx +11 -6
  141. package/src/Icon/IconText.jsx +63 -0
  142. package/src/Icon/index.js +2 -1
  143. package/src/InputLabel/InputLabel.jsx +99 -0
  144. package/src/InputLabel/LabelContent.native.jsx +6 -0
  145. package/src/InputLabel/LabelContent.web.jsx +13 -0
  146. package/src/InputLabel/index.js +3 -0
  147. package/src/InputSupports/InputSupports.jsx +86 -0
  148. package/src/InputSupports/index.js +3 -0
  149. package/src/InputSupports/propTypes.js +44 -0
  150. package/src/Link/ChevronLink.jsx +28 -7
  151. package/src/Link/InlinePressable.native.jsx +73 -0
  152. package/src/Link/InlinePressable.web.jsx +37 -0
  153. package/src/Link/Link.jsx +17 -13
  154. package/src/Link/LinkBase.jsx +62 -139
  155. package/src/Link/TextButton.jsx +25 -11
  156. package/src/Link/index.js +2 -1
  157. package/src/List/List.jsx +47 -0
  158. package/src/List/ListItem.jsx +187 -0
  159. package/src/List/index.js +3 -0
  160. package/src/Pagination/PageButton.jsx +3 -17
  161. package/src/Pagination/SideButton.jsx +27 -38
  162. package/src/Radio/Radio.jsx +270 -0
  163. package/src/Radio/RadioInput.native.jsx +6 -0
  164. package/src/Radio/RadioInput.web.jsx +57 -0
  165. package/src/Radio/index.js +3 -0
  166. package/src/Select/Group.native.jsx +14 -0
  167. package/src/Select/Group.web.jsx +15 -0
  168. package/src/Select/Item.native.jsx +10 -0
  169. package/src/Select/Item.web.jsx +11 -0
  170. package/src/Select/Picker.native.jsx +95 -0
  171. package/src/Select/Picker.web.jsx +67 -0
  172. package/src/Select/Select.jsx +265 -0
  173. package/src/Select/index.js +8 -0
  174. package/src/Skeleton/Skeleton.jsx +101 -0
  175. package/src/Skeleton/index.js +3 -0
  176. package/src/Spacer/Spacer.jsx +91 -0
  177. package/src/Spacer/index.js +3 -0
  178. package/src/StackView/StackView.jsx +104 -0
  179. package/src/StackView/StackWrap.jsx +33 -0
  180. package/src/StackView/StackWrap.native.jsx +4 -0
  181. package/src/StackView/StackWrapBox.jsx +93 -0
  182. package/src/StackView/StackWrapGap.jsx +49 -0
  183. package/src/StackView/common.jsx +28 -0
  184. package/src/StackView/getStackedContent.jsx +106 -0
  185. package/src/StackView/index.js +6 -0
  186. package/src/Tags/Tags.jsx +206 -0
  187. package/src/Tags/index.js +3 -0
  188. package/src/TextInput/TextArea.jsx +78 -0
  189. package/src/TextInput/TextInput.jsx +52 -0
  190. package/src/TextInput/TextInputBase.jsx +220 -0
  191. package/src/TextInput/index.js +4 -0
  192. package/src/TextInput/propTypes.js +29 -0
  193. package/src/ThemeProvider/useThemeTokens.js +54 -3
  194. package/src/ToggleSwitch/ToggleSwitch.jsx +1 -1
  195. package/src/Tooltip/Backdrop.native.jsx +33 -0
  196. package/src/Tooltip/Backdrop.web.jsx +60 -0
  197. package/src/Tooltip/Tooltip.jsx +294 -0
  198. package/src/Tooltip/dictionary.js +8 -0
  199. package/src/Tooltip/getTooltipPosition.js +161 -0
  200. package/src/Tooltip/index.js +3 -0
  201. package/src/TooltipButton/TooltipButton.jsx +53 -0
  202. package/src/TooltipButton/index.js +3 -0
  203. package/src/Typography/Typography.jsx +4 -19
  204. package/src/ViewportProvider/ViewportProvider.jsx +21 -0
  205. package/src/ViewportProvider/index.jsx +2 -41
  206. package/src/ViewportProvider/useViewport.js +5 -0
  207. package/src/ViewportProvider/useViewportListener.js +43 -0
  208. package/src/index.js +15 -1
  209. package/src/utils/a11y/index.js +1 -0
  210. package/src/utils/a11y/textSize.js +30 -0
  211. package/src/utils/index.js +8 -1
  212. package/src/utils/info/index.js +8 -0
  213. package/src/utils/info/platform/index.js +11 -0
  214. package/src/utils/info/platform/platform.android.js +1 -0
  215. package/src/utils/info/platform/platform.ios.js +1 -0
  216. package/src/utils/info/platform/platform.native.js +4 -0
  217. package/src/utils/info/platform/platform.web.js +1 -0
  218. package/src/utils/info/versions.js +6 -0
  219. package/src/utils/input.js +2 -1
  220. package/src/utils/pressability.js +92 -0
  221. package/src/utils/propTypes.js +97 -13
  222. package/src/utils/useCopy.js +13 -0
  223. package/src/utils/useResponsiveProp.js +50 -0
  224. package/src/utils/{spacing/useSpacingScale.js → useSpacingScale.js} +25 -10
  225. package/src/utils/useUniqueId.js +14 -0
  226. package/stories/A11yText/A11yText.stories.jsx +11 -5
  227. package/stories/ActivityIndicator/ActivityIndicator.stories.jsx +11 -2
  228. package/stories/Box/Box.stories.jsx +29 -2
  229. package/stories/Button/Button.stories.jsx +21 -20
  230. package/stories/Button/ButtonGroup.stories.jsx +2 -1
  231. package/stories/Button/ButtonLink.stories.jsx +6 -4
  232. package/stories/Card/Card.stories.jsx +13 -1
  233. package/stories/Checkbox/Checkbox.stories.jsx +71 -0
  234. package/stories/Divider/Divider.stories.jsx +26 -2
  235. package/stories/ExpandCollapse/ExpandCollapse.stories.jsx +74 -79
  236. package/stories/Feedback/Feedback.stories.jsx +96 -0
  237. package/stories/FlexGrid/01 FlexGrid.stories.jsx +20 -7
  238. package/stories/Icon/Icon.stories.jsx +11 -3
  239. package/stories/InputLabel/InputLabel.stories.jsx +42 -0
  240. package/stories/Link/ChevronLink.stories.jsx +20 -4
  241. package/stories/Link/Link.stories.jsx +39 -3
  242. package/stories/Link/TextButton.stories.jsx +24 -2
  243. package/stories/List/List.stories.jsx +117 -0
  244. package/stories/Pagination/Pagination.stories.jsx +28 -14
  245. package/stories/Radio/Radio.stories.jsx +113 -0
  246. package/stories/Select/Select.stories.jsx +55 -0
  247. package/stories/SideNav/SideNav.stories.jsx +17 -2
  248. package/stories/Skeleton/Skeleton.stories.jsx +36 -0
  249. package/stories/Spacer/Spacer.stories.jsx +38 -0
  250. package/stories/StackView/StackView.stories.jsx +75 -0
  251. package/stories/StackView/StackWrap.stories.jsx +64 -0
  252. package/stories/Tags/Tags.stories.jsx +69 -0
  253. package/stories/TextInput/TextArea.stories.jsx +100 -0
  254. package/stories/TextInput/TextInput.stories.jsx +103 -0
  255. package/stories/ToggleSwitch/ToggleSwitch.stories.jsx +16 -3
  256. package/stories/Tooltip/Tooltip.stories.jsx +81 -0
  257. package/stories/TooltipButton/TooltipButton.stories.jsx +11 -0
  258. package/stories/Typography/Typography.stories.jsx +12 -3
  259. package/stories/platform-supports.web.jsx +1 -1
  260. package/stories/supports.jsx +110 -14
  261. package/lib/Pagination/useCopy.js +0 -10
  262. package/lib/utils/spacing/index.js +0 -2
  263. package/lib/utils/spacing/utils.js +0 -32
  264. package/src/Pagination/useCopy.js +0 -7
  265. package/src/utils/spacing/index.js +0 -3
  266. package/src/utils/spacing/utils.js +0 -28
@@ -0,0 +1,294 @@
1
+ import React, { useEffect, useRef, useState } from 'react'
2
+ import { Dimensions, Platform, Pressable, StyleSheet, Text, View } from 'react-native'
3
+
4
+ import PropTypes from 'prop-types'
5
+ import { applyShadowToken, applyTextStyles, useThemeTokens } from '../ThemeProvider'
6
+ import { getTokensPropType, selectTokens, variantProp } 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 selectTooltipStyles = ({
14
+ backgroundColor,
15
+ paddingTop,
16
+ paddingBottom,
17
+ paddingLeft,
18
+ paddingRight,
19
+ borderRadius
20
+ }) => ({
21
+ backgroundColor,
22
+ paddingTop,
23
+ paddingBottom,
24
+ paddingLeft,
25
+ paddingRight,
26
+ borderRadius
27
+ })
28
+ const selectTooltipShadowStyles = ({ shadow, borderRadius }) => ({
29
+ borderRadius,
30
+ ...applyShadowToken(shadow)
31
+ })
32
+ const selectTooltipPositionStyles = ({ top, left, width }) => {
33
+ return { top, left, width }
34
+ }
35
+ const selectArrowStyles = (
36
+ { backgroundColor, arrowWidth, arrowBorderRadius, shadow },
37
+ { position, width: tooltipWidth, height: tooltipHeight }
38
+ ) => {
39
+ // the arrow width is actually a diagonal of the rectangle that we'll use as a tip
40
+ const rectangleSide = Math.sqrt((arrowWidth * arrowWidth) / 2)
41
+
42
+ // position the arrow at the side and center of the tooltip - this happens before rotation
43
+ // so we use the rectangle size as basis
44
+ const verticalOffset = (-1 * rectangleSide) / 2
45
+ const horizontalOffset = rectangleSide / 2
46
+
47
+ // percentage-based absolute positioning doesn't act well on native, so we have to
48
+ // calculate the pixel values
49
+ const directionalStyles = {
50
+ above: {
51
+ bottom: verticalOffset,
52
+ left: tooltipWidth / 2 - horizontalOffset,
53
+ transform: [{ rotateZ: '45deg' }]
54
+ },
55
+ below: {
56
+ top: verticalOffset,
57
+ left: tooltipWidth / 2 - horizontalOffset,
58
+ transform: [{ rotateZ: '-135deg' }]
59
+ },
60
+ left: {
61
+ right: verticalOffset,
62
+ top: tooltipHeight / 2 - horizontalOffset,
63
+ transform: [{ rotateZ: '-45deg' }]
64
+ },
65
+ right: {
66
+ left: verticalOffset,
67
+ top: tooltipHeight / 2 - horizontalOffset,
68
+ transform: [{ rotateZ: '135deg' }]
69
+ }
70
+ }
71
+
72
+ return {
73
+ backgroundColor,
74
+ width: rectangleSide,
75
+ height: rectangleSide,
76
+ borderBottomRightRadius: arrowBorderRadius, // this corner will be the arrow tip after rotation
77
+ ...applyShadowToken(shadow),
78
+ ...directionalStyles[position]
79
+ }
80
+ }
81
+
82
+ const selectTextStyles = (tokens) => applyTextStyles(selectTokens('Typography', tokens))
83
+ const defaultControl = (pressableState, variant) => (
84
+ <TooltipButton variant={{ ...pressableState, ...variant }} />
85
+ )
86
+
87
+ /**
88
+ * Tooltip provides a descriptive and detailed explanation or instructions. It can be used next to an input label
89
+ * to help a user fill it in, or as a standalone component.
90
+ *
91
+ * By default the TooltipButton component will be used as a control for triggering the tooltip, but you may attach
92
+ * a tooltip to any other component. A render function can be used to adjust the control's styling on state changes (hover, focus, etc.).
93
+ *
94
+ * ### Positioning
95
+ * By default a Tooltip will be automatically positioned in a way that ensures it fits within the viewport.
96
+ * You may suggest a position with a prop - it will be used, unless the tooltip would end up outside the viewport.
97
+ *
98
+ * ### Usage criteria
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
+ * - Tooltips may also be useful when vertical space is an issue.
101
+ */
102
+ const Tooltip = ({ children, content, position = 'auto', copy = 'en', tokens, variant }) => {
103
+ const [isOpen, setIsOpen] = useState(false)
104
+
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)
110
+
111
+ const getCopy = useCopy({ dictionary, copy })
112
+ const themeTokens = useThemeTokens('Tooltip', tokens, variant)
113
+
114
+ const { arrowWidth, arrowOffset } = themeTokens
115
+
116
+ useEffect(() => {
117
+ const subscription = Dimensions.addEventListener('change', ({ window }) => {
118
+ setWindowDimensions(window)
119
+ })
120
+
121
+ return () => subscription?.remove()
122
+ })
123
+
124
+ const toggleIsOpen = () => setIsOpen(!isOpen)
125
+ const close = () => setIsOpen(false)
126
+
127
+ const getPressableState = ({ pressed, hovered, focused }) => ({
128
+ pressed,
129
+ hover: hovered,
130
+ focus: focused
131
+ })
132
+
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
+ })
150
+ }
151
+ }
152
+
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])
164
+
165
+ useEffect(() => {
166
+ setIsOpen(false)
167
+ }, [windowDimensions])
168
+
169
+ useEffect(() => {
170
+ if (
171
+ (tooltipPosition !== null && !tooltipPosition?.isNormalized) ||
172
+ !isOpen ||
173
+ controlLayout === null ||
174
+ tooltipDimensions == null
175
+ ) {
176
+ return
177
+ }
178
+
179
+ const updatedPosition = getTooltipPosition(position, {
180
+ controlLayout,
181
+ tooltipDimensions,
182
+ windowDimensions,
183
+ arrowWidth,
184
+ arrowOffset
185
+ })
186
+
187
+ // avoid ending up in an infinite normalization loop
188
+ if (tooltipPosition?.isNormalized && updatedPosition.isNormalized) {
189
+ return
190
+ }
191
+
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
+ >
238
+ <View
239
+ style={[
240
+ staticStyles.arrow,
241
+ tooltipPosition && selectArrowStyles(themeTokens, tooltipPosition)
242
+ ]}
243
+ />
244
+ <View style={selectTooltipStyles(themeTokens)}>
245
+ <Text style={selectTextStyles(themeTokens)}>{content}</Text>
246
+ </View>
247
+ </View>
248
+ </Backdrop>
249
+ )}
250
+ </View>
251
+ )
252
+ }
253
+
254
+ Tooltip.propTypes = {
255
+ /**
256
+ * Used to render the control (i.e. tooltip trigger). If a render function is used it will receive the
257
+ * pressable state and tooltip variant as an argument.
258
+ */
259
+ children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
260
+ /**
261
+ * The message. Can be raw text or text components.
262
+ */
263
+ content: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
264
+ /**
265
+ * Select english or french copy for the accessible label.
266
+ */
267
+ copy: PropTypes.oneOf(['en', 'fr']),
268
+ /**
269
+ * Use to place the tooltip in a specific location (only if it fits within viewport).
270
+ */
271
+ position: PropTypes.oneOf(['auto', 'above', 'right', 'below', 'left']),
272
+ tokens: getTokensPropType('Tooltip'),
273
+ variant: variantProp.propType
274
+ }
275
+
276
+ export default Tooltip
277
+
278
+ const staticStyles = StyleSheet.create({
279
+ container: {
280
+ alignItems: 'flex-start'
281
+ },
282
+ tooltip: {
283
+ position: 'absolute',
284
+ maxWidth: 240,
285
+ top: 0,
286
+ left: 0
287
+ },
288
+ tooltipHidden: {
289
+ opacity: 0
290
+ },
291
+ arrow: {
292
+ position: 'absolute'
293
+ }
294
+ })
@@ -0,0 +1,8 @@
1
+ export default {
2
+ en: {
3
+ a11yText: 'Reveal additional information.'
4
+ },
5
+ fr: {
6
+ a11yText: 'Afficher des renseignements supplémentaires.'
7
+ }
8
+ }
@@ -0,0 +1,161 @@
1
+ function normalizePosition(position) {
2
+ const { left, right, bottom, top, width, ...rest } = position
3
+
4
+ // adjust the coordinates so that it fits within the window
5
+ const normalized = {
6
+ left: Math.max(0, left),
7
+ right: Math.max(0, right),
8
+ top: Math.max(0, top),
9
+ bottom // since it's ok the make the document grow downwards - no need to normalize here
10
+ }
11
+
12
+ const getAbsoluteDiff = (value1, value2) => Math.abs(Math.abs(value1) - Math.abs(value2))
13
+
14
+ // adjust the width by whatever has been subtracted from left or right
15
+ normalized.width =
16
+ width -
17
+ Math.abs(getAbsoluteDiff(left, normalized.left) - getAbsoluteDiff(right, normalized.right))
18
+
19
+ if (normalized.top !== top) {
20
+ normalized.bottom += normalized.top - top
21
+ }
22
+
23
+ const isNormalized =
24
+ normalized.right !== right || normalized.left !== left || normalized.top !== top
25
+
26
+ return {
27
+ ...normalized,
28
+ ...rest,
29
+ isNormalized
30
+ }
31
+ }
32
+
33
+ function invertPosition(position) {
34
+ switch (position) {
35
+ case 'above':
36
+ return 'below'
37
+ case 'below':
38
+ return 'above'
39
+ case 'left':
40
+ return 'right'
41
+ default:
42
+ return 'left'
43
+ }
44
+ }
45
+
46
+ function findRectByPosition(position, rectsArray) {
47
+ return rectsArray.find(({ position: rectPosition }) => rectPosition === position)
48
+ }
49
+
50
+ /**
51
+ * Used for absolute positioning of the tooltip. Since the tooltip is always centered relatively
52
+ * to the control (button) and we have a limited set of positions, an easy and consistent way
53
+ * of positioning it is to check all of the possible positions and pick one that will be rendered
54
+ * within the window bounds. This way we can also rely on the tooltip being actually rendered
55
+ * before it is shown, which makes it account for the width being limiting in styles, custom font
56
+ * rendering, etc.
57
+ */
58
+ function getTooltipPosition(
59
+ position,
60
+ { controlLayout, tooltipDimensions, windowDimensions, arrowWidth = 0, arrowOffset = 0 }
61
+ ) {
62
+ const { width: controlWidth, height: controlHeight, x: controlX, y: controlY } = controlLayout
63
+ const { width: tooltipWidth, height: tooltipHeight } = tooltipDimensions
64
+ const { width: windowWidth, height: windowHeight } = windowDimensions
65
+
66
+ const arrowSize = arrowWidth / 2 + arrowOffset
67
+
68
+ const horizontalBounds = {
69
+ left: controlX + controlWidth / 2 - tooltipWidth / 2,
70
+ right: windowWidth - (controlX + controlWidth / 2 + tooltipWidth / 2)
71
+ }
72
+
73
+ const verticalBounds = {
74
+ top: controlY + controlHeight / 2 - tooltipHeight / 2,
75
+ bottom: windowHeight - (controlY + controlHeight / 2 + tooltipHeight / 2)
76
+ }
77
+
78
+ // calculate absolute coordinates for each of the potential positions (relative to window)
79
+ const boundingRects = [
80
+ {
81
+ position: 'above',
82
+ ...horizontalBounds,
83
+ top: controlY - tooltipHeight - arrowSize,
84
+ bottom: windowHeight - (controlY - arrowSize)
85
+ },
86
+ {
87
+ position: 'right',
88
+ ...verticalBounds,
89
+ left: controlX + controlWidth + arrowSize,
90
+ right: windowWidth - (controlX + controlWidth + tooltipWidth + arrowSize)
91
+ },
92
+ {
93
+ position: 'below',
94
+ ...horizontalBounds,
95
+ top: controlY + controlHeight + arrowSize,
96
+ bottom: windowHeight - (controlY + controlHeight + tooltipHeight + arrowSize)
97
+ },
98
+ {
99
+ position: 'left',
100
+ ...verticalBounds,
101
+ left: controlX - tooltipWidth - arrowSize,
102
+ right: windowWidth - (controlX - arrowSize)
103
+ }
104
+ ].map((rect) => {
105
+ // an absolute value representing how much of the tooltip is overflowing the window on each side
106
+ const windowOverflow = Math.abs(
107
+ Math.min(rect.top, 0) -
108
+ Math.min(rect.left, 0) -
109
+ Math.min(rect.right, 0) -
110
+ Math.min(rect.bottom, 0)
111
+ )
112
+
113
+ return {
114
+ ...rect,
115
+ ...tooltipDimensions,
116
+ overflow: windowOverflow,
117
+ isNormalized: false
118
+ }
119
+ })
120
+
121
+ // the 'position' prop overrides the automatic positioning
122
+ if (position !== 'auto') {
123
+ let rect = findRectByPosition(position, boundingRects)
124
+
125
+ // check if the suggested 'position' fits in window
126
+ if (rect.overflow === 0) {
127
+ return rect
128
+ }
129
+
130
+ // otherwise try the inverted position (e.g. left -> right)
131
+ rect = findRectByPosition(invertPosition(position), boundingRects)
132
+
133
+ if (rect.overflow === 0) {
134
+ return rect
135
+ }
136
+ }
137
+
138
+ const inWindow = boundingRects.filter(({ overflow }) => overflow === 0)
139
+
140
+ // pick the first position that fits in window
141
+ // (these are sorted clockwise which makes them show where one would expect them to be)
142
+ if (inWindow.length > 0) {
143
+ return inWindow[0]
144
+ }
145
+
146
+ // if all positions would end up being out of window bounds, let's pick the one that is
147
+ // the least overflowing and normalize its position to fit within window bounds
148
+ boundingRects.sort(({ overflow: overflowA }, { overflow: overflowB }) => overflowA - overflowB)
149
+
150
+ const leastOverflowing = boundingRects[0]
151
+
152
+ // prefer 'below' over 'above', since we can always expand the document downwards,
153
+ // and 'above' might cause issues on small viewports with large tooltips
154
+ return normalizePosition(
155
+ leastOverflowing.position === 'above'
156
+ ? findRectByPosition('below', boundingRects)
157
+ : leastOverflowing
158
+ )
159
+ }
160
+
161
+ export default getTooltipPosition
@@ -0,0 +1,3 @@
1
+ import Tooltip from './Tooltip'
2
+
3
+ export default Tooltip
@@ -0,0 +1,53 @@
1
+ import React from 'react'
2
+ import { View } from 'react-native'
3
+
4
+ import { useThemeTokens } from '../ThemeProvider'
5
+ import { getTokensPropType, variantProp } from '../utils'
6
+
7
+ const selectOuterContainerStyles = ({
8
+ outerBorderColor,
9
+ outerBorderWidth = 0,
10
+ outerBorderGap = 0,
11
+ outerBorderRadius
12
+ }) => {
13
+ const outerBorderOffset = -1 * (outerBorderWidth + outerBorderGap)
14
+
15
+ return {
16
+ marginTop: outerBorderOffset,
17
+ marginLeft: outerBorderOffset,
18
+ marginRight: outerBorderOffset,
19
+ marginBottom: outerBorderOffset,
20
+ borderColor: outerBorderColor,
21
+ borderWidth: outerBorderWidth,
22
+ padding: outerBorderGap,
23
+ borderRadius: outerBorderRadius
24
+ }
25
+ }
26
+ const selectInnerContainerStyles = ({ borderRadius, width }) => ({ borderRadius, width })
27
+
28
+ const selectIconTokens = ({ iconSize, iconColor, iconScale = 1 }) => ({
29
+ size: iconSize,
30
+ color: iconColor,
31
+ scale: iconScale
32
+ })
33
+
34
+ const TooltipButton = ({ tokens, variant }) => {
35
+ const themeTokens = useThemeTokens('TooltipButton', tokens, variant)
36
+
37
+ const { icon: IconComponent } = themeTokens
38
+
39
+ return (
40
+ <View style={selectOuterContainerStyles(themeTokens)}>
41
+ <View style={selectInnerContainerStyles(themeTokens)}>
42
+ {IconComponent && <IconComponent tokens={selectIconTokens(themeTokens)} />}
43
+ </View>
44
+ </View>
45
+ )
46
+ }
47
+
48
+ TooltipButton.propTypes = {
49
+ tokens: getTokensPropType('TooltipButton'),
50
+ variant: variantProp.propType
51
+ }
52
+
53
+ export default TooltipButton
@@ -0,0 +1,3 @@
1
+ import TooltipButton from './TooltipButton'
2
+
3
+ export default TooltipButton
@@ -1,11 +1,11 @@
1
1
  import React from 'react'
2
2
  import PropTypes from 'prop-types'
3
- import { Platform, Text, View } from 'react-native'
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 } from '../utils/propTypes'
8
+ import { a11yProps, variantProp, getTokensPropType, getMaxFontMultiplier } from '../utils'
9
9
 
10
10
  /**
11
11
  * If passed a string like 'h1', 'h2' etc, returns the heading number as a string,
@@ -16,25 +16,12 @@ function getHeadingLevel(heading) {
16
16
  return match && match[1]
17
17
  }
18
18
 
19
- /**
20
- * Enforces `fontScaleCap` theme tokens as the maximum font size text can become
21
- * after iOS or Android font scaling, to give consistent accessible maximum sizes
22
- * that don't make the content unusable
23
- */
24
- function getMaxFontMultiplier({ fontSize, fontScaleCap }) {
25
- if (!fontScaleCap || !fontSize) return undefined
26
- if (fontScaleCap <= fontSize) return 1
27
- return fontScaleCap / fontSize
28
- }
29
-
30
19
  const selectTextStyles = ({
31
20
  fontWeight,
32
21
  fontSize,
33
22
  color,
34
23
  lineHeight,
35
24
  fontName,
36
- marginTop,
37
- marginBottom,
38
25
  textAlign,
39
26
  textTransform
40
27
  }) =>
@@ -44,8 +31,6 @@ const selectTextStyles = ({
44
31
  color,
45
32
  lineHeight,
46
33
  fontName,
47
- marginTop,
48
- marginBottom,
49
34
  textAlign,
50
35
  textTransform
51
36
  })
@@ -64,9 +49,9 @@ const Typography = ({
64
49
  const viewport = useViewport()
65
50
  const themeTokens = useThemeTokens('Typography', tokens, variant, { viewport })
66
51
  const textProps = {
67
- style: selectTextStyles(align ? { ...themeTokens, textAlign: align } : themeTokens)
52
+ style: selectTextStyles(align ? { ...themeTokens, textAlign: align } : themeTokens),
53
+ maxFontSizeMultiplier: getMaxFontMultiplier(themeTokens)
68
54
  }
69
- if (Platform.OS !== 'web') textProps.maxFontSizeMultiplier = getMaxFontMultiplier(themeTokens)
70
55
 
71
56
  const headingLevel = getHeadingLevel(heading)
72
57
  const a11y = {
@@ -0,0 +1,21 @@
1
+ import React, { useState } from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { viewports } from '@telus-uds/system-constants'
4
+ import { ViewportContext } from './useViewport'
5
+ import useViewportListener from './useViewportListener'
6
+
7
+ /**
8
+ * Provides an up-to-date viewport value from system-constants, available via the `useViewport` hook
9
+ */
10
+ const ViewportProvider = ({ children }) => {
11
+ // Default to the smallest viewport for mobile-first SSR. On client side, this is updated
12
+ // by useViewportListener in a layout effect before anything is shown to the user.
13
+ const [viewport, setViewport] = useState(viewports.keys[0])
14
+ useViewportListener(setViewport)
15
+ return <ViewportContext.Provider value={viewport}>{children}</ViewportContext.Provider>
16
+ }
17
+ ViewportProvider.propTypes = {
18
+ children: PropTypes.node.isRequired
19
+ }
20
+
21
+ export default ViewportProvider
@@ -1,43 +1,4 @@
1
- import React, { createContext, useState, useEffect, useContext } from 'react'
2
- import PropTypes from 'prop-types'
3
- import { Dimensions } from 'react-native'
4
- import { viewports } from '@telus-uds/system-constants'
5
-
6
- // we are using Dimensions instead of useWindowDimensions
7
- // that's because useWindowDimensions forces context to update on
8
- // every pixel change on the browser, therefore we used Dimension to only enforce
9
- // context update on viewport changes
10
- const lookupViewport = () => viewports.select(Dimensions.get('window').width)
11
-
12
- export const ViewportContext = createContext({})
13
-
14
- /**
15
- * Provides an up-to-date viewport value from system-constants, available via the `useViewport` hook
16
- */
17
- const ViewportProvider = ({ children }) => {
18
- // initialize with a function so we don't re-execute on each render
19
- const [viewport, setViewport] = useState(lookupViewport)
20
-
21
- useEffect(() => {
22
- const onChange = ({ window }) => setViewport(viewports.select(window.width))
23
-
24
- Dimensions.addEventListener('change', onChange)
25
- // We might have missed an update between calling `get` in render and
26
- // `addEventListener` in this handler, so we set it here. If there was
27
- // no change, React will filter out this update as a no-op.
28
- setViewport(lookupViewport())
29
- return () => {
30
- Dimensions.removeEventListener('change', onChange)
31
- }
32
- }, [])
33
-
34
- return <ViewportContext.Provider value={viewport}>{children}</ViewportContext.Provider>
35
- }
36
-
37
- ViewportProvider.propTypes = {
38
- children: PropTypes.node.isRequired
39
- }
1
+ import ViewportProvider from './ViewportProvider'
40
2
 
41
3
  export default ViewportProvider
42
-
43
- export const useViewport = () => useContext(ViewportContext)
4
+ export * from './useViewport'
@@ -0,0 +1,5 @@
1
+ import { createContext, useContext } from 'react'
2
+
3
+ export const ViewportContext = createContext({})
4
+
5
+ export const useViewport = () => useContext(ViewportContext)