@itwin/itwinui-react 3.7.3 → 3.8.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 (57) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/cjs/core/Carousel/Carousel.d.ts +6 -42
  3. package/cjs/core/Carousel/CarouselNavigation.d.ts +4 -4
  4. package/cjs/core/LabeledSelect/LabeledSelect.d.ts +2 -2
  5. package/cjs/core/Popover/Popover.d.ts +2 -2
  6. package/cjs/core/Table/Table.js +1 -1
  7. package/cjs/core/Table/cells/DefaultCell.d.ts +2 -1
  8. package/cjs/core/Table/cells/DefaultCell.js +6 -1
  9. package/cjs/core/Table/utils.d.ts +3 -1
  10. package/cjs/core/Table/utils.js +30 -1
  11. package/cjs/core/ThemeProvider/ThemeProvider.js +8 -7
  12. package/cjs/core/Tile/Tile.d.ts +4 -10
  13. package/cjs/core/Tooltip/Tooltip.js +5 -2
  14. package/cjs/core/VisuallyHidden/VisuallyHidden.js +2 -2
  15. package/cjs/core/utils/components/ShadowRoot.d.ts +6 -4
  16. package/cjs/core/utils/components/ShadowRoot.js +64 -28
  17. package/cjs/core/utils/hooks/index.d.ts +1 -0
  18. package/cjs/core/utils/hooks/index.js +1 -0
  19. package/cjs/core/utils/hooks/useMediaQuery.d.ts +1 -1
  20. package/cjs/core/utils/hooks/useMediaQuery.js +11 -26
  21. package/cjs/core/utils/hooks/useSyncExternalStore.d.ts +5 -0
  22. package/cjs/core/utils/hooks/useSyncExternalStore.js +68 -0
  23. package/cjs/core/utils/index.d.ts +1 -0
  24. package/cjs/core/utils/index.js +1 -0
  25. package/cjs/core/utils/providers/HydrationProvider.d.ts +16 -0
  26. package/cjs/core/utils/providers/HydrationProvider.js +75 -0
  27. package/cjs/core/utils/providers/index.d.ts +1 -0
  28. package/cjs/core/utils/providers/index.js +21 -0
  29. package/esm/core/Carousel/Carousel.d.ts +6 -42
  30. package/esm/core/Carousel/CarouselNavigation.d.ts +4 -4
  31. package/esm/core/LabeledSelect/LabeledSelect.d.ts +2 -2
  32. package/esm/core/Popover/Popover.d.ts +2 -2
  33. package/esm/core/Table/Table.js +2 -2
  34. package/esm/core/Table/cells/DefaultCell.d.ts +2 -1
  35. package/esm/core/Table/cells/DefaultCell.js +6 -1
  36. package/esm/core/Table/utils.d.ts +3 -1
  37. package/esm/core/Table/utils.js +6 -0
  38. package/esm/core/ThemeProvider/ThemeProvider.js +9 -8
  39. package/esm/core/Tile/Tile.d.ts +4 -10
  40. package/esm/core/Tooltip/Tooltip.js +6 -3
  41. package/esm/core/VisuallyHidden/VisuallyHidden.js +3 -3
  42. package/esm/core/utils/components/ShadowRoot.d.ts +6 -4
  43. package/esm/core/utils/components/ShadowRoot.js +64 -28
  44. package/esm/core/utils/hooks/index.d.ts +1 -0
  45. package/esm/core/utils/hooks/index.js +1 -0
  46. package/esm/core/utils/hooks/useMediaQuery.d.ts +1 -1
  47. package/esm/core/utils/hooks/useMediaQuery.js +11 -26
  48. package/esm/core/utils/hooks/useSyncExternalStore.d.ts +5 -0
  49. package/esm/core/utils/hooks/useSyncExternalStore.js +42 -0
  50. package/esm/core/utils/index.d.ts +1 -0
  51. package/esm/core/utils/index.js +1 -0
  52. package/esm/core/utils/providers/HydrationProvider.d.ts +16 -0
  53. package/esm/core/utils/providers/HydrationProvider.js +47 -0
  54. package/esm/core/utils/providers/index.d.ts +1 -0
  55. package/esm/core/utils/providers/index.js +5 -0
  56. package/package.json +2 -2
  57. package/styles.css +1 -677
