@telus-uds/components-base 1.16.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +32 -2
  2. package/component-docs.json +708 -120
  3. package/lib/BaseProvider/HydrationContext.js +74 -0
  4. package/lib/BaseProvider/index.js +14 -6
  5. package/lib/Button/ButtonBase.js +2 -1
  6. package/lib/List/List.js +11 -8
  7. package/lib/List/PressableListItemBase.js +5 -9
  8. package/lib/QuickLinks/QuickLinks.js +91 -0
  9. package/lib/QuickLinks/QuickLinksCard.js +47 -0
  10. package/lib/QuickLinks/QuickLinksItem.js +73 -0
  11. package/lib/QuickLinks/index.js +16 -0
  12. package/lib/StackView/StackWrap.js +16 -12
  13. package/lib/Timeline/Timeline.js +193 -0
  14. package/lib/Timeline/index.js +13 -0
  15. package/lib/ViewportProvider/useViewportListener.js +5 -18
  16. package/lib/index.js +28 -1
  17. package/lib/utils/animation/useVerticalExpandAnimation.js +3 -1
  18. package/lib/utils/index.js +9 -0
  19. package/lib/utils/useSafeLayoutEffect.js +40 -0
  20. package/lib-module/BaseProvider/HydrationContext.js +51 -0
  21. package/lib-module/BaseProvider/index.js +12 -6
  22. package/lib-module/Button/ButtonBase.js +2 -1
  23. package/lib-module/List/List.js +12 -8
  24. package/lib-module/List/PressableListItemBase.js +6 -10
  25. package/lib-module/QuickLinks/QuickLinks.js +71 -0
  26. package/lib-module/QuickLinks/QuickLinksCard.js +33 -0
  27. package/lib-module/QuickLinks/QuickLinksItem.js +50 -0
  28. package/lib-module/QuickLinks/index.js +4 -0
  29. package/lib-module/StackView/StackWrap.js +16 -13
  30. package/lib-module/Timeline/Timeline.js +174 -0
  31. package/lib-module/Timeline/index.js +2 -0
  32. package/lib-module/ViewportProvider/useViewportListener.js +5 -18
  33. package/lib-module/index.js +4 -1
  34. package/lib-module/utils/animation/useVerticalExpandAnimation.js +4 -3
  35. package/lib-module/utils/index.js +1 -0
  36. package/lib-module/utils/useSafeLayoutEffect.js +30 -0
  37. package/package.json +6 -5
  38. package/src/BaseProvider/HydrationContext.jsx +44 -0
  39. package/src/BaseProvider/index.jsx +11 -7
  40. package/src/Button/ButtonBase.jsx +2 -2
  41. package/src/List/List.jsx +9 -13
  42. package/src/List/PressableListItemBase.jsx +7 -9
  43. package/src/QuickLinks/QuickLinks.jsx +61 -0
  44. package/src/QuickLinks/QuickLinksCard.jsx +26 -0
  45. package/src/QuickLinks/QuickLinksItem.jsx +46 -0
  46. package/src/QuickLinks/index.js +6 -0
  47. package/src/StackView/StackWrap.jsx +20 -13
  48. package/src/Timeline/Timeline.jsx +148 -0
  49. package/src/Timeline/index.js +3 -0
  50. package/src/ViewportProvider/useViewportListener.js +4 -16
  51. package/src/index.js +3 -0
  52. package/src/utils/animation/useVerticalExpandAnimation.js +4 -2
  53. package/src/utils/index.js +1 -0
  54. package/src/utils/useSafeLayoutEffect.js +31 -0
