@itwin/itwinui-react 2.12.25 → 2.12.26

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.12.26
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1949](https://github.com/iTwin/iTwinUI/pull/1949): ThemeProvider will now correctly inherit theme changes from a v3 ancestor.
8
+
3
9
  ## 2.12.25
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -27,6 +27,10 @@ The goal of this package is to provide React components that make it easier to u
27
27
 
28
28
  ---
29
29
 
30
+ 🆕 **iTwinUI v3** is now available! To upgrade from an older version, check out the [v3 migration guide](https://github.com/iTwin/iTwinUI/wiki/iTwinUI-react-v3-migration-guide).
31
+
32
+ ---
33
+
30
34
  ## Installation
31
35
 
32
36
  ```
@@ -62,19 +62,17 @@ const useStyles_js_1 = require("../utils/hooks/useStyles.js");
62
62
  * </ThemeProvider>
63
63
  */
64
64
  exports.ThemeProvider = React.forwardRef((props, ref) => {
65
- var _a, _b, _c, _d;
65
+ var _a, _b, _c, _d, _e;
66
66
  const { theme: themeProp, children, themeOptions = {}, includeCss = { withLayer: true }, ...rest } = props;
67
67
  const rootRef = React.useRef(null);
68
68
  const mergedRefs = (0, index_js_1.useMergedRefs)(rootRef, ref);
69
69
  const hasChildren = React.Children.count(children) > 0;
70
- const parentContext = React.useContext(exports.ThemeContext);
71
- const theme = themeProp === 'inherit' ? (_a = parentContext === null || parentContext === void 0 ? void 0 : parentContext.theme) !== null && _a !== void 0 ? _a : 'light' : themeProp;
70
+ const parent = useParentThemeAndContext(rootRef);
71
+ const theme = themeProp === 'inherit' ? (_a = parent.theme) !== null && _a !== void 0 ? _a : 'light' : themeProp;
72
72
  // default inherit highContrast option from parent if also inheriting base theme
73
- (_b = themeOptions.highContrast) !== null && _b !== void 0 ? _b : (themeOptions.highContrast = themeProp === 'inherit'
74
- ? (_c = parentContext === null || parentContext === void 0 ? void 0 : parentContext.themeOptions) === null || _c === void 0 ? void 0 : _c.highContrast
75
- : undefined);
73
+ (_b = themeOptions.highContrast) !== null && _b !== void 0 ? _b : (themeOptions.highContrast = themeProp === 'inherit' ? parent.highContrast : undefined);
76
74
  const newStylesLoaded = React.useRef(false);
77
- const stylesLoaded = (_d = parentContext === null || parentContext === void 0 ? void 0 : parentContext.stylesLoaded) !== null && _d !== void 0 ? _d : newStylesLoaded;
75
+ const stylesLoaded = (_d = (_c = parent.context) === null || _c === void 0 ? void 0 : _c.stylesLoaded) !== null && _d !== void 0 ? _d : newStylesLoaded;
78
76
  const contextValue = React.useMemo(() => ({
79
77
  theme,
80
78
  themeOptions,
@@ -92,7 +90,7 @@ exports.ThemeProvider = React.forwardRef((props, ref) => {
92
90
  ]);
93
91
  // if no children, then fallback to this wrapper component which calls useTheme
94
92
  if (!hasChildren) {
95
- return (React.createElement(ThemeLogicWrapper, { theme: theme, themeOptions: themeOptions !== null && themeOptions !== void 0 ? themeOptions : parentContext === null || parentContext === void 0 ? void 0 : parentContext.themeOptions }));
93
+ return (React.createElement(ThemeLogicWrapper, { theme: theme, themeOptions: themeOptions !== null && themeOptions !== void 0 ? themeOptions : (_e = parent.context) === null || _e === void 0 ? void 0 : _e.themeOptions }));
96
94
  }
97
95
  // now that we know there are children, we can render the root and provide the context value
98
96
  return (React.createElement(Root, { theme: theme, isInheritingTheme: themeProp === 'inherit', themeOptions: themeOptions, ref: mergedRefs, ...rest },
@@ -130,3 +128,49 @@ const Styles = (props) => {
130
128
  (0, useStyles_js_1.useStyles)({ includeCss, document });
131
129
  return null;
132
130
  };
131
+ /**
132
+ * Returns theme information from either parent ThemeContext or by reading the closest
133
+ * data-iui-theme attribute if context is not found.
134
+ *
135
+ * Also returns the ThemeContext itself (if found).
136
+ */
137
+ const useParentThemeAndContext = (rootRef) => {
138
+ var _a, _b, _c, _d;
139
+ const parentContext = React.useContext(exports.ThemeContext);
140
+ const [parentThemeState, setParentTheme] = React.useState(parentContext === null || parentContext === void 0 ? void 0 : parentContext.theme);
141
+ const [parentHighContrastState, setParentHighContrastState] = React.useState((_a = parentContext === null || parentContext === void 0 ? void 0 : parentContext.themeOptions) === null || _a === void 0 ? void 0 : _a.highContrast);
142
+ const parentThemeRef = (0, index_js_1.useLatestRef)(parentContext === null || parentContext === void 0 ? void 0 : parentContext.theme);
143
+ (0, index_js_1.useIsomorphicLayoutEffect)(() => {
144
+ var _a, _b;
145
+ // bail if we already have theme from context
146
+ if (parentThemeRef.current) {
147
+ return;
148
+ }
149
+ // find parent theme from closest data-iui-theme attribute
150
+ const closestRoot = (_b = (_a = rootRef.current) === null || _a === void 0 ? void 0 : _a.parentElement) === null || _b === void 0 ? void 0 : _b.closest('[data-iui-theme]');
151
+ if (!closestRoot) {
152
+ return;
153
+ }
154
+ // helper function that updates state to match data attributes from closest root
155
+ const synchronizeTheme = () => {
156
+ setParentTheme(closestRoot === null || closestRoot === void 0 ? void 0 : closestRoot.getAttribute('data-iui-theme'));
157
+ setParentHighContrastState((closestRoot === null || closestRoot === void 0 ? void 0 : closestRoot.getAttribute('data-iui-contrast')) === 'high');
158
+ };
159
+ // set theme for initial mount
160
+ synchronizeTheme();
161
+ // use mutation observers to listen to future updates to data attributes
162
+ const observer = new MutationObserver(() => synchronizeTheme());
163
+ observer.observe(closestRoot, {
164
+ attributes: true,
165
+ attributeFilter: ['data-iui-theme', 'data-iui-contrast'],
166
+ });
167
+ return () => {
168
+ observer.disconnect();
169
+ };
170
+ }, [parentThemeRef]);
171
+ return {
172
+ theme: (_b = parentContext === null || parentContext === void 0 ? void 0 : parentContext.theme) !== null && _b !== void 0 ? _b : parentThemeState,
173
+ highContrast: (_d = (_c = parentContext === null || parentContext === void 0 ? void 0 : parentContext.themeOptions) === null || _c === void 0 ? void 0 : _c.highContrast) !== null && _d !== void 0 ? _d : parentHighContrastState,
174
+ context: parentContext,
175
+ };
176
+ };
@@ -4,7 +4,7 @@
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  import * as React from 'react';
6
6
  import cx from 'classnames';
7
- import { useTheme, useMediaQuery, useMergedRefs, useIsThemeAlreadySet, } from '../utils/index.js';
7
+ import { useTheme, useMediaQuery, useMergedRefs, useIsThemeAlreadySet, useIsomorphicLayoutEffect, useLatestRef, } from '../utils/index.js';
8
8
  import { useStyles } from '../utils/hooks/useStyles.js';
9
9
  /**
10
10
  * This component provides global styles and applies theme to the entire tree
@@ -33,19 +33,17 @@ import { useStyles } from '../utils/hooks/useStyles.js';
33
33
  * </ThemeProvider>
34
34
  */
35
35
  export const ThemeProvider = React.forwardRef((props, ref) => {
36
- var _a, _b, _c, _d;
36
+ var _a, _b, _c, _d, _e;
37
37
  const { theme: themeProp, children, themeOptions = {}, includeCss = { withLayer: true }, ...rest } = props;
38
38
  const rootRef = React.useRef(null);
39
39
  const mergedRefs = useMergedRefs(rootRef, ref);
40
40
  const hasChildren = React.Children.count(children) > 0;
41
- const parentContext = React.useContext(ThemeContext);
42
- const theme = themeProp === 'inherit' ? (_a = parentContext === null || parentContext === void 0 ? void 0 : parentContext.theme) !== null && _a !== void 0 ? _a : 'light' : themeProp;
41
+ const parent = useParentThemeAndContext(rootRef);
42
+ const theme = themeProp === 'inherit' ? (_a = parent.theme) !== null && _a !== void 0 ? _a : 'light' : themeProp;
43
43
  // default inherit highContrast option from parent if also inheriting base theme
44
- (_b = themeOptions.highContrast) !== null && _b !== void 0 ? _b : (themeOptions.highContrast = themeProp === 'inherit'
45
- ? (_c = parentContext === null || parentContext === void 0 ? void 0 : parentContext.themeOptions) === null || _c === void 0 ? void 0 : _c.highContrast
46
- : undefined);
44
+ (_b = themeOptions.highContrast) !== null && _b !== void 0 ? _b : (themeOptions.highContrast = themeProp === 'inherit' ? parent.highContrast : undefined);
47
45
  const newStylesLoaded = React.useRef(false);
48
- const stylesLoaded = (_d = parentContext === null || parentContext === void 0 ? void 0 : parentContext.stylesLoaded) !== null && _d !== void 0 ? _d : newStylesLoaded;
46
+ const stylesLoaded = (_d = (_c = parent.context) === null || _c === void 0 ? void 0 : _c.stylesLoaded) !== null && _d !== void 0 ? _d : newStylesLoaded;
49
47
  const contextValue = React.useMemo(() => ({
50
48
  theme,
51
49
  themeOptions,
@@ -63,7 +61,7 @@ export const ThemeProvider = React.forwardRef((props, ref) => {
63
61
  ]);
64
62
  // if no children, then fallback to this wrapper component which calls useTheme
65
63
  if (!hasChildren) {
66
- return (React.createElement(ThemeLogicWrapper, { theme: theme, themeOptions: themeOptions !== null && themeOptions !== void 0 ? themeOptions : parentContext === null || parentContext === void 0 ? void 0 : parentContext.themeOptions }));
64
+ return (React.createElement(ThemeLogicWrapper, { theme: theme, themeOptions: themeOptions !== null && themeOptions !== void 0 ? themeOptions : (_e = parent.context) === null || _e === void 0 ? void 0 : _e.themeOptions }));
67
65
  }
68
66
  // now that we know there are children, we can render the root and provide the context value
69
67
  return (React.createElement(Root, { theme: theme, isInheritingTheme: themeProp === 'inherit', themeOptions: themeOptions, ref: mergedRefs, ...rest },
@@ -101,3 +99,49 @@ const Styles = (props) => {
101
99
  useStyles({ includeCss, document });
102
100
  return null;
103
101
  };
102
+ /**
103
+ * Returns theme information from either parent ThemeContext or by reading the closest
104
+ * data-iui-theme attribute if context is not found.
105
+ *
106
+ * Also returns the ThemeContext itself (if found).
107
+ */
108
+ const useParentThemeAndContext = (rootRef) => {
109
+ var _a, _b, _c, _d;
110
+ const parentContext = React.useContext(ThemeContext);
111
+ const [parentThemeState, setParentTheme] = React.useState(parentContext === null || parentContext === void 0 ? void 0 : parentContext.theme);
112
+ const [parentHighContrastState, setParentHighContrastState] = React.useState((_a = parentContext === null || parentContext === void 0 ? void 0 : parentContext.themeOptions) === null || _a === void 0 ? void 0 : _a.highContrast);
113
+ const parentThemeRef = useLatestRef(parentContext === null || parentContext === void 0 ? void 0 : parentContext.theme);
114
+ useIsomorphicLayoutEffect(() => {
115
+ var _a, _b;
116
+ // bail if we already have theme from context
117
+ if (parentThemeRef.current) {
118
+ return;
119
+ }
120
+ // find parent theme from closest data-iui-theme attribute
121
+ const closestRoot = (_b = (_a = rootRef.current) === null || _a === void 0 ? void 0 : _a.parentElement) === null || _b === void 0 ? void 0 : _b.closest('[data-iui-theme]');
122
+ if (!closestRoot) {
123
+ return;
124
+ }
125
+ // helper function that updates state to match data attributes from closest root
126
+ const synchronizeTheme = () => {
127
+ setParentTheme(closestRoot === null || closestRoot === void 0 ? void 0 : closestRoot.getAttribute('data-iui-theme'));
128
+ setParentHighContrastState((closestRoot === null || closestRoot === void 0 ? void 0 : closestRoot.getAttribute('data-iui-contrast')) === 'high');
129
+ };
130
+ // set theme for initial mount
131
+ synchronizeTheme();
132
+ // use mutation observers to listen to future updates to data attributes
133
+ const observer = new MutationObserver(() => synchronizeTheme());
134
+ observer.observe(closestRoot, {
135
+ attributes: true,
136
+ attributeFilter: ['data-iui-theme', 'data-iui-contrast'],
137
+ });
138
+ return () => {
139
+ observer.disconnect();
140
+ };
141
+ }, [parentThemeRef]);
142
+ return {
143
+ theme: (_b = parentContext === null || parentContext === void 0 ? void 0 : parentContext.theme) !== null && _b !== void 0 ? _b : parentThemeState,
144
+ highContrast: (_d = (_c = parentContext === null || parentContext === void 0 ? void 0 : parentContext.themeOptions) === null || _c === void 0 ? void 0 : _c.highContrast) !== null && _d !== void 0 ? _d : parentHighContrastState,
145
+ context: parentContext,
146
+ };
147
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itwin/itwinui-react",
3
- "version": "2.12.25",
3
+ "version": "2.12.26",
4
4
  "author": "Bentley Systems",
5
5
  "license": "MIT",
6
6
  "main": "cjs/index.js",