@telus-uds/components-base 1.18.1 → 1.19.1

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 (160) hide show
  1. package/CHANGELOG.md +22 -2
  2. package/component-docs.json +111 -16
  3. package/jest.config-android.js +17 -0
  4. package/jest.config-ios.js +18 -0
  5. package/jest.config-web.js +31 -0
  6. package/lib/ActivityIndicator/Spinner.js +7 -7
  7. package/lib/ActivityIndicator/Spinner.native.js +2 -2
  8. package/lib/BaseProvider/HydrationContext.js +1 -1
  9. package/lib/BaseProvider/TamaguiProvider.js +30 -0
  10. package/lib/Button/ButtonBase.js +8 -4
  11. package/lib/Button/ButtonDropdown.js +207 -0
  12. package/lib/Button/ButtonGroup.js +1 -1
  13. package/lib/Carousel/Carousel.js +31 -5
  14. package/lib/Carousel/CarouselContext.js +1 -1
  15. package/lib/Carousel/CarouselFirstFocus/CarouselFirstFocus.js +1 -1
  16. package/lib/Carousel/CarouselTabs/CarouselTabsPanel.js +1 -10
  17. package/lib/Carousel/CarouselThumbnail.js +2 -2
  18. package/lib/Checkbox/Checkbox.js +1 -1
  19. package/lib/Checkbox/CheckboxGroup.js +2 -2
  20. package/lib/Divider/Divider.js +2 -2
  21. package/lib/FlexGrid/Col/Col.js +1 -1
  22. package/lib/Icon/Icon.js +1 -1
  23. package/lib/MultiSelectFilter/ModalOverlay.js +136 -0
  24. package/lib/MultiSelectFilter/MultiSelectFilter.js +314 -0
  25. package/lib/MultiSelectFilter/dictionary.js +19 -0
  26. package/lib/MultiSelectFilter/index.js +13 -0
  27. package/lib/Pagination/PageButton.js +2 -2
  28. package/lib/Pagination/Pagination.js +3 -5
  29. package/lib/Pagination/SideButton.js +6 -4
  30. package/lib/Pagination/usePagination.js +2 -2
  31. package/lib/Progress/ProgressBar.js +3 -3
  32. package/lib/Progress/ProgressBarBackground.js +3 -3
  33. package/lib/QuickLinksFeature/QuickLinksFeature.js +91 -0
  34. package/lib/QuickLinksFeature/QuickLinksFeatureItem.js +157 -0
  35. package/lib/QuickLinksFeature/index.js +16 -0
  36. package/lib/Radio/Radio.js +2 -2
  37. package/lib/Radio/RadioGroup.js +2 -2
  38. package/lib/RadioCard/RadioCard.js +1 -1
  39. package/lib/RadioCard/RadioCardGroup.js +2 -2
  40. package/lib/Responsive/Responsive.js +58 -0
  41. package/lib/Responsive/index.js +13 -0
  42. package/lib/Search/Search.js +30 -63
  43. package/lib/Select/constants.js +15 -0
  44. package/lib/SideNav/SideNav.js +2 -2
  45. package/lib/Skeleton/Skeleton.js +1 -1
  46. package/lib/Skeleton/skeletonWebAnimation.js +1 -1
  47. package/lib/StackView/StackWrap.js +1 -3
  48. package/lib/StackView/getStackedContent.js +2 -2
  49. package/lib/Tabs/Tabs.js +2 -4
  50. package/lib/Tags/Tags.js +11 -5
  51. package/lib/TextInput/TextInputBase.js +53 -19
  52. package/lib/TextInput/dictionary.js +19 -0
  53. package/lib/ThemeProvider/utils/styles.js +3 -3
  54. package/lib/ThemeProvider/utils/theme-tokens.js +9 -7
  55. package/lib/Timeline/Timeline.js +1 -1
  56. package/lib/ToggleSwitch/ToggleSwitch.js +1 -1
  57. package/lib/ToggleSwitch/ToggleSwitchGroup.js +1 -1
  58. package/lib/Tooltip/Backdrop.js +10 -2
  59. package/lib/Tooltip/Tooltip.native.js +357 -0
  60. package/lib/Tooltip/shared.js +39 -0
  61. package/lib/Validator/Validator.js +271 -0
  62. package/lib/Validator/index.js +13 -0
  63. package/lib/index.js +9 -0
  64. package/lib/utils/BaseView/BaseView.js +64 -0
  65. package/lib/utils/BaseView/BaseView.native.js +16 -0
  66. package/lib/utils/BaseView/index.js +13 -0
  67. package/lib/utils/animation/useVerticalExpandAnimation.js +1 -1
  68. package/lib/utils/children.js +2 -2
  69. package/lib/utils/floating-ui/index.js +43 -0
  70. package/lib/utils/floating-ui/index.native.js +43 -0
  71. package/lib/utils/input.js +12 -6
  72. package/lib/utils/props/componentPropType.js +3 -3
  73. package/lib/utils/props/selectSystemProps.js +2 -2
  74. package/lib/utils/props/tokens.js +2 -2
  75. package/lib/utils/useOverlaidPosition.js +243 -0
  76. package/lib/utils/useSpacingScale.js +1 -3
  77. package/lib/utils/useUniqueId.js +1 -1
  78. package/lib-module/ActivityIndicator/Spinner.js +7 -7
  79. package/lib-module/ActivityIndicator/Spinner.native.js +2 -2
  80. package/lib-module/BaseProvider/HydrationContext.js +1 -1
  81. package/lib-module/BaseProvider/TamaguiProvider.js +22 -0
  82. package/lib-module/Button/ButtonBase.js +8 -4
  83. package/lib-module/Button/ButtonDropdown.js +181 -0
  84. package/lib-module/Button/ButtonGroup.js +1 -1
  85. package/lib-module/Carousel/Carousel.js +31 -5
  86. package/lib-module/Carousel/CarouselContext.js +1 -1
  87. package/lib-module/Carousel/CarouselFirstFocus/CarouselFirstFocus.js +1 -1
  88. package/lib-module/Carousel/CarouselTabs/CarouselTabsPanel.js +1 -10
  89. package/lib-module/Carousel/CarouselThumbnail.js +2 -2
  90. package/lib-module/Checkbox/Checkbox.js +1 -1
  91. package/lib-module/Checkbox/CheckboxGroup.js +2 -2
  92. package/lib-module/Divider/Divider.js +2 -2
  93. package/lib-module/FlexGrid/Col/Col.js +1 -1
  94. package/lib-module/Icon/Icon.js +1 -1
  95. package/lib-module/MultiSelectFilter/ModalOverlay.js +112 -0
  96. package/lib-module/MultiSelectFilter/MultiSelectFilter.js +286 -0
  97. package/lib-module/MultiSelectFilter/dictionary.js +12 -0
  98. package/lib-module/MultiSelectFilter/index.js +2 -0
  99. package/lib-module/Pagination/PageButton.js +2 -2
  100. package/lib-module/Pagination/Pagination.js +3 -5
  101. package/lib-module/Pagination/SideButton.js +6 -4
  102. package/lib-module/Pagination/usePagination.js +2 -2
  103. package/lib-module/Progress/ProgressBar.js +3 -3
  104. package/lib-module/Progress/ProgressBarBackground.js +3 -3
  105. package/lib-module/QuickLinksFeature/QuickLinksFeature.js +69 -0
  106. package/lib-module/QuickLinksFeature/QuickLinksFeatureItem.js +130 -0
  107. package/lib-module/QuickLinksFeature/index.js +4 -0
  108. package/lib-module/Radio/Radio.js +2 -2
  109. package/lib-module/Radio/RadioGroup.js +2 -2
  110. package/lib-module/RadioCard/RadioCard.js +1 -1
  111. package/lib-module/RadioCard/RadioCardGroup.js +2 -2
  112. package/lib-module/Responsive/Responsive.js +45 -0
  113. package/lib-module/Responsive/index.js +2 -0
  114. package/lib-module/Search/Search.js +30 -61
  115. package/lib-module/Select/constants.js +5 -0
  116. package/lib-module/SideNav/SideNav.js +2 -2
  117. package/lib-module/Skeleton/Skeleton.js +1 -1
  118. package/lib-module/Skeleton/skeletonWebAnimation.js +1 -1
  119. package/lib-module/StackView/StackWrap.js +1 -3
  120. package/lib-module/StackView/getStackedContent.js +2 -2
  121. package/lib-module/Tabs/Tabs.js +2 -4
  122. package/lib-module/Tags/Tags.js +11 -5
  123. package/lib-module/TextInput/TextInputBase.js +52 -19
  124. package/lib-module/TextInput/dictionary.js +12 -0
  125. package/lib-module/ThemeProvider/utils/styles.js +3 -3
  126. package/lib-module/ThemeProvider/utils/theme-tokens.js +9 -7
  127. package/lib-module/Timeline/Timeline.js +1 -1
  128. package/lib-module/ToggleSwitch/ToggleSwitch.js +1 -1
  129. package/lib-module/ToggleSwitch/ToggleSwitchGroup.js +1 -1
  130. package/lib-module/Tooltip/Backdrop.js +10 -2
  131. package/lib-module/Tooltip/Tooltip.native.js +326 -0
  132. package/lib-module/Tooltip/shared.js +27 -0
  133. package/lib-module/Validator/Validator.js +245 -0
  134. package/lib-module/Validator/index.js +2 -0
  135. package/lib-module/index.js +1 -0
  136. package/lib-module/utils/BaseView/BaseView.js +43 -0
  137. package/lib-module/utils/BaseView/BaseView.native.js +6 -0
  138. package/lib-module/utils/BaseView/index.js +2 -0
  139. package/lib-module/utils/animation/useVerticalExpandAnimation.js +1 -1
  140. package/lib-module/utils/children.js +2 -2
  141. package/lib-module/utils/floating-ui/index.js +1 -0
  142. package/lib-module/utils/floating-ui/index.native.js +1 -0
  143. package/lib-module/utils/input.js +12 -6
  144. package/lib-module/utils/props/componentPropType.js +3 -3
  145. package/lib-module/utils/props/selectSystemProps.js +2 -2
  146. package/lib-module/utils/props/tokens.js +2 -2
  147. package/lib-module/utils/useOverlaidPosition.js +232 -0
  148. package/lib-module/utils/useSpacingScale.js +1 -3
  149. package/lib-module/utils/useUniqueId.js +1 -1
  150. package/package.json +7 -4
  151. package/src/Button/ButtonBase.jsx +4 -2
  152. package/src/Carousel/Carousel.jsx +42 -10
  153. package/src/Carousel/CarouselTabs/CarouselTabsPanel.jsx +0 -10
  154. package/src/Pagination/SideButton.jsx +5 -5
  155. package/src/Responsive/Responsive.jsx +33 -0
  156. package/src/Responsive/index.js +3 -0
  157. package/src/Search/Search.jsx +17 -32
  158. package/src/Tags/Tags.jsx +46 -33
  159. package/src/TextInput/TextInputBase.jsx +46 -16
  160. package/src/index.js +1 -0
