@itwin/itwinui-react
Version:
A react component library for iTwinUI
368 lines (367 loc) • 12.4 kB
JavaScript
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import cx from 'classnames';
import {
useMediaQuery,
useMergedRefs,
Box,
useLayoutEffect,
useLatestRef,
importCss,
isUnitTest,
HydrationProvider,
useHydration,
PortalContainerContext,
useId,
FutureFlagsProvider,
useFutureFlag,
} from '../../utils/index.js';
import { ThemeContext } from './ThemeContext.js';
import { ToastProvider, Toaster } from '../Toast/Toaster.js';
import { meta } from '../../utils/meta.js';
import { styles } from '../../styles.js';
let versionWithoutDots = meta.version.replace(/\./g, '');
let OwnerDocumentContext = React.createContext(void 0);
export const ThemeProvider = React.forwardRef((props, forwardedRef) => {
var _themeOptions, _themeOptions1, _themeOptions2;
let {
theme: themeProp = 'inherit',
children,
themeOptions = {},
portalContainer: portalContainerProp,
includeCss = 'inherit' === themeProp,
future: futureProp = {},
...rest
} = props;
let future;
future =
true === futureProp
? {
themeBridge: true,
synchronizeThemeToRoot: true,
ToggleSwitch: {
consistentPropsSpread: true,
},
}
: futureProp;
let [rootElement, setRootElement] = React.useState(null);
let parent = useParentThemeAndContext(rootElement);
let theme = 'inherit' === themeProp ? parent.theme || 'light' : themeProp;
(_themeOptions = themeOptions).applyBackground ??
(_themeOptions.applyBackground = !parent.theme);
(_themeOptions1 = themeOptions).highContrast ??
(_themeOptions1.highContrast =
'inherit' === themeProp ? parent.highContrast : void 0);
(_themeOptions2 = themeOptions).synchronizeThemeToRoot ??
(_themeOptions2.synchronizeThemeToRoot = future.synchronizeThemeToRoot);
let portalContainerFromParent = React.useContext(PortalContainerContext);
let themeContextValue = React.useMemo(
() => ({
theme,
themeOptions,
}),
[theme, JSON.stringify(themeOptions)],
);
let [portalContainer, setPortalContainer] = React.useState(
portalContainerProp || null,
);
return React.createElement(
FutureFlagsProvider,
{
value: future,
},
React.createElement(
PortalContainerContext.Provider,
{
value: portalContainer,
},
React.createElement(
HydrationProvider,
null,
React.createElement(
ThemeContext.Provider,
{
value: themeContextValue,
},
React.createElement(
ToastProvider,
{
inherit: 'inherit' === themeProp && !portalContainerProp,
},
includeCss && rootElement
? React.createElement(FallbackStyles, {
root: rootElement,
})
: null,
React.createElement(
MainRoot,
{
theme: theme,
themeOptions: themeOptions,
ref: useMergedRefs(
forwardedRef,
setRootElement,
useIuiDebugRef,
),
...rest,
},
React.createElement(
FutureFlagsProvider,
{
value: {
...future,
synchronizeThemeToRoot: false,
},
},
children,
),
React.createElement(PortalContainer, {
theme: theme,
themeOptions: themeOptions,
portalContainerProp: portalContainerProp,
portalContainerFromParent: portalContainerFromParent,
setPortalContainer: setPortalContainer,
isInheritingTheme: 'inherit' === themeProp,
}),
),
),
),
),
),
);
});
if ('development' === process.env.NODE_ENV)
ThemeProvider.displayName = 'ThemeProvider';
let MainRoot = React.forwardRef((props, forwardedRef) => {
let [ownerDocument, setOwnerDocument] = React.useState(void 0);
let findOwnerDocumentFromRef = React.useCallback(
(el) => {
if (el && el.ownerDocument !== ownerDocument)
setOwnerDocument(el.ownerDocument);
},
[ownerDocument, setOwnerDocument],
);
return React.createElement(
OwnerDocumentContext.Provider,
{
value: ownerDocument,
},
React.createElement(Root, {
...props,
ref: useMergedRefs(findOwnerDocumentFromRef, forwardedRef),
}),
);
});
let Root = React.forwardRef((props, forwardedRef) => {
let { theme, children, themeOptions, className, ...rest } = props;
let prefersDark = useMediaQuery('(prefers-color-scheme: dark)');
let prefersHighContrast = useMediaQuery('(prefers-contrast: more)');
let shouldApplyDark = 'dark' === theme || ('os' === theme && prefersDark);
let shouldApplyHC = themeOptions?.highContrast ?? prefersHighContrast;
let shouldApplyBackground = themeOptions?.applyBackground;
let shouldSynchronizeTheme = themeOptions?.synchronizeThemeToRoot;
let colorScheme = shouldApplyDark ? 'dark' : 'light';
let contrast = shouldApplyHC ? 'high' : 'default';
let themeBridge = useFutureFlag('themeBridge');
return React.createElement(
Box,
{
className: cx(
'iui-root',
{
'iui-root-background': shouldApplyBackground,
},
className,
),
'data-iui-theme': colorScheme,
'data-iui-contrast': contrast,
'data-iui-bridge': themeBridge ? 'true' : void 0,
ref: forwardedRef,
...rest,
},
shouldSynchronizeTheme &&
React.createElement(SynchronizeTheme, {
theme: colorScheme,
contrast: contrast,
}),
children,
);
});
let useParentThemeAndContext = (rootElement) => {
let parentContext = React.useContext(ThemeContext);
let [parentThemeState, setParentTheme] = React.useState(parentContext?.theme);
let [parentHighContrastState, setParentHighContrastState] = React.useState(
parentContext?.themeOptions?.highContrast,
);
let parentThemeRef = useLatestRef(parentContext?.theme);
useLayoutEffect(() => {
if (parentThemeRef.current) return;
let closestRoot = rootElement?.parentElement?.closest('[data-iui-theme]');
if (!closestRoot) return;
let synchronizeTheme = () => {
setParentTheme(closestRoot?.getAttribute('data-iui-theme'));
setParentHighContrastState(
closestRoot?.getAttribute('data-iui-contrast') === 'high',
);
};
synchronizeTheme();
let observer = new MutationObserver(() => synchronizeTheme());
observer.observe(closestRoot, {
attributes: true,
attributeFilter: ['data-iui-theme', 'data-iui-contrast'],
});
return () => {
observer.disconnect();
};
}, [rootElement, parentThemeRef]);
return {
theme: parentContext?.theme ?? parentThemeState,
highContrast:
parentContext?.themeOptions?.highContrast ?? parentHighContrastState,
context: parentContext,
};
};
let PortalContainer = React.memo(
({
portalContainerProp,
portalContainerFromParent,
setPortalContainer,
isInheritingTheme,
theme,
themeOptions,
}) => {
let ownerDocument = React.useContext(OwnerDocumentContext);
let shouldSetupPortalContainer =
!portalContainerProp &&
(!isInheritingTheme ||
!portalContainerFromParent ||
(!!ownerDocument &&
portalContainerFromParent.ownerDocument !== ownerDocument));
let id = useId();
React.useEffect(() => {
if (shouldSetupPortalContainer) return;
let portalTarget = portalContainerProp || portalContainerFromParent;
if (portalTarget) setPortalContainer(portalTarget);
}, [
portalContainerProp,
portalContainerFromParent,
shouldSetupPortalContainer,
setPortalContainer,
]);
let isHydrated = 'hydrated' === useHydration();
if (!isHydrated) return null;
if (shouldSetupPortalContainer && ownerDocument)
return ReactDOM.createPortal(
React.createElement(
Root,
{
theme: theme,
themeOptions: {
...themeOptions,
applyBackground: false,
synchronizeThemeToRoot: false,
},
'data-iui-portal': true,
style: {
display: 'contents',
},
ref: setPortalContainer,
id: id,
},
React.createElement(Toaster, null),
),
ownerDocument.body,
);
if (portalContainerProp)
return ReactDOM.createPortal(
React.createElement(Toaster, null),
portalContainerProp,
);
return null;
},
);
let SynchronizeTheme = ({ theme, contrast }) => {
let ownerDocument = React.useContext(OwnerDocumentContext);
let themeBridge = useFutureFlag('themeBridge');
useLayoutEffect(() => {
if (!ownerDocument?.body) return;
let body = ownerDocument.body;
let previousTheme = body.getAttribute('data-iui-theme');
let previousContrast = body.getAttribute('data-iui-contrast');
let previousBridge = body.getAttribute('data-iui-bridge');
let previouslyHadRootClass = body.classList.contains(styles['iui-root']);
body.classList.toggle(styles['iui-root'], true);
synchronizeAttribute(body, 'data-iui-theme', theme);
synchronizeAttribute(body, 'data-iui-contrast', contrast);
synchronizeAttribute(body, 'data-iui-bridge', themeBridge ? 'true' : null);
return () => {
if (!previouslyHadRootClass) body.classList.remove(styles['iui-root']);
synchronizeAttribute(body, 'data-iui-theme', previousTheme);
synchronizeAttribute(body, 'data-iui-contrast', previousContrast);
synchronizeAttribute(body, 'data-iui-bridge', previousBridge);
};
}, [contrast, ownerDocument, theme, themeBridge]);
return null;
};
let synchronizeAttribute = (element, attributeName, value) => {
if (null == value) return void element.removeAttribute(attributeName);
element.setAttribute(attributeName, value);
};
let FallbackStyles = ({ root }) => {
useLayoutEffect(() => {
if (
'yes' ===
getComputedStyle(root).getPropertyValue(`--_iui-v${versionWithoutDots}`)
)
return;
if (isUnitTest) return;
(async () => {
try {
await import('../../../styles.css');
} catch (error) {
console.log('Error loading styles.css locally', error);
let css = await importCss(
`https://cdn.jsdelivr.net/npm/@itwin/itwinui-react@${meta.version}/styles.css`,
);
document.adoptedStyleSheets = [
...document.adoptedStyleSheets,
css.default,
];
}
})();
}, [root]);
return React.createElement(React.Fragment, null);
};
let useIuiDebugRef = () => {
var _globalThis;
let _globalThis1 = globalThis;
(_globalThis = _globalThis1).__iui ||
(_globalThis.__iui = {
versions: new Set(),
});
if ('development' === process.env.NODE_ENV && !isUnitTest)
_globalThis1.__iui.versions.add = (version) => {
Set.prototype.add.call(_globalThis1.__iui.versions, version);
if (_globalThis1.__iui.versions.size > 1) {
_globalThis1.__iui._shouldWarn = true;
if (_globalThis1.__iui._warnTimeout)
clearTimeout(_globalThis1.__iui._warnTimeout);
_globalThis1.__iui._warnTimeout = setTimeout(() => {
if (_globalThis1.__iui._shouldWarn) {
console.warn(
"Multiple versions of iTwinUI were detected on this page. This can lead to unexpected behavior and duplicated code in the bundle. Make sure you're using a single iTwinUI instance across your app. https://github.com/iTwin/iTwinUI/wiki/Version-conflicts",
);
console.groupCollapsed('iTwinUI versions detected:');
let versionsTable = [];
_globalThis1.__iui.versions.forEach((version) => {
versionsTable.push(JSON.parse(version));
});
console.table(versionsTable);
console.groupEnd();
_globalThis1.__iui._shouldWarn = false;
}
}, 3000);
}
};
_globalThis1.__iui.versions.add(JSON.stringify(meta));
};