@@ -0,0 +1,71 @@
1
+ import React, { forwardRef } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { useThemeTokens } from '../ThemeProvider';
4
+ import { useViewport } from '../ViewportProvider';
5
+ import { getTokensPropType, variantProp } from '../utils';
6
+ import List from '../List';
7
+ import StackWrap from '../StackView/StackWrap';
8
+ import QuickLinksCard from './QuickLinksCard';
9
+ /**
10
+ * QuickLinks renders a list of interactive items. How it renders these items depends on theme options:
11
+ * - If the theme returns `list` token as true, it renders an ordered list based on List
12
+ * - If the theme returns `button` token as true and `list` as false, it renders a wrapping horizontal bar of buttons
13
+ * - If the theme returns `card` token as true, it wraps the above with a `Card`.
14
+ */
15
+
16
+ import { jsx as _jsx } from "react/jsx-runtime";
17
+ const QuickLinks = /*#__PURE__*/forwardRef((_ref, ref) => {
18
+ let {
19
+ tokens,
20
+ variant,
21
+ listTokens,
22
+ cardTokens,
23
+ children,
24
+ tag = 'ul',
25
+ ...rest
26
+ } = _ref;
27
+ const viewport = useViewport();
28
+ const {
29
+ dividers,
30
+ list,
31
+ card,
32
+ stackSpace,
33
+ stackGap,
34
+ stackJustify
35
+ } = useThemeTokens('QuickLinks', tokens, variant, {
36
+ viewport
37
+ });
38
+
39
+ const content = list && /*#__PURE__*/_jsx(List, {
40
+ ref: ref,
41
+ tokens: listTokens,
42
+ showDivider: dividers,
43
+ tag: tag,
44
+ ...rest,
45
+ children: children
46
+ }) || /*#__PURE__*/_jsx(StackWrap, {
47
+ space: stackSpace,
48
+ gap: stackGap,
49
+ tokens: {
50
+ justifyContent: stackJustify
51
+ },
52
+ tag: tag,
53
+ ...rest,
54
+ children: children
55
+ });
56
+
57
+ return card ? /*#__PURE__*/_jsx(QuickLinksCard, {
58
+ tokens: cardTokens,
59
+ children: content
60
+ }) : content;
61
+ });
62
+ QuickLinks.displayName = 'QuickLinks';
63
+ QuickLinks.propTypes = {
64
+ tokens: getTokensPropType('QuickLinks'),
65
+ cardTokens: getTokensPropType('Card'),
66
+ listTokens: getTokensPropType('QuickLinksList'),
67
+ tag: PropTypes.string,
68
+ variant: variantProp.propType,
69
+ children: PropTypes.node
70
+ };
71
+ export default QuickLinks;
@@ -0,0 +1,33 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { useThemeTokens } from '../ThemeProvider';
4
+ import { getTokensPropType, variantProp } from '../utils';
5
+ import CardBase from '../Card/CardBase';
6
+ /**
7
+ * Private subcomponent for use within QuickLinks.
8
+ *
9
+ * Restyled Card with identical behaviour to Card, but themed according to the
10
+ * QuickLinksCard theme rather than the Card theme.
11
+ */
12
+
13
+ import { jsx as _jsx } from "react/jsx-runtime";
14
+
15
+ const QuickLinksList = _ref => {
16
+ let {
17
+ tokens,
18
+ variant,
19
+ children
20
+ } = _ref;
21
+ const themeTokens = useThemeTokens('QuickLinksCard', tokens, variant);
22
+ return /*#__PURE__*/_jsx(CardBase, {
23
+ tokens: themeTokens,
24
+ children: children
25
+ });
26
+ };
27
+
28
+ QuickLinksList.propTypes = {
29
+ tokens: getTokensPropType('QuickLinksCard'),
30
+ variant: variantProp.propType,
31
+ children: PropTypes.node
32
+ };
33
+ export default QuickLinksList;
@@ -0,0 +1,50 @@
1
+ import React, { forwardRef } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { getTokensPropType, variantProp, withLinkRouter } from '../utils';
4
+ import { useViewport } from '../ViewportProvider';
5
+ import { useThemeTokens, useThemeTokensCallback } from '../ThemeProvider';
6
+ import PressableListItemBase from '../List/PressableListItemBase';
7
+ import ButtonBase from '../Button/ButtonBase';
8
+ /**
9
+ * Public component exported as QuickLinks.Item, for use as children of QuickLinks.
10
+ *
11
+ * Receives props injected by QuickLinks and renders the appropriate child component.
12
+ */
13
+
14
+ import { jsx as _jsx } from "react/jsx-runtime";
15
+ const QuickLinksItem = /*#__PURE__*/forwardRef((_ref, ref) => {
16
+ let {
17
+ tokens,
18
+ variant,
19
+ children,
20
+ ...rest
21
+ } = _ref;
22
+ const viewport = useViewport();
23
+ const {
24
+ list
25
+ } = useThemeTokens('QuickLinks', tokens, variant, {
26
+ viewport
27
+ });
28
+ const themeName = list ? 'QuickLinksList' : 'QuickLinksButton';
29
+ const getTokens = useThemeTokensCallback(themeName, tokens, variant);
30
+ return list ? /*#__PURE__*/_jsx(PressableListItemBase, {
31
+ ref: ref,
32
+ tokens: getTokens,
33
+ ...rest,
34
+ children: children
35
+ }) : /*#__PURE__*/_jsx(ButtonBase, {
36
+ ref: ref,
37
+ tokens: getTokens,
38
+ ...rest,
39
+ children: children
40
+ });
41
+ });
42
+ QuickLinksItem.displayName = 'QuickLinksItem';
43
+ QuickLinksItem.propTypes = { ...withLinkRouter.propTypes,
44
+ ...PressableListItemBase.propTypes,
45
+ ...ButtonBase.propTypes,
46
+ tokens: getTokensPropType('QuickLinksList', 'QuickLinksButton'),
47
+ variant: variantProp.propType,
48
+ children: PropTypes.node
49
+ };
50
+ export default withLinkRouter(QuickLinksItem);
@@ -0,0 +1,4 @@
1
+ import QuickLinks from './QuickLinks';
2
+ import QuickLinksItem from './QuickLinksItem';
3
+ QuickLinks.Item = QuickLinksItem;
4
+ export default QuickLinks;
@@ -1,14 +1,15 @@
1
- import React, { forwardRef } from 'react';
1
+ import React, { forwardRef, useState } from 'react';
2
2
  import Platform from "react-native-web/dist/exports/Platform";