@@ -0,0 +1,43 @@
1
+ import React, { forwardRef } from 'react';
2
+ import NativeView from "react-native-web/dist/exports/View";
3
+ import StyleSheet from "react-native-web/dist/exports/StyleSheet";
4
+ import PropTypes from 'prop-types';
5
+ import { useTheme } from '../../ThemeProvider';
6
+ /**
7
+ * Identical to React Native's View and supporting all the same props, but with:
8
+ * - a zIndex: 'auto' style added to prevent unexpectedly causing children to overlap other elements from other stacking contexts
9
+ */
10
+
11
+ import { jsx as _jsx } from "react/jsx-runtime";
12
+ const BaseView = /*#__PURE__*/forwardRef((_ref, ref) => {
13
+ let {
14
+ children,
15
+ style,
16
+ ...rest
17
+ } = _ref;
18
+ const {
19
+ themeOptions
20
+ } = useTheme();
21
+ const styleProp = Array.isArray(style) ? [...style] : [style];
22
+
23
+ if (!themeOptions.forceZIndex) {
24
+ styleProp.unshift(styles.resetZIndex);
25
+ }
26
+
27
+ return /*#__PURE__*/_jsx(NativeView, { ...rest,
28
+ style: styleProp,
29
+ ref: ref,
30
+ children: children
31
+ });
32
+ });
33
+ BaseView.displayName = 'BaseView';
34
+ const styles = StyleSheet.create({
35
+ resetZIndex: {
36
+ zIndex: 'auto'
37
+ }
38
+ });
39
+ BaseView.propTypes = {
40
+ children: PropTypes.node,
41
+ style: PropTypes.oneOfType([PropTypes.object, PropTypes.array])
42
+ };
43
+ export default BaseView;
@@ -0,0 +1,6 @@
1
+ import BaseView from "react-native-web/dist/exports/View";
2
+ /**
3
+ * Android crashes on non-standard style properties like `zIndex` so adding a `BaseView` for native platforms
4
+ */
5
+
6
+ export default BaseView;
@@ -0,0 +1,2 @@
1
+ import BaseView from './BaseView';
2
+ export default BaseView;
@@ -72,7 +72,7 @@ function useVerticalExpandAnimation(_ref) {
72
72
  }
