UNPKG

@itwin/itwinui-react

Version:

A react component library for iTwinUI

327 lines (326 loc) 10.5 kB
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; } })(); }, []); };