diff --git a/.changeset/five-frogs-complain.md b/.changeset/five-frogs-complain.md new file mode 100644 index 00000000000..4f590aa55db --- /dev/null +++ b/.changeset/five-frogs-complain.md @@ -0,0 +1,7 @@ +--- +'@itwin/itwinui-react': minor +--- + +`ThemeProvider` will now attempt to automatically load `styles.css` if using `theme="inherit"` (or `includeCss` if using other themes). + +While applications are still advised to manually import `styles.css`, this new behavior is intended to ease the migration for applications that may be using an older version of iTwinUI but want to consume dependencies that rely on iTwinUI v3. diff --git a/packages/itwinui-react/src/core/ThemeProvider/ThemeProvider.tsx b/packages/itwinui-react/src/core/ThemeProvider/ThemeProvider.tsx index 8ada3380f54..a6c7eb4dbca 100644 --- a/packages/itwinui-react/src/core/ThemeProvider/ThemeProvider.tsx +++ b/packages/itwinui-react/src/core/ThemeProvider/ThemeProvider.tsx @@ -12,6 +12,8 @@ import { useIsomorphicLayoutEffect, useControlledState, useLatestRef, + importCss, + isJest, } from '../utils/index.js'; import type { PolymorphicForwardRefComponent } from '../utils/index.js'; import { ThemeContext } from './ThemeContext.js'; @@ -84,6 +86,16 @@ type ThemeProviderOwnProps = Pick & { * */ portalContainer?: HTMLElement; + /** + * This prop will be used to determine if `styles.css` should be automatically imported at runtime (if not already found). + * + * By default, this is enabled when using `theme='inherit'`. + * This default behavior is useful for packages that want to support incremental adoption of latest iTwinUI, + * without requiring consuming applications (that might still be using an older version) to manually import the CSS. + * + * If true or false is passed, it will override the default behavior. + */ + includeCss?: boolean; }; /** @@ -120,6 +132,7 @@ export const ThemeProvider = React.forwardRef((props, forwardedRef) => { children, themeOptions = {}, portalContainer: portalContainerProp, + includeCss = themeProp === 'inherit', ...rest } = props; @@ -158,6 +171,8 @@ export const ThemeProvider = React.forwardRef((props, forwardedRef) => { return ( + {includeCss && rootElement ? : null} + { context: parentContext, } as const; }; + +// ---------------------------------------------------------------------------- + +/** + * When `@itwin/itwinui-react/styles.css` is not imported, we will attempt to + * dynamically import it (if possible) and fallback to loading it from a CDN. + */ +const FallbackStyles = ({ root }: { root: HTMLElement }) => { + useIsomorphicLayoutEffect(() => { + // bail if styles are already loaded + if (getComputedStyle(root).getPropertyValue('--_iui-v3-loaded') === 'yes') { + return; + } + + // bail if jest because it doesn't care about CSS 🤷 + if (isJest) { + return; + } + + (async () => { + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + await import('../../../styles.css'); + } catch (error) { + console.log('Error loading styles.css locally', error); + const css = await importCss( + 'https://cdn.jsdelivr.net/npm/@itwin/itwinui-react@3/styles.css', + ); + document.adoptedStyleSheets = [ + ...document.adoptedStyleSheets, + css.default, + ]; + } + })(); + }, [root]); + + return <>; +}; diff --git a/packages/itwinui-react/src/core/utils/functions/import.ts b/packages/itwinui-react/src/core/utils/functions/import.ts index bd5dd518443..5bb69cbe9d7 100644 --- a/packages/itwinui-react/src/core/utils/functions/import.ts +++ b/packages/itwinui-react/src/core/utils/functions/import.ts @@ -2,3 +2,36 @@ * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ + +/** + * Wrapper around native CSS module scripts (import attributes) for dynamic imports. + * In unsupported browsers, it gracefully degrades to import assertions, and then `fetch`. + * + * Returns a constructable CSSStyleSheet object that can be adopted. + * + * @see https://web.dev/articles/css-module-scripts + * @see https://github.com/tc39/proposal-import-attributes + */ +export const importCss = async ( + url: string, +): Promise<{ default: CSSStyleSheet }> => { + try { + return await new Function( + `return import("${url}", { with: { type: "css" } })`, + )(); + } catch { + try { + return await new Function( + `return import("${url}", { assert: { type: "css" } })`, + )(); + } catch { + return await fetch(url) + .then((res) => res.text()) + .then((cssText) => { + const stylesheet = new CSSStyleSheet(); + stylesheet.replaceSync(cssText); + return { default: stylesheet }; + }); + } + } +}; diff --git a/packages/itwinui-react/src/styles.js/styles.module.css b/packages/itwinui-react/src/styles.js/styles.module.css index 578f02f0f35..f8ee6062d14 100644 --- a/packages/itwinui-react/src/styles.js/styles.module.css +++ b/packages/itwinui-react/src/styles.js/styles.module.css @@ -7,3 +7,9 @@ @import '@itwin/itwinui-variables' layer(itwinui.v3); @import '@itwin/itwinui-css' layer(itwinui.v3); + +@layer itwinui.v3 { + .iui-root { + --_iui-v3-loaded: yes; + } +} diff --git a/playgrounds/next/next.config.mjs b/playgrounds/next/next.config.mjs index 8de48876ce8..a38122b5160 100644 --- a/playgrounds/next/next.config.mjs +++ b/playgrounds/next/next.config.mjs @@ -4,6 +4,7 @@ const nextConfig = { reactStrictMode: true, swcMinify: true, + transpilePackages: ['@itwin/itwinui-react'], }; export default nextConfig;