@itwin/itwinui-react
Version:
A react component library for iTwinUI
327 lines (326 loc) • 10.5 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';
let versionWithoutDots = meta.version.replace(/\./g, '');
let OwnerDocumentContext = React.createContext(void 0);
export const ThemeProvider = React.forwardRef((props, forwardedRef) => {
var _themeOptions, _themeOptions1;
let {
theme: themeProp = 'inherit',
children,
themeOptions = {},
portalContainer: portalContainerProp,
includeCss = 'inherit' === themeProp,
future: futureProp = {},
...rest
} = props;
useInertPolyfill();
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);
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: futureProp,
},
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,
},
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 themeBridge = useFutureFlag('themeBridge');
return React.createElement(
Box,
{
className: cx(
'iui-root',
{
'iui-root-background': shouldApplyBackground,
},
className,
),
'data-iui-theme': shouldApplyDark ? 'dark' : 'light',
'data-iui-contrast': shouldApplyHC ? 'high' : 'default',
'data-iui-bridge': themeBridge ? 'true' : void 0,
ref: forwardedRef,
...rest,
},
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,
},
'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 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));
};
let useInertPolyfill = () => {
let loaded = React.useRef(false);
let modulePath =
'https://cdn.jsdelivr.net/npm/wicg-inert@3.1.2/dist/inert.min.js';
React.useEffect(() => {
(async () => {
if (
!HTMLElement.prototype.hasOwnProperty('inert') &&
!loaded.current &&
!isUnitTest
) {
await new Function('url', 'return import(url)')(modulePath);
loaded.current = true;
}
})();
}, []);
};