@telus-uds/components-base 1.17.0 → 1.18.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.
- package/CHANGELOG.md +22 -2
- package/lib/BaseProvider/HydrationContext.js +74 -0
- package/lib/BaseProvider/index.js +11 -7
- package/lib/Search/Search.js +10 -2
- package/lib/StackView/StackWrap.js +16 -12
- package/lib/ViewportProvider/useViewportListener.js +5 -18
- package/lib/utils/animation/useVerticalExpandAnimation.js +3 -1
- package/lib/utils/index.js +9 -0
- package/lib/utils/useSafeLayoutEffect.js +40 -0
- package/lib-module/BaseProvider/HydrationContext.js +51 -0
- package/lib-module/BaseProvider/index.js +10 -7
- package/lib-module/Search/Search.js +10 -2
- package/lib-module/StackView/StackWrap.js +16 -13
- package/lib-module/ViewportProvider/useViewportListener.js +5 -18
- package/lib-module/utils/animation/useVerticalExpandAnimation.js +4 -3
- package/lib-module/utils/index.js +1 -0
- package/lib-module/utils/useSafeLayoutEffect.js +30 -0
- package/package.json +1 -1
- package/src/BaseProvider/HydrationContext.jsx +44 -0
- package/src/BaseProvider/index.jsx +10 -7
- package/src/Search/Search.jsx +4 -1
- package/src/StackView/StackWrap.jsx +20 -13
- package/src/ViewportProvider/useViewportListener.js +4 -16
- package/src/utils/animation/useVerticalExpandAnimation.js +4 -2
- package/src/utils/index.js +1 -0
- package/src/utils/useSafeLayoutEffect.js +31 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
# Change Log - @telus-uds/components-base
|
|
2
2
|
|
|
3
|
-
This log was last generated on
|
|
3
|
+
This log was last generated on Wed, 05 Oct 2022 21:29:58 GMT and should not be manually modified.
|
|
4
4
|
|
|
5
5
|
<!-- Start content -->
|
|
6
6
|
|
|
7
|
+
## 1.18.1
|
|
8
|
+
|
|
9
|
+
Wed, 05 Oct 2022 21:29:58 GMT
|
|
10
|
+
|
|
11
|
+
### Patches
|
|
12
|
+
|
|
13
|
+
- feat: add passing native / test ID from the search to the input (ruslan.bredikhin@nearform.com)
|
|
14
|
+
|
|
15
|
+
## 1.18.0
|
|
16
|
+
|
|
17
|
+
Tue, 27 Sep 2022 19:33:40 GMT
|
|
18
|
+
|
|
19
|
+
### Minor changes
|
|
20
|
+
|
|
21
|
+
- Add exported useSafeLayoutEffect utility (alan.slater@nearform.com)
|
|
22
|
+
|
|
23
|
+
### Patches
|
|
24
|
+
|
|
25
|
+
- Fix SSR hydration mismatches (alan.slater@nearform.com)
|
|
26
|
+
|
|
7
27
|
## 1.17.0
|
|
8
28
|
|
|
9
|
-
Mon, 19 Sep 2022 22:
|
|
29
|
+
Mon, 19 Sep 2022 22:51:24 GMT
|
|
10
30
|
|
|
11
31
|
### Minor changes
|
|
12
32
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.useHydrationContext = exports.default = exports.HydrationProvider = void 0;
|
|
7
|
+
|
|
8
|
+
var _react = _interopRequireWildcard(require("react"));
|
|
9
|
+
|
|
10
|
+
var _propTypes = _interopRequireDefault(require("prop-types"));
|
|
11
|
+
|
|
12
|
+
var _Platform = _interopRequireDefault(require("react-native-web/dist/cjs/exports/Platform"));
|
|
13
|
+
|
|
14
|
+
var _jsxRuntime = require("react/jsx-runtime");
|
|
15
|
+
|
|
16
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
17
|
+
|
|
18
|
+
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
|
|
19
|
+
|
|
20
|
+
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
|
|
21
|
+
|
|
22
|
+
const HydrationContext = /*#__PURE__*/(0, _react.createContext)();
|
|
23
|
+
const isSSR = typeof window === 'undefined';
|
|
24
|
+
|
|
25
|
+
const hasWebStyleTag = () => {
|
|
26
|
+
var _document;
|
|
27
|
+
|
|
28
|
+
if (isSSR || _Platform.default.OS !== 'web' || typeof document !== 'object') {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return Boolean((_document = document) === null || _document === void 0 ? void 0 : _document.getElementById('react-native-stylesheet'));
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Returns true if this render cycle is the hydration of existing SSR content.
|
|
36
|
+
*
|
|
37
|
+
* Use this when changing how content renders based on data that is instantly available
|
|
38
|
+
* during the very first client-side render or hydration, but not available on the server,
|
|
39
|
+
* to ensure no changes happen until the original SSR DOM has been hydrated.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
const useHydrationContext = () => (0, _react.useContext)(HydrationContext);
|
|
44
|
+
/**
|
|
45
|
+
* Allows components and hooks to observe if SSR hydration is currently in progress
|
|
46
|
+
* and if so, to re-render with content that differs to the server when it is complete.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
exports.useHydrationContext = useHydrationContext;
|
|
51
|
+
|
|
52
|
+
const HydrationProvider = _ref => {
|
|
53
|
+
let {
|
|
54
|
+
children
|
|
55
|
+
} = _ref;
|
|
56
|
+
const [hasMounted, setHasMounted] = (0, _react.useState)(false);
|
|
57
|
+
(0, _react.useEffect)(() => {
|
|
58
|
+
setHasMounted(true);
|
|
59
|
+
}, []); // If we've got a HydrationProvider inside a HydrationProvider somehow, defer to the top one
|
|
60
|
+
|
|
61
|
+
const valueFromAncestor = useHydrationContext();
|
|
62
|
+
const isHydrating = valueFromAncestor !== null && valueFromAncestor !== void 0 ? valueFromAncestor : Boolean(!hasMounted && hasWebStyleTag());
|
|
63
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(HydrationContext.Provider, {
|
|
64
|
+
value: isHydrating,
|
|
65
|
+
children: children
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
exports.HydrationProvider = HydrationProvider;
|
|
70
|
+
HydrationProvider.propTypes = {
|
|
71
|
+
children: _propTypes.default.node
|
|
72
|
+
};
|
|
73
|
+
var _default = HydrationProvider;
|
|
74
|
+
exports.default = _default;
|
|
@@ -17,6 +17,8 @@ var _ViewportProvider = _interopRequireDefault(require("../ViewportProvider"));
|
|
|
17
17
|
|
|
18
18
|
var _ThemeProvider = _interopRequireDefault(require("../ThemeProvider"));
|
|
19
19
|
|
|
20
|
+
var _HydrationContext = require("./HydrationContext");
|
|
21
|
+
|
|
20
22
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
21
23
|
|
|
22
24
|
var _ThemeProvider$propTy;
|
|
@@ -29,13 +31,15 @@ const BaseProvider = _ref => {
|
|
|
29
31
|
children,
|
|
30
32
|
themeOptions
|
|
31
33
|
} = _ref;
|
|
32
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsx)(
|
|
33
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(
|
|
34
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
children:
|
|
34
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_HydrationContext.HydrationProvider, {
|
|
35
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_A11yInfoProvider.default, {
|
|
36
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_ViewportProvider.default, {
|
|
37
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_ThemeProvider.default, {
|
|
38
|
+
defaultTheme: defaultTheme,
|
|
39
|
+
themeOptions: themeOptions,
|
|
40
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_portal.PortalProvider, {
|
|
41
|
+
children: children
|
|
42
|
+
})
|
|
39
43
|
})
|
|
40
44
|
})
|
|
41
45
|
})
|
package/lib/Search/Search.js
CHANGED
|
@@ -159,10 +159,18 @@ const Search = /*#__PURE__*/(0, _react.forwardRef)((_ref4, ref) => {
|
|
|
159
159
|
const a11yLabelText = accessibilityLabel || getCopy('accessibilityLabel'); // Placeholder is optional and may be unset by passing an empty string
|
|
160
160
|
|
|
161
161
|
const placeholderText = placeholder !== null && placeholder !== void 0 ? placeholder : a11yLabelText;
|
|
162
|
+
const {
|
|
163
|
+
nativeID,
|
|
164
|
+
testID,
|
|
165
|
+
...containerProps
|
|
166
|
+
} = selectContainerProps(rest);
|
|
162
167
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_View.default, {
|
|
163
168
|
style: staticStyles.container,
|
|
164
|
-
...
|
|
165
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_TextInputBase.default, {
|
|
169
|
+
...containerProps,
|
|
170
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_TextInputBase.default, {
|
|
171
|
+
nativeID: nativeID,
|
|
172
|
+
testID: testID,
|
|
173
|
+
...selectInputProps(rest),
|
|
166
174
|
ref: ref,
|
|
167
175
|
tokens: appearances => selectInputTokens({
|
|
168
176
|
searchTokens: getThemeTokens(appearances),
|
|
@@ -9,6 +9,8 @@ var _react = _interopRequireWildcard(require("react"));
|
|
|
9
9
|
|
|
10
10
|
var _Platform = _interopRequireDefault(require("react-native-web/dist/cjs/exports/Platform"));
|
|
11
11
|
|
|
12
|
+
var _useSafeLayoutEffect = _interopRequireDefault(require("../utils/useSafeLayoutEffect"));
|
|
13
|
+
|
|
12
14
|
var _StackWrapBox = _interopRequireDefault(require("./StackWrapBox"));
|
|
13
15
|
|
|
14
16
|
var _StackWrapGap = _interopRequireDefault(require("./StackWrapGap"));
|
|
@@ -22,10 +24,10 @@ function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "functio
|
|
|
22
24
|
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
|
|
23
25
|
|
|
24
26
|
// In Jest/CI/SSR, global CSS isn't always available and doesn't always have .supports method
|
|
25
|
-
const cssSupports =
|
|
27
|
+
const cssSupports = (property, value) => {
|
|
26
28
|
var _window$CSS;
|
|
27
29
|
|
|
28
|
-
return typeof window !== 'undefined' && typeof ((_window$CSS = window.CSS) === null || _window$CSS === void 0 ? void 0 : _window$CSS.supports) === 'function' && window.CSS.supports(
|
|
30
|
+
return _Platform.default.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);
|
|
29
31
|
}; // CSS.supports needs an example of the type of value you intend to use.
|
|
30
32
|
// Will be an integer appended `px` after hooks and JSX styles are resolved.
|
|
31
33
|
|
|
@@ -42,22 +44,24 @@ const exampleGapValue = '1px';
|
|
|
42
44
|
const StackWrap = /*#__PURE__*/(0, _react.forwardRef)((props, ref) => {
|
|
43
45
|
var _props$gap;
|
|
44
46
|
|
|
47
|
+
const [canUseCSSGap, setCanUseCSSGap] = (0, _react.useState)(false);
|
|
45
48
|
const {
|
|
46
49
|
space
|
|
47
50
|
} = props; // Don't apply separate gap if `null` or `undefined`, so can be unset in Storybook etc
|
|
48
51
|
|
|
49
52
|
const gap = (_props$gap = props.gap) !== null && _props$gap !== void 0 ? _props$gap : space;
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// If possible, use the cleaner implementation that applies CSS `gap` styles to the container.
|
|
54
|
-
(0, _jsxRuntime.jsx)(_StackWrapGap.default, {
|
|
55
|
-
ref: ref,
|
|
56
|
-
...props
|
|
57
|
-
}) :
|
|
58
|
-
/*#__PURE__*/
|
|
53
|
+
const gapEqualsSpace = gap === space; // If possible, use the cleaner implementation that applies CSS `gap` styles to the container,
|
|
54
|
+
// preserving direct parent-child relationships between the container and each item, which
|
|
55
|
+
// can result in clearer descriptions on some screen readers (e.g. radio "X of Y" on MacOS).
|
|
59
56
|
// Else, use the fallback implementation which renders a `Box` component around each child.
|
|
60
|
-
|
|
57
|
+
|
|
58
|
+
const Component = canUseCSSGap ? _StackWrapGap.default : _StackWrapBox.default; // In SSR, the type of implementation must match the server during hydration, but
|
|
59
|
+
// the server can't know if gap is supported, so never use it until after hydration.
|
|
60
|
+
|
|
61
|
+
(0, _useSafeLayoutEffect.default)(() => {
|
|
62
|
+
setCanUseCSSGap(gapEqualsSpace && cssSupports('gap', exampleGapValue));
|
|
63
|
+
}, [gapEqualsSpace]);
|
|
64
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(Component, {
|
|
61
65
|
ref: ref,
|
|
62
66
|
...props
|
|
63
67
|
});
|
|
@@ -5,28 +5,18 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.default = void 0;
|
|
7
7
|
|
|
8
|
-
var _react = require("react");
|
|
9
|
-
|
|
10
8
|
var _Dimensions = _interopRequireDefault(require("react-native-web/dist/cjs/exports/Dimensions"));
|
|
11
9
|
|
|
12
10
|
var _systemConstants = require("@telus-uds/system-constants");
|
|
13
11
|
|
|
12
|
+
var _useSafeLayoutEffect = _interopRequireDefault(require("../utils/useSafeLayoutEffect"));
|
|
13
|
+
|
|
14
14
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
15
15
|
|
|
16
16
|
// Use Dimensions instead of useWindowDimensions because useWindowDimensions forces context
|
|
17
17
|
// to update on every pixel change during window resize; but we only want rerenders to occur
|
|
18
18
|
// when a viewport threshold has been crossed.
|
|
19
19
|
const lookupViewport = () => _systemConstants.viewports.select(_Dimensions.default.get('window').width);
|
|
20
|
-
/**
|
|
21
|
-
* In SSR, React gets spooked if it sees `useLayoutEffect` and fires warnings assuming the
|
|
22
|
-
* developer doesn't realise the effect won't run: https://reactjs.org/link/uselayouteffect-ssr
|
|
23
|
-
*
|
|
24
|
-
* To avoid these warnings while still conforming to the rules of hooks, always use this
|
|
25
|
-
* explicitly no-op hook, instead of the useLayoutEffect that is implicitly no-op on SSR.
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const useViewportListenerSSR = () => {};
|
|
30
20
|
/**
|
|
31
21
|
* When client-side rendering, immediately set the viewport to the correct value as a layout effect so
|
|
32
22
|
* if the viewport isn't the smallest, any SSR-rendered components rerender correctly before anything
|
|
@@ -34,8 +24,8 @@ const useViewportListenerSSR = () => {};
|
|
|
34
24
|
*/
|
|
35
25
|
|
|
36
26
|
|
|
37
|
-
const
|
|
38
|
-
(0,
|
|
27
|
+
const useViewportListener = setViewport => {
|
|
28
|
+
(0, _useSafeLayoutEffect.default)(() => {
|
|
39
29
|
setViewport(lookupViewport());
|
|
40
30
|
|
|
41
31
|
const onChange = _ref => {
|
|
@@ -57,10 +47,7 @@ const useViewportListenerCSR = setViewport => {
|
|
|
57
47
|
}
|
|
58
48
|
};
|
|
59
49
|
}, [setViewport]);
|
|
60
|
-
};
|
|
61
|
-
|
|
50
|
+
};
|
|
62
51
|
|
|
63
|
-
const isSSR = typeof window === 'undefined';
|
|
64
|
-
const useViewportListener = isSSR ? useViewportListenerSSR : useViewportListenerCSR;
|
|
65
52
|
var _default = useViewportListener;
|
|
66
53
|
exports.default = _default;
|
|
@@ -13,6 +13,8 @@ var _Easing = _interopRequireDefault(require("react-native-web/dist/cjs/exports/
|
|
|
13
13
|
|
|
14
14
|
var _Platform = _interopRequireDefault(require("react-native-web/dist/cjs/exports/Platform"));
|
|
15
15
|
|
|
16
|
+
var _useSafeLayoutEffect = _interopRequireDefault(require("../useSafeLayoutEffect"));
|
|
17
|
+
|
|
16
18
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
17
19
|
|
|
18
20
|
// TODO: systematise animations
|
|
@@ -32,7 +34,7 @@ function useVerticalExpandAnimation(_ref) {
|
|
|
32
34
|
expandDuration,
|
|
33
35
|
collapseDuration
|
|
34
36
|
} = tokens;
|
|
35
|
-
(0,
|
|
37
|
+
(0, _useSafeLayoutEffect.default)(() => {
|
|
36
38
|
if (expandStateChanged) {
|
|
37
39
|
setIsAnimating(true);
|
|
38
40
|
setWasExpanded(isExpanded);
|
package/lib/utils/index.js
CHANGED
|
@@ -9,6 +9,7 @@ var _exportNames = {
|
|
|
9
9
|
useHash: true,
|
|
10
10
|
useSpacingScale: true,
|
|
11
11
|
useResponsiveProp: true,
|
|
12
|
+
useSafeLayoutEffect: true,
|
|
12
13
|
useScrollBlocking: true,
|
|
13
14
|
useUniqueId: true,
|
|
14
15
|
withLinkRouter: true,
|
|
@@ -44,6 +45,12 @@ Object.defineProperty(exports, "useResponsiveProp", {
|
|
|
44
45
|
return _useResponsiveProp.default;
|
|
45
46
|
}
|
|
46
47
|
});
|
|
48
|
+
Object.defineProperty(exports, "useSafeLayoutEffect", {
|
|
49
|
+
enumerable: true,
|
|
50
|
+
get: function () {
|
|
51
|
+
return _useSafeLayoutEffect.default;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
47
54
|
Object.defineProperty(exports, "useScrollBlocking", {
|
|
48
55
|
enumerable: true,
|
|
49
56
|
get: function () {
|
|
@@ -175,6 +182,8 @@ Object.keys(_useResponsiveProp).forEach(function (key) {
|
|
|
175
182
|
});
|
|
176
183
|
});
|
|
177
184
|
|
|
185
|
+
var _useSafeLayoutEffect = _interopRequireDefault(require("./useSafeLayoutEffect"));
|
|
186
|
+
|
|
178
187
|
var _useScrollBlocking = _interopRequireDefault(require("./useScrollBlocking"));
|
|
179
188
|
|
|
180
189
|
var _useUniqueId = _interopRequireDefault(require("./useUniqueId"));
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
|
|
8
|
+
var _react = require("react");
|
|
9
|
+
|
|
10
|
+
var _HydrationContext = require("../BaseProvider/HydrationContext");
|
|
11
|
+
|
|
12
|
+
const isSSR = typeof window === 'undefined';
|
|
13
|
+
|
|
14
|
+
const noop = () => {};
|
|
15
|
+
/**
|
|
16
|
+
* useSafeLayoutEffect is a alternative to useLayoutEffect that avoids SSR hydration problems:
|
|
17
|
+
* - In a client-side render, it uses useLayoutEffect to avoid flashing the pre-render UI to the user.
|
|
18
|
+
* - During hydration from SSR, the provided function is skipped to avoid mismatches from server content.
|
|
19
|
+
* - In SSR, it is a no-op function to avoid warnings about using useLayoutEffect in SSR
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
const useSafeLayoutEffect = isSSR ? noop // avoid React's fussy warnings by ensuring to never call useLayoutEffect on server
|
|
24
|
+
: function (fn) {
|
|
25
|
+
let deps = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
|
|
26
|
+
const isHydrating = (0, _HydrationContext.useHydrationContext)(); // Callback updates and effect re-runs when deps array content changes, like useEffect.
|
|
27
|
+
|
|
28
|
+
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
|
29
|
+
|
|
30
|
+
const callback = (0, _react.useCallback)(fn, deps);
|
|
31
|
+
(0, _react.useLayoutEffect)(() => {
|
|
32
|
+
// Do nothing before hydrating server-generated content, like useEffect. When hydration completes,
|
|
33
|
+
// useHydrationContext provides false, re-rendering this hook and re-running the effect.
|
|
34
|
+
if (isHydrating) return noop; // If there's no hydration in progress, behave like useLayoutEffect.
|
|
35
|
+
|
|
36
|
+
return callback();
|
|
37
|
+
}, [isHydrating, callback]);
|
|
38
|
+
};
|
|
39
|
+
var _default = useSafeLayoutEffect;
|
|
40
|
+
exports.default = _default;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React, { createContext, useContext, useEffect, useState } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import Platform from "react-native-web/dist/exports/Platform";
|
|
4
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
5
|
+
const HydrationContext = /*#__PURE__*/createContext();
|
|
6
|
+
const isSSR = typeof window === 'undefined';
|
|
7
|
+
|
|
8
|
+
const hasWebStyleTag = () => {
|
|
9
|
+
var _document;
|
|
10
|
+
|
|
11
|
+
if (isSSR || Platform.OS !== 'web' || typeof document !== 'object') {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return Boolean((_document = document) === null || _document === void 0 ? void 0 : _document.getElementById('react-native-stylesheet'));
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Returns true if this render cycle is the hydration of existing SSR content.
|
|
19
|
+
*
|
|
20
|
+
* Use this when changing how content renders based on data that is instantly available
|
|
21
|
+
* during the very first client-side render or hydration, but not available on the server,
|
|
22
|
+
* to ensure no changes happen until the original SSR DOM has been hydrated.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
export const useHydrationContext = () => useContext(HydrationContext);
|
|
27
|
+
/**
|
|
28
|
+
* Allows components and hooks to observe if SSR hydration is currently in progress
|
|
29
|
+
* and if so, to re-render with content that differs to the server when it is complete.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
export const HydrationProvider = _ref => {
|
|
33
|
+
let {
|
|
34
|
+
children
|
|
35
|
+
} = _ref;
|
|
36
|
+
const [hasMounted, setHasMounted] = useState(false);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
setHasMounted(true);
|
|
39
|
+
}, []); // If we've got a HydrationProvider inside a HydrationProvider somehow, defer to the top one
|
|
40
|
+
|
|
41
|
+
const valueFromAncestor = useHydrationContext();
|
|
42
|
+
const isHydrating = valueFromAncestor !== null && valueFromAncestor !== void 0 ? valueFromAncestor : Boolean(!hasMounted && hasWebStyleTag());
|
|
43
|
+
return /*#__PURE__*/_jsx(HydrationContext.Provider, {
|
|
44
|
+
value: isHydrating,
|
|
45
|
+
children: children
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
HydrationProvider.propTypes = {
|
|
49
|
+
children: PropTypes.node
|
|
50
|
+
};
|
|
51
|
+
export default HydrationProvider;
|
|
@@ -6,6 +6,7 @@ import { PortalProvider } from '@gorhom/portal';
|
|
|
6
6
|
import A11yInfoProvider from '../A11yInfoProvider';
|
|
7
7
|
import ViewportProvider from '../ViewportProvider';
|
|
8
8
|
import ThemeProvider from '../ThemeProvider';
|
|
9
|
+
import { HydrationProvider } from './HydrationContext';
|
|
9
10
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
10
11
|
|
|
11
12
|
const BaseProvider = _ref => {
|
|
@@ -14,13 +15,15 @@ const BaseProvider = _ref => {
|
|
|
14
15
|
children,
|
|
15
16
|
themeOptions
|
|
16
17
|
} = _ref;
|
|
17
|
-
return /*#__PURE__*/_jsx(
|
|
18
|
-
children: /*#__PURE__*/_jsx(
|
|
19
|
-
children: /*#__PURE__*/_jsx(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
children:
|
|
18
|
+
return /*#__PURE__*/_jsx(HydrationProvider, {
|
|
19
|
+
children: /*#__PURE__*/_jsx(A11yInfoProvider, {
|
|
20
|
+
children: /*#__PURE__*/_jsx(ViewportProvider, {
|
|
21
|
+
children: /*#__PURE__*/_jsx(ThemeProvider, {
|
|
22
|
+
defaultTheme: defaultTheme,
|
|
23
|
+
themeOptions: themeOptions,
|
|
24
|
+
children: /*#__PURE__*/_jsx(PortalProvider, {
|
|
25
|
+
children: children
|
|
26
|
+
})
|
|
24
27
|
})
|
|
25
28
|
})
|
|
26
29
|
})
|
|
@@ -135,10 +135,18 @@ const Search = /*#__PURE__*/forwardRef((_ref4, ref) => {
|
|
|
135
135
|
const a11yLabelText = accessibilityLabel || getCopy('accessibilityLabel'); // Placeholder is optional and may be unset by passing an empty string
|
|
136
136
|
|
|
137
137
|
const placeholderText = placeholder !== null && placeholder !== void 0 ? placeholder : a11yLabelText;
|
|
138
|
+
const {
|
|
139
|
+
nativeID,
|
|
140
|
+
testID,
|
|
141
|
+
...containerProps
|
|
142
|
+
} = selectContainerProps(rest);
|
|
138
143
|
return /*#__PURE__*/_jsxs(View, {
|
|
139
144
|
style: staticStyles.container,
|
|
140
|
-
...
|
|
141
|
-
children: [/*#__PURE__*/_jsx(TextInputBase, {
|
|
145
|
+
...containerProps,
|
|
146
|
+
children: [/*#__PURE__*/_jsx(TextInputBase, {
|
|
147
|
+
nativeID: nativeID,
|
|
148
|
+
testID: testID,
|
|
149
|
+
...selectInputProps(rest),
|
|
142
150
|
ref: ref,
|
|
143
151
|
tokens: appearances => selectInputTokens({
|
|
144
152
|
searchTokens: getThemeTokens(appearances),
|
|
@@ -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 =
|
|
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(
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
});
|
|
@@ -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';
|
|
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
|
|
26
|
-
|
|
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
|
-
};
|
|
48
|
-
|
|
37
|
+
};
|
|
49
38
|
|
|
50
|
-
const isSSR = typeof window === 'undefined';
|
|
51
|
-
const useViewportListener = isSSR ? useViewportListenerSSR : useViewportListenerCSR;
|
|
52
39
|
export default useViewportListener;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { useEffect,
|
|
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";
|
|
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
|
-
|
|
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
|
@@ -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
|
|
@@ -4,15 +4,18 @@ import { PortalProvider } from '@gorhom/portal'
|
|
|
4
4
|
import A11yInfoProvider from '../A11yInfoProvider'
|
|
5
5
|
import ViewportProvider from '../ViewportProvider'
|
|
6
6
|
import ThemeProvider from '../ThemeProvider'
|
|
7
|
+
import { HydrationProvider } from './HydrationContext'
|
|
7
8
|
|
|
8
9
|
const BaseProvider = ({ defaultTheme, children, themeOptions }) => (
|
|
9
|
-
<
|
|
10
|
-
<
|
|
11
|
-
<
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
<HydrationProvider>
|
|
11
|
+
<A11yInfoProvider>
|
|
12
|
+
<ViewportProvider>
|
|
13
|
+
<ThemeProvider defaultTheme={defaultTheme} themeOptions={themeOptions}>
|
|
14
|
+
<PortalProvider>{children}</PortalProvider>
|
|
15
|
+
</ThemeProvider>
|
|
16
|
+
</ViewportProvider>
|
|
17
|
+
</A11yInfoProvider>
|
|
18
|
+
</HydrationProvider>
|
|
16
19
|
)
|
|
17
20
|
|
|
18
21
|
BaseProvider.propTypes = {
|
package/src/Search/Search.jsx
CHANGED
|
@@ -123,10 +123,13 @@ const Search = forwardRef(
|
|
|
123
123
|
const a11yLabelText = accessibilityLabel || getCopy('accessibilityLabel')
|
|
124
124
|
// Placeholder is optional and may be unset by passing an empty string
|
|
125
125
|
const placeholderText = placeholder ?? a11yLabelText
|
|
126
|
+
const { nativeID, testID, ...containerProps } = selectContainerProps(rest)
|
|
126
127
|
|
|
127
128
|
return (
|
|
128
|
-
<View style={staticStyles.container} {...
|
|
129
|
+
<View style={staticStyles.container} {...containerProps}>
|
|
129
130
|
<TextInputBase
|
|
131
|
+
nativeID={nativeID}
|
|
132
|
+
testID={testID}
|
|
130
133
|
{...selectInputProps(rest)}
|
|
131
134
|
ref={ref}
|
|
132
135
|
tokens={(appearances) =>
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import React, { forwardRef } from 'react'
|
|
1
|
+
import React, { forwardRef, useState } from 'react'
|
|
2
2
|
import { Platform } from 'react-native'
|
|
3
3
|
|
|
4
|
+
import useSafeLayoutEffect from '../utils/useSafeLayoutEffect'
|
|
4
5
|
import StackWrapBox from './StackWrapBox'
|
|
5
6
|
import StackWrapGap from './StackWrapGap'
|
|
6
7
|
|
|
7
8
|
// In Jest/CI/SSR, global CSS isn't always available and doesn't always have .supports method
|
|
8
|
-
const cssSupports = (
|
|
9
|
+
const cssSupports = (property, value) =>
|
|
10
|
+
Platform.OS === 'web' &&
|
|
9
11
|
typeof window !== 'undefined' &&
|
|
10
12
|
typeof window.CSS?.supports === 'function' &&
|
|
11
|
-
window.CSS.supports(
|
|
13
|
+
window.CSS.supports(property, value)
|
|
12
14
|
|
|
13
15
|
// CSS.supports needs an example of the type of value you intend to use.
|
|
14
16
|
// Will be an integer appended `px` after hooks and JSX styles are resolved.
|
|
@@ -22,19 +24,24 @@ const exampleGapValue = '1px'
|
|
|
22
24
|
* If a different spacing is desired between wrapped lines, pass a spacing value to the `gap` prop.
|
|
23
25
|
*/
|
|
24
26
|
const StackWrap = forwardRef((props, ref) => {
|
|
27
|
+
const [canUseCSSGap, setCanUseCSSGap] = useState(false)
|
|
25
28
|
const { space } = props
|
|
26
29
|
// Don't apply separate gap if `null` or `undefined`, so can be unset in Storybook etc
|
|
27
30
|
const gap = props.gap ?? space
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
)
|
|
31
|
+
const gapEqualsSpace = gap === space
|
|
32
|
+
|
|
33
|
+
// If possible, use the cleaner implementation that applies CSS `gap` styles to the container,
|
|
34
|
+
// preserving direct parent-child relationships between the container and each item, which
|
|
35
|
+
// can result in clearer descriptions on some screen readers (e.g. radio "X of Y" on MacOS).
|
|
36
|
+
// Else, use the fallback implementation which renders a `Box` component around each child.
|
|
37
|
+
const Component = canUseCSSGap ? StackWrapGap : StackWrapBox
|
|
38
|
+
// In SSR, the type of implementation must match the server during hydration, but
|
|
39
|
+
// the server can't know if gap is supported, so never use it until after hydration.
|
|
40
|
+
useSafeLayoutEffect(() => {
|
|
41
|
+
setCanUseCSSGap(gapEqualsSpace && cssSupports('gap', exampleGapValue))
|
|
42
|
+
}, [gapEqualsSpace])
|
|
43
|
+
|
|
44
|
+
return <Component ref={ref} {...props} />
|
|
38
45
|
})
|
|
39
46
|
StackWrap.displayName = 'StackWrap'
|
|
40
47
|
|
|
@@ -1,28 +1,20 @@
|
|
|
1
|
-
import { useLayoutEffect } from 'react'
|
|
2
1
|
import { Dimensions } from 'react-native'
|
|
3
2
|
import { viewports } from '@telus-uds/system-constants'
|
|
4
3
|
|
|
4
|
+
import useSafeLayoutEffect from '../utils/useSafeLayoutEffect'
|
|
5
|
+
|
|
5
6
|
// Use Dimensions instead of useWindowDimensions because useWindowDimensions forces context
|
|
6
7
|
// to update on every pixel change during window resize; but we only want rerenders to occur
|
|
7
8
|
// when a viewport threshold has been crossed.
|
|
8
9
|
const lookupViewport = () => viewports.select(Dimensions.get('window').width)
|
|
9
10
|
|
|
10
|
-
/**
|
|
11
|
-
* In SSR, React gets spooked if it sees `useLayoutEffect` and fires warnings assuming the
|
|
12
|
-
* developer doesn't realise the effect won't run: https://reactjs.org/link/uselayouteffect-ssr
|
|
13
|
-
*
|
|
14
|
-
* To avoid these warnings while still conforming to the rules of hooks, always use this
|
|
15
|
-
* explicitly no-op hook, instead of the useLayoutEffect that is implicitly no-op on SSR.
|
|
16
|
-
*/
|
|
17
|
-
const useViewportListenerSSR = () => {}
|
|
18
|
-
|
|
19
11
|
/**
|
|
20
12
|
* When client-side rendering, immediately set the viewport to the correct value as a layout effect so
|
|
21
13
|
* if the viewport isn't the smallest, any SSR-rendered components rerender correctly before anything
|
|
22
14
|
* is shown to the user. Then bind events to update the viewport if it changes.
|
|
23
15
|
*/
|
|
24
|
-
const
|
|
25
|
-
|
|
16
|
+
const useViewportListener = (setViewport) => {
|
|
17
|
+
useSafeLayoutEffect(() => {
|
|
26
18
|
setViewport(lookupViewport())
|
|
27
19
|
|
|
28
20
|
const onChange = ({ window }) => setViewport(viewports.select(window.width))
|
|
@@ -41,8 +33,4 @@ const useViewportListenerCSR = (setViewport) => {
|
|
|
41
33
|
}, [setViewport])
|
|
42
34
|
}
|
|
43
35
|
|
|
44
|
-
// Window is a defined global object in both Web and Native client-side, and undefined in SSR
|
|
45
|
-
const isSSR = typeof window === 'undefined'
|
|
46
|
-
const useViewportListener = isSSR ? useViewportListenerSSR : useViewportListenerCSR
|
|
47
|
-
|
|
48
36
|
export default useViewportListener
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { useEffect,
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
2
|
import { Animated, Easing, Platform } from 'react-native'
|
|
3
3
|
|
|
4
|
+
import useSafeLayoutEffect from '../useSafeLayoutEffect'
|
|
5
|
+
|
|
4
6
|
// TODO: systematise animations
|
|
5
7
|
// https://github.com/telus/universal-design-system/issues/487
|
|
6
8
|
function useVerticalExpandAnimation({ containerHeight, isExpanded, tokens }) {
|
|
@@ -13,7 +15,7 @@ function useVerticalExpandAnimation({ containerHeight, isExpanded, tokens }) {
|
|
|
13
15
|
|
|
14
16
|
const { expandDuration, collapseDuration } = tokens
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
useSafeLayoutEffect(() => {
|
|
17
19
|
if (expandStateChanged) {
|
|
18
20
|
setIsAnimating(true)
|
|
19
21
|
setWasExpanded(isExpanded)
|
package/src/utils/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export { default as useCopy } from './useCopy'
|
|
|
10
10
|
export { default as useHash } from './useHash'
|
|
11
11
|
export { default as useSpacingScale } from './useSpacingScale'
|
|
12
12
|
export { default as useResponsiveProp } from './useResponsiveProp'
|
|
13
|
+
export { default as useSafeLayoutEffect } from './useSafeLayoutEffect'
|
|
13
14
|
export { default as useScrollBlocking } from './useScrollBlocking'
|
|
14
15
|
export * from './useResponsiveProp'
|
|
15
16
|
export { default as useUniqueId } from './useUniqueId'
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useLayoutEffect, useCallback } from 'react'
|
|
2
|
+
import { useHydrationContext } from '../BaseProvider/HydrationContext'
|
|
3
|
+
|
|
4
|
+
const isSSR = typeof window === 'undefined'
|
|
5
|
+
const noop = () => {}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* useSafeLayoutEffect is a alternative to useLayoutEffect that avoids SSR hydration problems:
|
|
9
|
+
* - In a client-side render, it uses useLayoutEffect to avoid flashing the pre-render UI to the user.
|
|
10
|
+
* - During hydration from SSR, the provided function is skipped to avoid mismatches from server content.
|
|
11
|
+
* - In SSR, it is a no-op function to avoid warnings about using useLayoutEffect in SSR
|
|
12
|
+
*/
|
|
13
|
+
const useSafeLayoutEffect = isSSR
|
|
14
|
+
? noop // avoid React's fussy warnings by ensuring to never call useLayoutEffect on server
|
|
15
|
+
: (fn, deps = []) => {
|
|
16
|
+
const isHydrating = useHydrationContext()
|
|
17
|
+
|
|
18
|
+
// Callback updates and effect re-runs when deps array content changes, like useEffect.
|
|
19
|
+
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
|
20
|
+
const callback = useCallback(fn, deps)
|
|
21
|
+
|
|
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
|
|
26
|
+
// If there's no hydration in progress, behave like useLayoutEffect.
|
|
27
|
+
return callback()
|
|
28
|
+
}, [isHydrating, callback])
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default useSafeLayoutEffect
|