73
73
  } else if (Platform.OS === 'web') {
74
74
  const transitionDuration = isExpanded ? expandDuration : collapseDuration;
75
- containerStyles.transition = "height ".concat(transitionDuration, "ms ease-in-out");
75
+ containerStyles.transition = `height ${transitionDuration}ms ease-in-out`;
76
76
  containerStyles.height = isExpanded ? containerHeight : 0;
77
77
  } else if (Platform.OS === 'ios' && isExpanded && !isAnimating && !expandStateChanged && typeof containerHeight === 'number') {
78
78
  // iOS reflows text while the height animation is in progress. Sometimes, it hits a timing bug where
@@ -55,7 +55,7 @@ const isWrapable = child => {
55
55
  return isStringOrNumber(child) || child.type === A11yText || ((_child$type = child.type) === null || _child$type === void 0 ? void 0 : _child$type.name) === 'FootnoteLink';
56
56
  };
57
57
 
58
- const combineKeys = childrenArray => childrenArray.reduce((newKey, child) => "".concat(newKey).concat(child.key || ''), ''); // Group wrappable children for one `<Text>` parent, merging adjacent text nodes
58
+ const combineKeys = childrenArray => childrenArray.reduce((newKey, child) => `${newKey}${child.key || ''}`, ''); // Group wrappable children for one `<Text>` parent, merging adjacent text nodes
59
59
 
60
60
 
61
61
  const wrapChild = (child, wrappedText) => {
@@ -64,7 +64,7 @@ const wrapChild = (child, wrappedText) => {
64
64
 
65
65
  if (lastIndex >= 0 && isStringOrNumber(child) && isStringOrNumber(wrappedText[lastIndex])) {
66
66
  /* eslint-disable-next-line no-param-reassign */
67
- wrappedText[lastIndex] = "".concat(wrappedText[lastIndex]).concat(child);
67
+ wrappedText[lastIndex] = `${wrappedText[lastIndex]}${child}`;
68
68
  } else {
69
69
  wrappedText.push(child);
70
70
  }
@@ -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';
@@ -21,23 +21,29 @@ const validateProps = (_ref, _ref2, hookName) => {
21
21
  const usageError = error => {
22
22
  // Errors inside hooks in React Native get incomplete stack traces pointing at parent component only.
23
23
  // Help devs out by telling them exactly which hook threw the error as well as why.
24
- throw new Error("".concat(hookName, " ").concat(error, ".\n\nConsumers of this hook must be one of:\n1. An \"uncontrolled\" component responding directly to user actions, with an optional `initialValue").concat(s, "` but no `value").concat(s, "` prop,\n2. A \"controlled\" component where an always-defined `value").concat(s, "` prop is managed by an `onChange` handler, with no `initialValue").concat(s, "`,\n3. A \"read-only\" component, with `readOnly` prop set as `true`.\n"));
24
+ throw new Error(`${hookName} ${error}.
25
+
26
+ Consumers of this hook must be one of:
27
+ 1. An "uncontrolled" component responding directly to user actions, with an optional \`initialValue${s}\` but no \`value${s}\` prop,
28
+ 2. A "controlled" component where an always-defined \`value${s}\` prop is managed by an \`onChange\` handler, with no \`initialValue${s}\`,
29
+ 3. A "read-only" component, with \`readOnly\` prop set as \`true\`.
30
+ `);
25
31
  };
26
32
 
27
33
  if (value && !onChange && !readOnly) {
28
- usageError("has `value".concat(s, "` prop without `onChange` or `readOnly`"));
34
+ usageError(`has \`value${s}\` prop without \`onChange\` or \`readOnly\``);
29
35
  }
30
36
 
31
37
  if (initialValue && value) {
32
- usageError("has both `initialValue".concat(s, "` and `value").concat(s, "`"));
38
+ usageError(`has both \`initialValue${s}\` and \`value${s}\``);
33
39
  }
34
40
 
35
41
  if (isControlled && !isCurrentlyControlled) {
36
- usageError("stopped receiving `value".concat(s, "` from parent after delegating state management"));
42
+ usageError(`stopped receiving \`value${s}\` from parent after delegating state management`);
37
43
  }
38
44
 
39
45
  if (!isControlled && isCurrentlyControlled) {
40
- usageError("started receiving `value".concat(s, "` from parent after starting managing own state"));
46
+ usageError(`started receiving \`value${s}\` from parent after starting managing own state`);
41
47
  }
42
48
  };
43
49
  /**
@@ -148,7 +154,7 @@ export const useMultipleInputValues = function () {
148
154
  onChange,
149
155
  value: values,
150
156
  // if we're controlling our own state, always start with an array
151
- initialValue: initialValues !== null && initialValues !== void 0 ? initialValues : values === undefined ? [] : undefined
157
+ initialValue: initialValues ?? (values === undefined ? [] : undefined)
152
158
  }, 'useMultipleInputValues');
153
159
  const enforceMaxValues = useCallback(newValues => {
154
160
  if (!maxValues) return newValues;
@@ -26,8 +26,8 @@ export default function componentPropType(passedName) {
26
26
  const nameInProp = ((_props$propName$type = props[propName].type) === null || _props$propName$type === void 0 ? void 0 : _props$propName$type.displayName) || ((_props$propName$type2 = props[propName].type) === null || _props$propName$type2 === void 0 ? void 0 : _props$propName$type2.name);
27
27
 
28
28
  if (!nameInProp || !passedNames.includes(nameInProp)) {
29
- const propDescription = nameInProp ? "Component ".concat(nameInProp) : typeof props[propName];
30
- return new Error("".concat(componentName, ": ").concat(propDescription, " was passed to `").concat(propName, "` prop; should be ").concat(passedNames.join(' or ')));
29
+ const propDescription = nameInProp ? `Component ${nameInProp}` : typeof props[propName];
30
+ return new Error(`${componentName}: ${propDescription} was passed to \`${propName}\` prop; should be ${passedNames.join(' or ')}`);
31
31
  }
32
32
 
33
33
  return undefined;
@@ -35,7 +35,7 @@ export default function componentPropType(passedName) {
35
35
 
36
36
  const checkRequired = (props, propName, componentName) => {
37
37
  if (props[propName] === undefined) {
38
- return new Error("The prop `".concat(propName, "` is marked as required in `").concat(componentName, "`, but its value is ").concat(props[propName], "."));
38
+ return new Error(`The prop \`${propName}\` is marked as required in \`${componentName}\`, but its value is ${props[propName]}.`);
39
39
  }
40
40
 
41
41
  return undefined;
@@ -3,7 +3,7 @@
3
3
  export default function selectSystemProps(systemPropHelpers) {
4
4
  const selectProps = props => systemPropHelpers.reduce((acc, propHelper) => {
5
5
  if (typeof (propHelper === null || propHelper === void 0 ? void 0 : propHelper.select) !== 'function') {
6
- throw new Error("An invalid system prop helper has been passed to 'selectSystemProps': prop selector ('.select') is missing in ".concat(propHelper));
6
+ throw new Error(`An invalid system prop helper has been passed to 'selectSystemProps': prop selector ('.select') is missing in ${propHelper}`);
7
7
  }
8
8
 
9
9
  return { ...acc,
@@ -13,7 +13,7 @@ export default function selectSystemProps(systemPropHelpers) {
13
13
 
14
14
  const selectedPropTypes = systemPropHelpers.reduce((acc, propHelper) => {
15
15
  if (!(propHelper !== null && propHelper !== void 0 && propHelper.types)) {
16
- throw new Error("An invalid system prop helper has been passed to 'selectSystemProps': types selector ('.types') is missing in ".concat(propHelper));
16
+ throw new Error(`An invalid system prop helper has been passed to 'selectSystemProps': types selector ('.types') is missing in ${propHelper}`);
17
17
  }
18
18
 
19
19
  return { ...acc,
@@ -9,7 +9,7 @@ export const getTokenNames = componentName => {
9
9
  const componentTokenSchema = tokenKeys[componentName];
10
10
 
11
11
  if (!componentTokenSchema) {
12
- throw new Error("No \"".concat(componentName, "\" tokenKeys in @telus-uds/system-theme-tokens"));
12
+ throw new Error(`No "${componentName}" tokenKeys in @telus-uds/system-theme-tokens`);
13
13
  }
14
14
 
15
15
  return Object.keys(componentTokenSchema);
@@ -47,7 +47,7 @@ export const getTokenNames = componentName => {
47
47
  export const selectTokens = (specifier, tokens, prefix) => {
48
48
  const tokenNames = typeof specifier === 'string' ? getTokenNames(specifier) : specifier;
49
49
  const filteredTokens = tokenNames.reduce((validTokens, key) => {
50
- const prefixedKey = prefix ? "".concat(prefix).concat(key[0].toUpperCase()).concat(key.slice(1)) : key;
50
+ const prefixedKey = prefix ? `${prefix}${key[0].toUpperCase()}${key.slice(1)}` : key;
51
51
  const token = tokens[prefixedKey];
52
52
  return token !== undefined ? { ...validTokens,
53
53
  [key]: token
@@ -0,0 +1,232 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import Dimensions from "react-native-web/dist/exports/Dimensions";
3
+
4
+ const adjustHorizontalToFit = (initialOffset, windowWidth, sourceWidth) => {
5
+ const offset = Math.max(0, initialOffset);
6
+ const otherEdgeOverflow = Math.max(0, offset + sourceWidth - windowWidth);
7
+ const tooWideBy = Math.max(0, otherEdgeOverflow - offset);
8
+ const adjusted = {
9
+ offset: Math.max(0, offset - otherEdgeOverflow)
10
+ };
11
+ if (tooWideBy) adjusted.width = Math.max(0, sourceWidth - tooWideBy);
12
+ return adjusted;
13
+ };
14
+
15
+ const getPosition = _ref => {
16
+ let {
17
+ edge,
18
+ fromEdge,
19
+ sourceSize
20
+ } = _ref;
21
+
22
+ switch (edge) {
23
+ case 'near':
24
+ return fromEdge;
25
+
26
+ case 'mid':
27
+ return fromEdge + sourceSize / 2;
28
+
29
+ case 'far':
30
+ return fromEdge + sourceSize;
31
+
32
+ default:
33
+ return 0;
34
+ }
35
+ };
36
+
37
+ const getEdgeType = (align, alignSide) => {
38
+ const alignTo = align[alignSide];
39
+ const edge = ['center', 'middle'].includes(alignTo) && 'mid' || (alignSide === alignTo ? 'near' : 'far');
40
+ return edge;
41
+ };
42
+ /**
43
+ * Based on UDS's private getTooltipPosition but generalised.
44
+ *
45
+ * Used for absolute positioning of the tooltip. Since the tooltip is always centered relatively
46
+ * to the source (button) and we have a limited set of positions, an easy and consistent way
47
+ * of positioning it is to check all of the possible positions and pick one that will be rendered
48
+ * within the window bounds. This way we can also rely on the tooltip being actually rendered
49
+ * before it is shown, which makes it account for the width being limiting in styles, custom font
50
+ * rendering, etc.
51
+ */
52
+
53
+
54
+ function getOverlaidPosition(_ref2) {
55
+ let {
56
+ sourceLayout,
57
+ targetDimensions,
58
+ windowDimensions,
59
+ offsets = {},
60
+ align
61
+ } = _ref2;
62
+ // Web-only: this will be difficult to mimic on native because there's no global scroll position.
63
+ // TODO: wire something in e.g. a scroll ref accessible from a provider included in Allium provider
64
+ // that can be passed to the appropriate ScrollView?
65
+ const {
66
+ scrollX = 0,
67
+ scrollY = 0
68
+ } = typeof window === 'object' ? window : {}; // Will have top, bottom, left and/or right offsets depending on `align`
69
+
70
+ const positioning = {};
71
+ if (align.top) positioning.top = getPosition({
72
+ edge: getEdgeType(align, 'top'),
73
+ fromEdge: sourceLayout.y + scrollY + (offsets.vertical ?? 0),
74
+ sourceSize: sourceLayout.height
75
+ });
76
+ if (align.middle) positioning.top = getPosition({
77
+ edge: getEdgeType(align, 'middle'),
78
+ fromEdge: sourceLayout.y + scrollY + (offsets.vertical ?? 0) - targetDimensions.height / 2,
79
+ sourceSize: sourceLayout.height
80
+ });
81
+ if (align.bottom) positioning.bottom = getPosition({
82
+ edge: getEdgeType(align, 'bottom'),
83
+ fromEdge: windowDimensions.height - (sourceLayout.y + scrollY + sourceLayout.height - (offsets.vertical ?? 0)),
84
+ sourceSize: sourceLayout.height
85
+ });
86
+ if (align.left) positioning.left = getPosition({
87
+ edge: getEdgeType(align, 'left'),
88
+ fromEdge: sourceLayout.x + scrollX + (offsets.horizontal ?? 0),
89
+ sourceSize: sourceLayout.width
90
+ });
91
+ if (align.center) positioning.left = getPosition({
92
+ edge: getEdgeType(align, 'center'),
93
+ fromEdge: sourceLayout.x + scrollX + (offsets.horizontal ?? 0) - targetDimensions.width / 2,
94
+ sourceSize: sourceLayout.width
95
+ });
96
+ if (align.right) positioning.right = getPosition({
97
+ edge: getEdgeType(align, 'right'),
98
+ fromEdge: windowDimensions.width - (sourceLayout.x + scrollX + sourceLayout.width - (offsets.horizontal ?? 0)),
99
+ sourceSize: sourceLayout.width
100
+ });
101
+
102
+ if (!(align.left && align.right)) {
103
+ // Check if the position and/or width need adjusting to fit on the screen
104
+ const side = align.right ? 'right' : 'left';
105
+ const adjusted = adjustHorizontalToFit(positioning[side], windowDimensions.width, sourceLayout.width);
106
+ if (typeof adjusted.width === 'number') positioning.width = adjusted.width;
107
+
108
+ if (typeof adjusted.offset === 'number') {
109
+ positioning[side] = adjusted.offset;
110
+ }
111
+ }
112
+
113
+ return positioning;
114
+ }
115
+ /**
116
+ * Positions an element in a modal or portal so that it appears tooltip-like below the
117
+ * target element.
118
+ *
119
+ * @TODO - add support for positioning other than 'below' like UDS's tooltip (this is not
120
+ * a small task because UDS's tooltip logic only really works for short text - it might be
121
+ * better to use a third-party library).
122
+ */
123
+
124
+
125
+ const useOverlaidPosition = _ref3 => {
126
+ let {
127
+ isShown = false,
128
+ offsets,
129
+ // By default, align the overlaid target's `top` to the bottom of the source, and center horizontally.
130
+ align = {
131
+ center: 'center',
132
+ top: 'bottom'
133
+ }
134
+ } = _ref3;
135
+ // Element in main document flow that the targetRef element is positioned around
136
+ const sourceRef = useRef(null);
137
+ const [sourceLayout, setSourceLayout] = useState(null); // Element in a modal or portal overlay positioned to appear adjacent to sourceRef
138
+
139
+ const targetRef = useRef(null);
140
+ const [targetDimensions, setTargetDimensions] = useState(null);
141
+ const [windowDimensions, setWindowDimensions] = useState(null);
142
+ const onTargetLayout = useCallback(_ref4 => {
143
+ let {
144
+ nativeEvent: {
145
+ layout: {
146
+ width,
147
+ height
148
+ }
149
+ }
150
+ } = _ref4;
151
+ // NOTE: UDS's Tooltip logic injects some additional width to allow for antialiasing etc of text,
152
+ // avoiding adding unnecessary line breaks to text that is slightly wider than it thinks it is.
153
+ // That is probably something specific to text tooltips that doesn't belong in a generic hook.
154
+ setTargetDimensions(previousDimensions => {
155
+ // Re-render on first non-zero width / height: avoid infinite loops on changes, or mispositioning
156
+ // if user scrolls while a slidedown animation is changing the height and recalculating position.
157
+ if (!previousDimensions && width && height) {
158
+ return {
159
+ width,
160
+ height
161
+ };
162
+ }
163
+
164
+ return previousDimensions;
165
+ });
166
+ }, []);
167
+ const readyToShow = Boolean(isShown && sourceRef.current);
168
+ useEffect(() => {
169
+ const handleDimensionsChange = _ref5 => {
170
+ var _sourceRef$current;
171
+
172
+ let {
173
+ window
174
+ } = _ref5;
175
+ (_sourceRef$current = sourceRef.current) === null || _sourceRef$current === void 0 ? void 0 : _sourceRef$current.measureInWindow((x, y, width, height) => {
176
+ // Could add a debouncer here if there's too many rerenders during gradual resizes
177
+ setWindowDimensions(window);
178
+ setSourceLayout({
179
+ x,
180
+ y,
181
+ width,
182
+ height
183
+ });
184
+ });
185
+ };
186
+
187
+ let subscription;
188
+
189
+ const unsubscribe = () => {
190
+ var _subscription;
191
+
192
+ if (typeof ((_subscription = subscription) === null || _subscription === void 0 ? void 0 : _subscription.remove) === 'function') {
193
+ // React Native >=0.65.0
194
+ subscription.remove();
195
+ } else if (typeof Dimensions.removeEventListener === 'function') {
196
+ // React Native <0.65.0
197
+ Dimensions.removeEventListener('change', handleDimensionsChange);
198
+ }
199
+
200
+ setSourceLayout(null);
201
+ setTargetDimensions(null);
202
+ };
203
+
204
+ if (readyToShow) {
205
+ subscription = Dimensions.addEventListener('change', handleDimensionsChange);
206
+ handleDimensionsChange({
207
+ window: Dimensions.get('window')
208
+ });
209
+ } else {
210
+ unsubscribe();
211
+ }
212
+
213
+ return unsubscribe;
214
+ }, [readyToShow]);
215
+ const isReady = Boolean(isShown && sourceLayout && windowDimensions && targetDimensions);
216
+ const overlaidPosition = isReady ? getOverlaidPosition({
217
+ sourceLayout,
218
+ targetDimensions,
219
+ windowDimensions,
220
+ offsets,
221
+ align
222
+ }) : {};
223
+ return {
224
+ overlaidPosition,
225
+ sourceRef,
226
+ targetRef,
227
+ onTargetLayout,
228
+ isReady
229
+ };
230
+ };
231
+
232
+ export default useOverlaidPosition;
@@ -101,8 +101,6 @@ const resolveSpacingOptions = space => {
101
101
 
102
102
 
103
103
  const useSpacingScale = spaceValue => {
104
- var _spaceValue$space;
105
-
106
104
  // In future, may need to consider window height as well as width, particularly for native apps,
107
105
  // e.g. to ensure designs don't look lost on large, tall, not-so-wide portrait screens.
108
106
  const viewport = useViewport();
@@ -112,7 +110,7 @@ const useSpacingScale = spaceValue => {
112
110
  overridden,
113
111
  subtract = 0
114
112
  } = resolveSpacingOptions(spaceValue);
115
- const space = !overridden && ((_spaceValue$space = spaceValue === null || spaceValue === void 0 ? void 0 : spaceValue.space) !== null && _spaceValue$space !== void 0 ? _spaceValue$space : resolveResponsiveProp(spaceValue, viewport, 0));
113
+ const space = !overridden && ((spaceValue === null || spaceValue === void 0 ? void 0 : spaceValue.space) ?? resolveResponsiveProp(spaceValue, viewport, 0));
116
114
  const {
117
115
  size
118
116
  } = useThemeTokens('spacingScale', tokens, variant, {
@@ -5,7 +5,7 @@ function useUniqueId() {
5
5
  let prefix = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
6
6
  const [uniqueId] = useState(() => {
7
7
  id += 1;
8
- return "".concat(prefix, "-").concat(id);
8
+ return `${prefix}-${id}`;
9
9
  });
10
10
  return uniqueId;
11
11
  }
package/package.json CHANGED
@@ -8,14 +8,14 @@
8
8
  },
9
9
  "dependencies": {
10
10
  "@gorhom/portal": "^1.0.14",
11
- "@telus-uds/system-constants": "^1.0.4",
12
- "@telus-uds/system-theme-tokens": "^2.6.0",
11
+ "@telus-uds/system-constants": "^1.1.0",
12
+ "@telus-uds/system-theme-tokens": "^2.7.0",
13
13
  "airbnb-prop-types": "^2.16.0",
14
14
  "lodash.debounce": "^4.0.8",
15
15
  "lodash.merge": "^4.6.2",
16
16
  "prop-types": "^15.7.2",
17
17
  "react-native-picker-select": "^8.0.4",
18
- "semver": "^7.3.5"
18
+ "semver": "7.3.5"
19
19
  },
20
20
  "description": "Base components",
21
21
  "devDependencies": {
@@ -54,6 +54,9 @@
54
54
  },
55
55
  "scripts": {
56
56
  "test": "jest",
57
+ "test:web": "jest --config jest.config-web.js",
58
+ "test:ios": "jest --config jest.config-ios.js",
59
+ "test:android": "jest --config jest.config-android.js",
57
60
  "lint": "npm run --prefix ../.. lint:path -- --color packages/components-base",
58
61
  "lint:fix": "npm run --prefix ../.. lint:path -- --fix packages/components-base",
59
62
  "format": "prettier --write .",
@@ -67,5 +70,5 @@
67
70
  "standard-engine": {
68
71
  "skip": true
69
72
  },
70
- "version": "1.18.1"
73
+ "version": "1.19.1"
71
74
  }
@@ -123,7 +123,7 @@ const selectBorderStyles = ({ borderColor, borderWidth, borderRadius }) => ({
123
123
  })
124
124
 
125
125
  const selectTextStyles = (
126
- { fontSize, color, lineHeight, fontName, fontWeight, textAlign },
126
+ { fontSize, color, lineHeight, fontName, fontWeight, textAlign, textLine, textLineStyle },
127
127
  themeOptions
128
128
  ) =>
129
129
  applyTextStyles({
@@ -133,7 +133,9 @@ const selectTextStyles = (
133
133
  fontName,
134
134
  fontWeight,
135
135
  themeOptions,
136
- textAlign
136
+ textAlign,
137
+ textDecorationLine: textLine,
138
+ textDecorationStyle: textLineStyle
137
139
  })
138
140
 
139
141
  const selectWebOnlyStyles = (inactive, themeTokens, { accessibilityRole }) => {
@@ -164,7 +164,7 @@ const Carousel = React.forwardRef(
164
164
  ),
165
165
  tag = 'ul',
166
166
  accessibilityRole,
167
- accessibilityLabel = title,
167
+ accessibilityLabel,
168
168
  accessibilityLiveRegion = 'polite',
169
169
  copy,
170
170
  ...rest
@@ -422,6 +422,32 @@ const Carousel = React.forwardRef(
422
422
  const activePanelNavigation =
423
423
  tabs && showPanelTabs ? <CarouselTabsPanel items={tabs} /> : panelNavigation
424
424
 
425
+ const isFirstFocusContainer = Boolean(refocus && !skipLinkHref)
426
+ const containerRef = (element) => {
427
+ // Apply both firstFocusRef to the container
428
+ firstFocusRef.current = element
429
+ // Also apply forwarded ref if there is one (which could be a function ref)
430
+ if (ref) {
431
+ if (typeof ref === 'object') {
432
+ // eslint-disable-next-line no-param-reassign
433
+ ref.current = element
434
+ } else if (typeof ref === 'function') {
435
+ ref(element)
436
+ }
437
+ }
438
+ }
439
+ // If container isn't used for focus, give it a label of title if none is passed in,
440
+ // otherwise read the current position on focus
441
+ const containerAccessibilityLabel =
442
+ systemProps.accessibilityLabel ?? isFirstFocusContainer
443
+ ? `${title ? `${title} ` : ''}${getCopyWithPlaceholders('stepTrackerLabel')}`
444
+ : title
445
+ const containerProps = {
446
+ accessibilityLabel: containerAccessibilityLabel,
447
+ // If used for focus, attach the ref and draw a focus box around the whole carousel
448
+ ...(isFirstFocusContainer && { ref: containerRef, focusable: true })
449
+ }
450
+
425
451
  return (
426
452
  <CarouselProvider
427
453
  activeIndex={activeIndex}
@@ -434,7 +460,13 @@ const Carousel = React.forwardRef(
434
460
  refocus={refocus}
435
461
  width={containerLayout.width}
436
462
  >
437
- <View style={staticStyles.root} onLayout={onContainerLayout} ref={ref} {...systemProps}>
463
+ <View
464
+ style={staticStyles.root}
465
+ onLayout={onContainerLayout}
466
+ ref={ref}
467
+ {...systemProps}
468
+ {...containerProps}
469
+ >
438
470
  {showPreviousNextNavigation && (
439
471
  <View
440
472
  style={selectPreviousNextNavigationButtonStyles(
@@ -464,14 +496,14 @@ const Carousel = React.forwardRef(
464
496
  {getCopyWithPlaceholders('skipLink')}
465
497
  </SkipLink>
466
498
  )}
467
- <A11yText
468
- // Read the current slide position to screen readers on slide.
469
- // If it's set to refocus and doesn't have a SkipLink to focus to, focus this.
470
- ref={!skipLinkHref && refocus ? firstFocusRef : null}
471
- accessibilityLiveRegion={!skipLinkHref && refocus ? undefined : 'polite'}
472
- focusable={!skipLinkHref && refocus}
473
- text={getCopyWithPlaceholders('stepTrackerLabel')}
474
- />
499
+ {!isFirstFocusContainer && (
500
+ <A11yText
501
+ // Read the current slide position to screen readers on slide.
502
+ // If it's set to refocus and doesn't have a SkipLink to focus to, focus this.
503
+ accessibilityLiveRegion={!skipLinkHref && refocus ? undefined : 'polite'}
504
+ text={getCopyWithPlaceholders('stepTrackerLabel')}
505
+ />
506
+ )}
475
507
  <View style={selectContainerStyles(containerLayout.width)}>
476
508
  <Animated.View
477
509
  style={StyleSheet.flatten([
@@ -21,16 +21,6 @@ const CarouselTabsPanel = forwardRef(({ items }, ref) => {
21
21
 
22
22
  return (
23
23
  <>
24
- <View
25
- focusable
26
- accessible
27
- onFocus={(event) => {
28
- // When user forward-tabs into this section, focus the next tab; if they backwards-tab
29
- // (shift-tab) back into the carousel content, don't interfere.
30
- const previousWebFocus = event.relatedTarget
31
- if (previousWebFocus !== firstTabRef.current) nextFocusRef.current.focus()
32
- }}
33
- />
34
24
  <StackView direction="row" space={3} divider={{ variant: dividerVariant }} ref={ref}>
35
25
  {items.map(({ title, onPress, ...panelItemProps }, index) => {
36
26
  const selected = index === activeIndex
@@ -13,8 +13,9 @@ import dictionary from './dictionary'
13
13
  import useCopy from '../utils/useCopy'
14
14
 
15
15
  // We need to drop the icon here since it gets rendered via children and not
16
- // `ButtonBase` in order to tap into the state of the button
17
- const selectButtonTokens = ({ icon: _, ...rest }) => selectTokens('Button', rest)
16
+ // `ButtonBase` in order to tap into the state of the button; `displayLabel` flag
17
+ // is also not needed
18
+ const selectButtonTokens = ({ icon: _, displayLabel: __, ...rest }) => selectTokens('Button', rest)
18
19
  const selectIconTokens = ({ color, iconSize, iconDisplace }, direction) => {
19
20
  return {
20
21
  color,
@@ -36,13 +37,12 @@ const SideButton = forwardRef(
36
37
 
37
38
  const getCopy = useCopy({ dictionary, copy })
38
39
 
39
- const { icon } = getTokens(tokens, buttonVariant)
40
+ const { icon, displayLabel } = getTokens(tokens, buttonVariant)
40
41
 
41
42
  const getButtonTokens = (buttonState) => selectButtonTokens(getTokens(buttonState))
42
43
  const getIconTokens = (buttonState) => selectIconTokens(getTokens(buttonState), direction)
43
44
 
44
45
  const label = direction === 'previous' ? getCopy('previousText') : getCopy('nextText')
45
- const showLabel = viewport !== 'sm' && viewport !== 'xs'
46
46
 
47
47
  const accessibilityLabel =
48
48
  direction === 'previous' ? getCopy('previousLabel') : getCopy('nextLabel')
@@ -69,7 +69,7 @@ const SideButton = forwardRef(
69
69
  iconPosition={directionToSide[direction]}
70
70
  iconProps={iconProps}
71
71
  >
72
- {showLabel && <Text style={textStyles}>{label}</Text>}
72
+ {displayLabel && <Text style={textStyles}>{label}</Text>}
73
73
  </IconText>
74
74
  )
75
75
  }}