@@ -4,6 +4,8 @@
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  import * as React from 'react';
6
6
  import * as ReactDOM from 'react-dom';
7
+ import { useLatestRef, useLayoutEffect } from '../hooks/index.js';
8
+ import { useHydration } from '../providers/index.js';
7
9
  const isBrowser = typeof document !== 'undefined';
8
10
  const supportsDSD = isBrowser && 'shadowRootMode' in HTMLTemplateElement.prototype;
9
11
  const supportsAdoptedStylesheets = isBrowser && 'adoptedStyleSheets' in Document.prototype;
@@ -13,28 +15,8 @@ const supportsAdoptedStylesheets = isBrowser && 'adoptedStyleSheets' in Document
13
15
  *
14
16
  * @private
15
17
  */
16
- export const ShadowRoot = ({ children, css, }) => {
17
- const [shadowRoot, setShadowRoot] = React.useState();
18
- const isFirstRender = useIsFirstRender();
19
- const styleSheet = React.useRef();
20
- const attachShadowRef = React.useCallback((template) => {
21
- const parent = template?.parentElement;
22
- if (!template || !parent) {
23
- return;
24
- }
25
- if (parent.shadowRoot) {
26
- parent.shadowRoot.replaceChildren(); // Remove previous shadowroot content
27
- }
28
- queueMicrotask(() => {
29
- const shadow = parent.shadowRoot || parent.attachShadow({ mode: 'open' });
30
- if (css && supportsAdoptedStylesheets) {
31
- styleSheet.current || (styleSheet.current = new CSSStyleSheet());
32
- styleSheet.current.replaceSync(css);
33
- shadow.adoptedStyleSheets = [styleSheet.current];
34
- }
35
- ReactDOM.flushSync(() => setShadowRoot(shadow));
36
- });
37
- }, [css]);
18
+ export const ShadowRoot = ({ children, css }) => {
19
+ const isHydrating = useHydration() === 'hydrating';
38
20
  if (!isBrowser) {
39
21
  return (React.createElement("template", { shadowrootmode: 'open' },
40
22
  css && React.createElement("style", null, css),
@@ -42,14 +24,68 @@ export const ShadowRoot = ({ children, css, }) => {
42
24
  }
43
25
  // In browsers that support DSD, the template will be automatically removed as soon as it's parsed.
44
26
  // To pass hydration, the first client render needs to emulate this browser behavior and return null.
45
- if (supportsDSD && isFirstRender) {
27
+ if (supportsDSD && isHydrating) {
46
28
  return null;
47
29
  }
48
- return (React.createElement(React.Fragment, null, shadowRoot ? (ReactDOM.createPortal(children, shadowRoot)) : (React.createElement("template", { ref: attachShadowRef }))));
30
+ return React.createElement(ClientShadowRoot, { css: css }, children);
49
31
  };
50
32
  // ----------------------------------------------------------------------------
51
- function useIsFirstRender() {
52
- const [isFirstRender, setIsFirstRender] = React.useState(true);
53
- React.useEffect(() => setIsFirstRender(false), []);
54
- return isFirstRender;
33
+ const ClientShadowRoot = ({ children, css }) => {
34
+ const templateRef = React.useRef(null);
35
+ const shadowRoot = useShadowRoot(templateRef, { css });
36
+ // fallback to <style> tag if adoptedStyleSheets is not supported
37
+ const fallbackCss = !supportsAdoptedStylesheets && css ? React.createElement("style", null, css) : null;
38
+ return shadowRoot ? (ReactDOM.createPortal(React.createElement(React.Fragment, null,
39
+ fallbackCss,
40
+ children), shadowRoot)) : (React.createElement("template", { ref: templateRef }));
41
+ };
42
+ // ----------------------------------------------------------------------------
43
+ /**
44
+ * Given a ref, this hook will return a shadowRoot attached to its parent element.
45
+ *
46
+ * The css will be added to the shadowRoot using `adoptedStyleSheets` (if supported).
47
+ */
48
+ function useShadowRoot(templateRef, { css = '' }) {
49
+ const [shadowRoot, setShadowRoot] = React.useState(null);
50
+ const styleSheet = React.useRef();
51
+ const latestCss = useLatestRef(css);
52
+ const latestShadowRoot = useLatestRef(shadowRoot);
53
+ useLayoutEffect(() => {
54
+ const parent = templateRef.current?.parentElement;
55
+ if (!parent) {
56
+ return;
57
+ }
58
+ const setupOrReuseShadowRoot = () => {
59
+ if (parent.shadowRoot && latestShadowRoot.current === null) {
60
+ parent.shadowRoot.replaceChildren(); // Remove previous shadowroot content
61
+ }
62
+ const shadow = parent.shadowRoot || parent.attachShadow({ mode: 'open' });
63
+ if (supportsAdoptedStylesheets) {
64
+ // create an empty stylesheet and add it to the shadowRoot
65
+ const currentWindow = shadow.ownerDocument.defaultView || globalThis;
66
+ styleSheet.current = new currentWindow.CSSStyleSheet();
67
+ shadow.adoptedStyleSheets = [styleSheet.current];
68
+ // add the CSS immediately to avoid FOUC (one-time)
69
+ if (latestCss.current) {
70
+ styleSheet.current.replaceSync(latestCss.current);
71
+ }
72
+ }
73
+ // Flush the state immediately after shadow-root is attached, to ensure that layout
74
+ // measurements in parent component are correct.
75
+ // Without this, the parent component may end up measuring the layout when the shadow-root
76
+ // is attached in the DOM but React hasn't rendered any slots or content into it yet.
77
+ ReactDOM.flushSync(() => setShadowRoot(shadow));
78
+ };
79
+ queueMicrotask(() => {
80
+ setupOrReuseShadowRoot();
81
+ });
82
+ return () => void setShadowRoot(null);
83
+ }, [templateRef, latestCss, latestShadowRoot]);
84
+ // Synchronize `css` with contents of the existing stylesheet
85
+ useLayoutEffect(() => {
86
+ if (css && supportsAdoptedStylesheets) {
87
+ styleSheet.current?.replaceSync(css);
88
+ }
89
+ }, [css]);
90
+ return shadowRoot;
55
91
  }
@@ -12,3 +12,4 @@ export * from './useIsomorphicLayoutEffect.js';
12
12
  export * from './useIsClient.js';
13
13
  export * from './useId.js';
14
14
  export * from './useControlledState.js';
15
+ export * from './useSyncExternalStore.js';
@@ -16,3 +16,4 @@ export * from './useIsomorphicLayoutEffect.js';
16
16
  export * from './useIsClient.js';
17
17
  export * from './useId.js';
18
18
  export * from './useControlledState.js';
19
+ export * from './useSyncExternalStore.js';
@@ -1 +1 @@
1
- export declare const useMediaQuery: (queryString: string) => boolean;
1
+ export declare const useMediaQuery: (queryString: string) => boolean | undefined;
@@ -3,32 +3,17 @@
3
3
  * See LICENSE.md in the project root for license terms and full copyright notice.
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  import * as React from 'react';
6
- import { getWindow } from '../functions/index.js';
7
- import { useLayoutEffect } from './useIsomorphicLayoutEffect.js';
6
+ import { useSyncExternalStore } from './useSyncExternalStore.js';
8
7
  export const useMediaQuery = (queryString) => {
9
- const [matches, setMatches] = React.useState();
10
- useLayoutEffect(() => {
11
- const mediaQueryList = getWindow()?.matchMedia?.(queryString);
12
- const handleChange = ({ matches }) => setMatches(matches);
13
- if (mediaQueryList != undefined) {
14
- setMatches(mediaQueryList.matches);
15
- try {
16
- mediaQueryList.addEventListener('change', handleChange);
17
- }
18
- catch {
19
- // Safari 13 fallback
20
- mediaQueryList.addListener?.(handleChange);
21
- }
22
- }
23
- return () => {
24
- try {
25
- mediaQueryList?.removeEventListener('change', handleChange);
26
- }
27
- catch {
28
- // Safari 13 fallback
29
- mediaQueryList?.removeListener?.(handleChange);
30
- }
31
- };
8
+ const getSnapshot = React.useCallback(() => {
9
+ return typeof window !== 'undefined'
10
+ ? window.matchMedia?.(queryString).matches
11
+ : undefined;
32
12
  }, [queryString]);
33
- return !!matches;
13
+ const subscribe = React.useCallback((onChange) => {
14
+ const mediaQueryList = window.matchMedia?.(queryString);
15
+ mediaQueryList?.addEventListener?.('change', onChange);
16
+ return () => mediaQueryList?.removeEventListener?.('change', onChange);
17
+ }, [queryString]);
18
+ return useSyncExternalStore(subscribe, getSnapshot, () => undefined);
34
19
  };
@@ -0,0 +1,5 @@
1
+ import * as React from 'react';
2
+ /**
3
+ * Wrapper around `React.useSyncExternalStore` that uses a shim for React 17.
4
+ */
5
+ export declare const useSyncExternalStore: typeof React.useSyncExternalStore;
@@ -0,0 +1,42 @@
1
+ /*---------------------------------------------------------------------------------------------
2
+ * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
3
+ * See LICENSE.md in the project root for license terms and full copyright notice.
4
+ *--------------------------------------------------------------------------------------------*/
5
+ import * as React from 'react';
6
+ const _React = React; // prevent bundlers from stripping the namespace import
7
+ /**
8
+ * Wrapper around `React.useSyncExternalStore` that uses a shim for React 17.
9
+ */
10
+ export const useSyncExternalStore = _React.useSyncExternalStore || useSyncExternalStoreShim;
11
+ // ----------------------------------------------------------------------------
12
+ /**
13
+ * The shim below is adapted from React's source to make it ESM-compatible.
14
+ *
15
+ * Note: This does not use `getServerSnapshot` at all, because there is
16
+ * apparently no way to check "hydrating" state in pre-18.
17
+ *
18
+ * @see https://github.com/facebook/react/tree/main/packages/use-sync-external-store
19
+ */
20
+ function useSyncExternalStoreShim(subscribe, getSnapshot) {
21
+ const value = getSnapshot();
22
+ const [{ instance }, forceUpdate] = React.useState({
23
+ instance: { value, getSnapshot },
24
+ });
25
+ React.useLayoutEffect(() => {
26
+ instance.value = value;
27
+ instance.getSnapshot = getSnapshot;
28
+ if (!Object.is(value, getSnapshot())) {
29
+ forceUpdate({ instance });
30
+ }
31
+ }, [subscribe, value, getSnapshot]); // eslint-disable-line
32
+ React.useEffect(() => {
33
+ const synchronize = () => {
34
+ if (!Object.is(instance.value, instance.getSnapshot())) {
35
+ forceUpdate({ instance });
36
+ }
37
+ };
38
+ synchronize();
39
+ return subscribe(synchronize);
40
+ }, [subscribe]); // eslint-disable-line
41
+ return value;
42
+ }
@@ -5,3 +5,4 @@ export * from './props.js';
5
5
  export * from './color/index.js';
6
6
  export * from './icons/index.js';
7
7
  export * from './types.js';
8
+ export * from './providers/index.js';
@@ -9,3 +9,4 @@ export * from './props.js';
9
9
  export * from './color/index.js';
10
10
  export * from './icons/index.js';
11
11
  export * from './types.js';
12
+ export * from './providers/index.js';
@@ -0,0 +1,16 @@
1
+ import * as React from 'react';
2
+ /**
3
+ * Hook that returns the hydration status of the app.
4
+ *
5
+ * @returns one of the following values:
6
+ * - `"hydrated"` after hydration is *definitely* complete (or is a pure client render)
7
+ * - `"hydrating"` if we know for sure that hydration is happening (in React 18)
8
+ * - `undefined` if the hydration status is unknown
9
+ *
10
+ * @private
11
+ */
12
+ export declare const useHydration: () => "hydrated" | "hydrating" | undefined;
13
+ /** @private */
14
+ export declare const HydrationProvider: ({ children, }: {
15
+ children: React.ReactNode;
16
+ }) => React.JSX.Element;
@@ -0,0 +1,47 @@
1
+ /*---------------------------------------------------------------------------------------------
2
+ * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
3
+ * See LICENSE.md in the project root for license terms and full copyright notice.
4
+ *--------------------------------------------------------------------------------------------*/
5
+ import * as React from 'react';
6
+ import { useSyncExternalStore, useIsClient } from '../hooks/index.js';
7
+ const HydrationContext = React.createContext(false);
8
+ const noopSubscribe = () => () => { };
9
+ const isServer = typeof window === 'undefined';
10
+ /**
11
+ * Hook that returns the hydration status of the app.
12
+ *
13
+ * @returns one of the following values:
14
+ * - `"hydrated"` after hydration is *definitely* complete (or is a pure client render)
15
+ * - `"hydrating"` if we know for sure that hydration is happening (in React 18)
16
+ * - `undefined` if the hydration status is unknown
17
+ *
18
+ * @private
19
+ */
20
+ export const useHydration = () => {
21
+ // Returns true only if getServerSnapshot is called on the client.
22
+ // In React 18, this is true during hydration.
23
+ const hydrating = useSyncExternalStore(noopSubscribe, () => false, () => !isServer);
24
+ // Returns true after hydration is complete (in all React versions).
25
+ const hydrated = React.useContext(HydrationContext);
26
+ const hydratedFallback = useIsClient();
27
+ if (hydrated || hydratedFallback) {
28
+ return 'hydrated';
29
+ }
30
+ else if (hydrating) {
31
+ return 'hydrating';
32
+ }
33
+ return undefined;
34
+ };
35
+ /** @private */
36
+ export const HydrationProvider = ({ children, }) => {
37
+ const [isHydrated, setIsHydrated] = React.useState(React.useContext(HydrationContext));
38
+ const onHydrate = React.useCallback(() => setIsHydrated(true), []);
39
+ return (React.createElement(HydrationContext.Provider, { value: isHydrated },
40
+ !isHydrated ? React.createElement(HydrationCheck, { onHydrate: onHydrate }) : null,
41
+ children));
42
+ };
43
+ /** This is extracted into a child component to ensure it runs first. */
44
+ const HydrationCheck = ({ onHydrate }) => {
45
+ React.useEffect(() => void onHydrate(), [onHydrate]);
46
+ return null;
47
+ };
@@ -0,0 +1 @@
1
+ export * from './HydrationProvider.js';
@@ -0,0 +1,5 @@
1
+ /*---------------------------------------------------------------------------------------------
2
+ * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
3
+ * See LICENSE.md in the project root for license terms and full copyright notice.
4
+ *--------------------------------------------------------------------------------------------*/
5
+ export * from './HydrationProvider.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itwin/itwinui-react",
3
- "version": "3.7.3",
3
+ "version": "3.8.0",
4
4
  "author": "Bentley Systems",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -56,7 +56,7 @@
56
56
  "ux"
57
57
  ],
58
58
  "dependencies": {
59
- "@floating-ui/react": "^0.26.3",
59
+ "@floating-ui/react": "^0.26.10",
60
60
  "@itwin/itwinui-illustrations-react": "^2.1.0",
61
61
  "classnames": "^2.3.2",
62
62
  "react-table": "^7.8.0",