3
+ import useSafeLayoutEffect from '../utils/useSafeLayoutEffect';
3
4
  import StackWrapBox from './StackWrapBox';
4
5
  import StackWrapGap from './StackWrapGap'; // In Jest/CI/SSR, global CSS isn't always available and doesn't always have .supports method
5
6
 
6
7
  import { jsx as _jsx } from "react/jsx-runtime";
7
8
 
8
- const cssSupports = function () {
9
+ const cssSupports = (property, value) => {
9
10
  var _window$CSS;
10
11
 
11
- return typeof window !== 'undefined' && typeof ((_window$CSS = window.CSS) === null || _window$CSS === void 0 ? void 0 : _window$CSS.supports) === 'function' && window.CSS.supports(...arguments);
12
+ return Platform.OS === 'web' && typeof window !== 'undefined' && typeof ((_window$CSS = window.CSS) === null || _window$CSS === void 0 ? void 0 : _window$CSS.supports) === 'function' && window.CSS.supports(property, value);
12
13
  }; // CSS.supports needs an example of the type of value you intend to use.
13
14
  // Will be an integer appended `px` after hooks and JSX styles are resolved.
14
15
 
@@ -25,22 +26,24 @@ const exampleGapValue = '1px';
25
26
  const StackWrap = /*#__PURE__*/forwardRef((props, ref) => {
26
27
  var _props$gap;
27
28
 
29
+ const [canUseCSSGap, setCanUseCSSGap] = useState(false);
28
30
  const {
29
31
  space
30
32
  } = props; // Don't apply separate gap if `null` or `undefined`, so can be unset in Storybook etc
31
33
 
32
34
  const gap = (_props$gap = props.gap) !== null && _props$gap !== void 0 ? _props$gap : space;
33
- const canUseCSSGap = Platform.OS === 'web' && gap === space && cssSupports('gap', exampleGapValue);
34
- return canUseCSSGap ?
35
- /*#__PURE__*/
36
- // If possible, use the cleaner implementation that applies CSS `gap` styles to the container.
37
- _jsx(StackWrapGap, {
38
- ref: ref,
39
- ...props
40
- }) :
41
- /*#__PURE__*/
35
+ const gapEqualsSpace = gap === space; // If possible, use the cleaner implementation that applies CSS `gap` styles to the container,
36
+ // preserving direct parent-child relationships between the container and each item, which
37
+ // can result in clearer descriptions on some screen readers (e.g. radio "X of Y" on MacOS).
42
38
  // Else, use the fallback implementation which renders a `Box` component around each child.
43
- _jsx(StackWrapBox, {
39
+
40
+ const Component = canUseCSSGap ? StackWrapGap : StackWrapBox; // In SSR, the type of implementation must match the server during hydration, but
41
+ // the server can't know if gap is supported, so never use it until after hydration.
42
+
43
+ useSafeLayoutEffect(() => {
44
+ setCanUseCSSGap(gapEqualsSpace && cssSupports('gap', exampleGapValue));
45
+ }, [gapEqualsSpace]);
46
+ return /*#__PURE__*/_jsx(Component, {
44
47
  ref: ref,
45
48
  ...props
46
49
  });
@@ -0,0 +1,174 @@
1
+ import PropTypes from 'prop-types';
2
+ import React, { forwardRef } from 'react';
3
+ import View from "react-native-web/dist/exports/View";
4
+ import { useThemeTokens } from '../ThemeProvider';
5
+ import { getTokensPropType, variantProp, a11yProps, viewProps, selectSystemProps, getA11yPropsFromHtmlTag, layoutTags } from '../utils';
6
+ import { useViewport } from '../ViewportProvider';
7
+ import { jsx as _jsx } from "react/jsx-runtime";
8
+ import { jsxs as _jsxs } from "react/jsx-runtime";
9
+
10
+ const selectDotStyles = _ref => {
11
+ let {
12
+ dotWidth,
13
+ timelineColor,
14
+ dotBorderWidth,
15
+ dotColor
16
+ } = _ref;
17
+ return {
18
+ width: dotWidth,
19
+ height: dotWidth,
20
+ borderRadius: dotWidth / 2,
21
+ backgroundColor: dotColor,
22
+ borderWidth: dotBorderWidth,
23
+ borderColor: timelineColor
24
+ };
25
+ };
26
+
27
+ const selectConnectorStyles = _ref2 => {
28
+ let {
29
+ timelineColor,
30
+ connectorHeight,
31
+ connectorWidth
32
+ } = _ref2;
33
+ return {
34
+ width: connectorWidth,
35
+ height: connectorHeight,
36
+ backgroundColor: timelineColor
37
+ };
38
+ };
39
+
40
+ const selectTimelineContainerStyle = _ref3 => {
41
+ let {
42
+ timelineContainerDirection
43
+ } = _ref3;
44
+ return {
45
+ flexDirection: timelineContainerDirection
46
+ };
47
+ };
48
+
49
+ const selectLineItemStyles = _ref4 => {
50
+ let {
51
+ lineItemAlign,
52
+ lineItemDirection,
53
+ lineItemMarginBottom,
54
+ lineItemMarginRight
55
+ } = _ref4;
56
+ return {
57
+ alignItems: lineItemAlign,
58
+ flexDirection: lineItemDirection,
59
+ marginBottom: lineItemMarginBottom,
60
+ marginRight: lineItemMarginRight,
61
+ overflow: 'hidden'
62
+ };
63
+ };
64
+
65
+ const selectLineItemContainer = _ref5 => {
66
+ let {
67
+ lineItemContainerDirection,
68
+ lineContainerFlexSize
69
+ } = _ref5;
70
+ return {
71
+ flexDirection: lineItemContainerDirection,
72
+ flex: lineContainerFlexSize
73
+ };
74
+ };
75
+
76
+ const selectItemContentStyles = (_ref6, isLastChild) => {
77
+ let {
78
+ itemContentFlexSize,
79
+ itemContentMarginBottom,
80
+ itemContentMarginRight
81
+ } = _ref6;
82
+ return {
83
+ flex: itemContentFlexSize,
84
+ marginBottom: !isLastChild && itemContentMarginBottom,
85
+ marginRight: !isLastChild && itemContentMarginRight
86
+ };
87
+ };
88
+
89
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps]);
90
+ /**
91
+ * Timeline is a component that displays either a horizontal or vertical list of the
92
+ * children components passed by props
93
+ *
94
+ * ## Component API
95
+ *
96
+ * - `horizontal` In order to display the Component list horizontally
97
+ *
98
+ *
99
+ * ## A11y guidelines
100
+ * Timeline link supports all the common a11y props.
101
+ */
102
+
103
+ const Timeline = /*#__PURE__*/forwardRef((_ref7, ref) => {
104
+ let {
105
+ tokens,
106
+ variant = {},
107
+ children,
108
+ accessibilityLabel,
109
+ tag = 'ul',
110
+ childrenTag = 'li',
111
+ ...rest
112
+ } = _ref7;
113
+ const viewport = useViewport();
114
+ const themeTokens = useThemeTokens('Timeline', tokens, variant, {
115
+ viewport
116
+ });
117
+ const containerProps = { ...selectProps(rest),
118
+ ...getA11yPropsFromHtmlTag(tag, rest.accessibilityRole || 'list'),
119
+ accessibilityLabel
120
+ };
121
+ return /*#__PURE__*/_jsx(View, { ...containerProps,
122
+ ref: ref,
123
+ style: selectTimelineContainerStyle(themeTokens),
124
+ children: children.map((child, index) => {
125
+ var _child$props;
126
+
127
+ const childrenProps = { ...getA11yPropsFromHtmlTag(childrenTag, (child === null || child === void 0 ? void 0 : (_child$props = child.props) === null || _child$props === void 0 ? void 0 : _child$props.accessibilityRole) || 'listitem')
128
+ };
129
+ return /*#__PURE__*/_jsxs(View, {
130
+ style: selectLineItemContainer(themeTokens) // eslint-disable-next-line react/no-array-index-key
131
+ ,
132
+ ...childrenProps,
133
+ children: [/*#__PURE__*/_jsxs(View, {
134
+ style: selectLineItemStyles(themeTokens),
135
+ children: [/*#__PURE__*/_jsx(View, {
136
+ style: selectDotStyles(themeTokens)
137
+ }), /*#__PURE__*/_jsx(View, {
138
+ style: selectConnectorStyles(themeTokens)
139
+ })]
140
+ }), /*#__PURE__*/_jsx(View, {
141
+ style: selectItemContentStyles(themeTokens, index + 1 === children.length),
142
+ children: child
143
+ })]
144
+ }, "timeline-".concat(index, "-").concat(child.displayName));
145
+ })
146
+ });
147
+ });
148
+ Timeline.displayName = 'Timeline';
149
+ Timeline.propTypes = { ...selectedSystemPropTypes,
150
+ tokens: getTokensPropType('Timeline'),
151
+ variant: variantProp.propType,
152
+
153
+ /**
154
+ * A list of components that will be rendered either horizontally or vertically
155
+ */
156
+ children: PropTypes.arrayOf(PropTypes.node).isRequired,
157
+
158
+ /**
159
+ * A required accessibility label that needs to be passed to be used on List
160
+ * which is applied as normal for a React Native accessibilityLabel prop.
161
+ */
162
+ accessibilityLabel: PropTypes.string.isRequired,
163
+
164
+ /**
165
+ * Sets the HTML tag of the outer container and the children. By default `'li'` for the children
166
+ * and `'ul'` for the container
167
+ *
168
+ * If either `tag` or `childrenTag` is overridden, the other should be too, to avoid producing invalid HTML.
169
+ *
170
+ */
171
+ tag: PropTypes.oneOf(layoutTags),
172
+ childrenTag: PropTypes.oneOf(layoutTags)
173
+ };
174
+ export default Timeline;
@@ -0,0 +1,2 @@
1
+ import Timeline from './Timeline';
2
+ export default Timeline;
@@ -1,20 +1,10 @@
1
- import { useLayoutEffect } from 'react';
2
1
  import Dimensions from "react-native-web/dist/exports/Dimensions";
3
- import { viewports } from '@telus-uds/system-constants'; // Use Dimensions instead of useWindowDimensions because useWindowDimensions forces context
2
+ import { viewports } from '@telus-uds/system-constants';
3
+ import useSafeLayoutEffect from '../utils/useSafeLayoutEffect'; // Use Dimensions instead of useWindowDimensions because useWindowDimensions forces context
4
4
  // to update on every pixel change during window resize; but we only want rerenders to occur
5
5
  // when a viewport threshold has been crossed.
6
6
 
7
7
  const lookupViewport = () => viewports.select(Dimensions.get('window').width);
8
- /**
9
- * In SSR, React gets spooked if it sees `useLayoutEffect` and fires warnings assuming the
10
- * developer doesn't realise the effect won't run: https://reactjs.org/link/uselayouteffect-ssr
11
- *
12
- * To avoid these warnings while still conforming to the rules of hooks, always use this
13
- * explicitly no-op hook, instead of the useLayoutEffect that is implicitly no-op on SSR.
14
- */
15
-
16
-
17
- const useViewportListenerSSR = () => {};
18
8
  /**
19
9
  * When client-side rendering, immediately set the viewport to the correct value as a layout effect so
20
10
  * if the viewport isn't the smallest, any SSR-rendered components rerender correctly before anything
@@ -22,8 +12,8 @@ const useViewportListenerSSR = () => {};
22
12
  */
23
13
 
24
14
 
25
- const useViewportListenerCSR = setViewport => {
26
- useLayoutEffect(() => {
15
+ const useViewportListener = setViewport => {
16
+ useSafeLayoutEffect(() => {
27
17
  setViewport(lookupViewport());
28
18
 
29
19
  const onChange = _ref => {
@@ -44,9 +34,6 @@ const useViewportListenerCSR = setViewport => {
44
34
  }
45
35
  };
46
36
  }, [setViewport]);
47
- }; // Window is a defined global object in both Web and Native client-side, and undefined in SSR
48
-
37
+ };
49
38
 
50
- const isSSR = typeof window === 'undefined';
51
- const useViewportListener = isSSR ? useViewportListenerSSR : useViewportListenerCSR;
52
39
  export default useViewportListener;
@@ -24,6 +24,7 @@ export { default as Modal } from './Modal';
24
24
  export { default as Notification } from './Notification';
25
25
  export { default as Pagination } from './Pagination';
26
26
  export { default as Progress } from './Progress';
27
+ export { default as QuickLinks } from './QuickLinks';
27
28
  export { default as Radio } from './Radio';
28
29
  export * from './Radio';
29
30
  export { default as RadioCard } from './RadioCard';
@@ -40,6 +41,7 @@ export { default as StepTracker } from './StepTracker';
40
41
  export { default as Tabs } from './Tabs';
41
42
  export { default as Tags } from './Tags';
42
43
  export * from './TextInput';
44
+ export { default as Timeline } from './Timeline';
43
45
  export * from './ToggleSwitch';
44
46
  export { default as Tooltip } from './Tooltip';
45
47
  export { default as TooltipButton } from './TooltipButton';
@@ -48,4 +50,5 @@ export { default as A11yInfoProvider, useA11yInfo } from './A11yInfoProvider';
48
50
  export { default as BaseProvider } from './BaseProvider';
49
51
  export { default as ViewportProvider, useViewport, ViewportContext } from './ViewportProvider';
50
52
  export { default as ThemeProvider, useTheme, useSetTheme, useThemeTokens, getThemeTokens, applyOuterBorder, applyTextStyles, applyShadowToken } from './ThemeProvider';
51
- export * from './utils';
53
+ export * from './utils';
54
+ export { Portal } from '@gorhom/portal';
@@ -1,7 +1,8 @@
1
- import { useEffect, useLayoutEffect, useRef, useState } from 'react';
1
+ import { useEffect, useRef, useState } from 'react';
2
2
  import Animated from "react-native-web/dist/exports/Animated";
3
3
  import Easing from "react-native-web/dist/exports/Easing";
4
- import Platform from "react-native-web/dist/exports/Platform"; // TODO: systematise animations
4
+ import Platform from "react-native-web/dist/exports/Platform";
5
+ import useSafeLayoutEffect from '../useSafeLayoutEffect'; // TODO: systematise animations
5
6
  // https://github.com/telus/universal-design-system/issues/487
6
7
 
7
8
  function useVerticalExpandAnimation(_ref) {
@@ -19,7 +20,7 @@ function useVerticalExpandAnimation(_ref) {
19
20
  expandDuration,
20
21
  collapseDuration
21
22
  } = tokens;
22
- useLayoutEffect(() => {
23
+ useSafeLayoutEffect(() => {
23
24
  if (expandStateChanged) {
24
25
  setIsAnimating(true);
25
26
  setWasExpanded(isExpanded);
@@ -9,6 +9,7 @@ export { default as useCopy } from './useCopy';
9
9
  export { default as useHash } from './useHash';
10
10
  export { default as useSpacingScale } from './useSpacingScale';
11
11
  export { default as useResponsiveProp } from './useResponsiveProp';
12
+ export { default as useSafeLayoutEffect } from './useSafeLayoutEffect';
12
13
  export { default as useScrollBlocking } from './useScrollBlocking';
13
14
  export * from './useResponsiveProp';
14
15
  export { default as useUniqueId } from './useUniqueId';
@@ -0,0 +1,30 @@
1
+ import { useLayoutEffect, useCallback } from 'react';
2
+ import { useHydrationContext } from '../BaseProvider/HydrationContext';
3
+ const isSSR = typeof window === 'undefined';
4
+
5
+ const noop = () => {};
6
+ /**
7
+ * useSafeLayoutEffect is a alternative to useLayoutEffect that avoids SSR hydration problems:
8
+ * - In a client-side render, it uses useLayoutEffect to avoid flashing the pre-render UI to the user.
9
+ * - During hydration from SSR, the provided function is skipped to avoid mismatches from server content.
10
+ * - In SSR, it is a no-op function to avoid warnings about using useLayoutEffect in SSR
11
+ */
12
+
13
+
14
+ const useSafeLayoutEffect = isSSR ? noop // avoid React's fussy warnings by ensuring to never call useLayoutEffect on server
15
+ : function (fn) {
16
+ let deps = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
17
+ const isHydrating = useHydrationContext(); // Callback updates and effect re-runs when deps array content changes, like useEffect.
18
+
19
+ /* eslint-disable-next-line react-hooks/exhaustive-deps */
20
+
21
+ const callback = useCallback(fn, deps);
22
+ useLayoutEffect(() => {
23
+ // Do nothing before hydrating server-generated content, like useEffect. When hydration completes,
24
+ // useHydrationContext provides false, re-rendering this hook and re-running the effect.
25
+ if (isHydrating) return noop; // If there's no hydration in progress, behave like useLayoutEffect.
26
+
27
+ return callback();
28
+ }, [isHydrating, callback]);
29
+ };
30
+ export default useSafeLayoutEffect;
package/package.json CHANGED
@@ -7,9 +7,10 @@
7
7
  "url": "https://github.com/telus/universal-design-system/issues"
8
8
  },
9
9
  "dependencies": {
10
- "airbnb-prop-types": "^2.16.0",
10
+ "@gorhom/portal": "^1.0.14",
11
11
  "@telus-uds/system-constants": "^1.0.4",
12
- "@telus-uds/system-theme-tokens": "^2.5.0",
12
+ "@telus-uds/system-theme-tokens": "^2.6.0",
13
+ "airbnb-prop-types": "^2.16.0",
13
14
  "lodash.debounce": "^4.0.8",
14
15
  "lodash.merge": "^4.6.2",
15
16
  "prop-types": "^15.7.2",
@@ -21,10 +22,10 @@
21
22
  "@telus-uds/browserslist-config": "^1.0.4",
22
23
  "@testing-library/jest-native": "^4.0.1",
23
24
  "@testing-library/react": "^13.3.0",
24
- "@testing-library/react-native": "11.0.0",
25
- "react-test-renderer": "~18.0.0",
26
25
  "@testing-library/react-hooks": "~7.0.1",
26
+ "@testing-library/react-native": "11.0.0",
27
27
  "@testing-library/react-native-17": "npm:@testing-library/react-native@7.2.0",
28
+ "react-test-renderer": "~18.0.0",
28
29
  "react-test-renderer-17": "npm:react-test-renderer@17.0.2",
29
30
  "webpack": "5.x"
30
31
  },
@@ -66,5 +67,5 @@
66
67
  "standard-engine": {
67
68
  "skip": true
68
69
  },
69
- "version": "1.16.0"
70
+ "version": "1.18.0"
70
71
  }
@@ -0,0 +1,44 @@
1
+ import React, { createContext, useContext, useEffect, useState } from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { Platform } from 'react-native'
4
+
5
+ const HydrationContext = createContext()
6
+ const isSSR = typeof window === 'undefined'
7
+ const hasWebStyleTag = () => {
8
+ if (isSSR || Platform.OS !== 'web' || typeof document !== 'object') {
9
+ return false
10
+ }
11
+ return Boolean(document?.getElementById('react-native-stylesheet'))
12
+ }
13
+
14
+ /**
15
+ * Returns true if this render cycle is the hydration of existing SSR content.
16
+ *
17
+ * Use this when changing how content renders based on data that is instantly available
18
+ * during the very first client-side render or hydration, but not available on the server,
19
+ * to ensure no changes happen until the original SSR DOM has been hydrated.
20
+ */
21
+ export const useHydrationContext = () => useContext(HydrationContext)
22
+
23
+ /**
24
+ * Allows components and hooks to observe if SSR hydration is currently in progress
25
+ * and if so, to re-render with content that differs to the server when it is complete.
26
+ */
27
+ export const HydrationProvider = ({ children }) => {
28
+ const [hasMounted, setHasMounted] = useState(false)
29
+ useEffect(() => {
30
+ setHasMounted(true)
31
+ }, [])
32
+
33
+ // If we've got a HydrationProvider inside a HydrationProvider somehow, defer to the top one
34
+ const valueFromAncestor = useHydrationContext()
35
+
36
+ const isHydrating = valueFromAncestor ?? Boolean(!hasMounted && hasWebStyleTag())
37
+ return <HydrationContext.Provider value={isHydrating}>{children}</HydrationContext.Provider>
38
+ }
39
+
40
+ HydrationProvider.propTypes = {
41
+ children: PropTypes.node
42
+ }
43
+
44
+ export default HydrationProvider
@@ -1,17 +1,21 @@
1
1
  import React from 'react'
2
2
  import PropTypes from 'prop-types'
3
+ import { PortalProvider } from '@gorhom/portal'
3
4
  import A11yInfoProvider from '../A11yInfoProvider'
4
5
  import ViewportProvider from '../ViewportProvider'
5
6
  import ThemeProvider from '../ThemeProvider'
7
+ import { HydrationProvider } from './HydrationContext'
6
8
 
7
9
  const BaseProvider = ({ defaultTheme, children, themeOptions }) => (
8
- <A11yInfoProvider>
9
- <ViewportProvider>
10
- <ThemeProvider defaultTheme={defaultTheme} themeOptions={themeOptions}>
11
- {children}
12
- </ThemeProvider>
13
- </ViewportProvider>
14
- </A11yInfoProvider>
10
+ <HydrationProvider>
11
+ <A11yInfoProvider>
12
+ <ViewportProvider>
13
+ <ThemeProvider defaultTheme={defaultTheme} themeOptions={themeOptions}>
14
+ <PortalProvider>{children}</PortalProvider>
15
+ </ThemeProvider>
16
+ </ViewportProvider>
17
+ </A11yInfoProvider>
18
+ </HydrationProvider>
15
19
  )
16
20
 
17
21
  BaseProvider.propTypes = {
@@ -148,9 +148,9 @@ const selectWebOnlyStyles = (inactive, themeTokens, { accessibilityRole }) => {
148
148
  })
149
149
  }
150
150
 
151
- const selectItemIconTokens = ({ color, iconSize }) => ({
151
+ const selectItemIconTokens = ({ color, iconColor, iconSize }) => ({
152
152
  size: iconSize,
153
- color
153
+ color: iconColor || color
154
154
  })
155
155
 
156
156
  const ButtonBase = forwardRef(