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