UNPKG

@atlaskit/app-provider

Version:

A top level provider for the Design System.

254 lines (250 loc) • 11.2 kB
/* index.tsx generated by @compiled/babel-plugin v3.0.2 */ import _extends from "@babel/runtime/helpers/extends"; import "./index.compiled.css"; import { ax, ix } from "@compiled/react/runtime"; import React, { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react'; import { bind } from 'bind-event-listener'; import { fg } from '@atlaskit/platform-feature-flags/fg'; import { SUBTREE_THEME_ATTRIBUTE } from '@atlaskit/tokens/constants'; import { CUSTOM_THEME_SCOPE_ATTRIBUTE, getThemeAndOverrides } from '@atlaskit/tokens/custom-theme-overrides'; import { getThemeHtmlAttrs } from '@atlaskit/tokens/get-theme-html-attrs'; import { setGlobalTheme } from '@atlaskit/tokens/set-global-theme'; import { useIsAppProviderThemingEnabled } from '../use-is-app-provider-theming-enabled'; import { useIsInsideAppProvider } from '../use-is-inside-app-provider'; import { ColorModeContext } from './context/color-mode'; import { InsideThemeProviderContext } from './context/inside-theme-provider'; import { SetColorModeContext } from './context/set-color-mode-context'; import { SetThemeContext, ThemeContext } from './context/theme'; import { useIsInsideThemeProvider } from './hooks/use-is-inside-theme-provider'; import { getInlineThemeStyles } from './utils/get-inline-theme-styles'; import { loadAndMountThemes } from './utils/load-and-mount-themes'; const defaultThemeSettings = { dark: 'dark', light: 'light', shape: 'shape', spacing: 'spacing', typography: 'typography' }; const isMatchMediaAvailable = typeof window !== 'undefined' && 'matchMedia' in window; const prefersDarkModeMql = isMatchMediaAvailable ? window.matchMedia('(prefers-color-scheme: dark)') : undefined; // TODO: currently 'auto' color mode will always return 'light' in SSR. // Additional work required: https://product-fabric.atlassian.net/browse/DSP-9781 function getReconciledColorMode(colorMode) { if (colorMode === 'auto') { return prefersDarkModeMql !== null && prefersDarkModeMql !== void 0 && prefersDarkModeMql.matches ? 'dark' : 'light'; } return colorMode; } const contentStyles = { body: "_1e0c1bgi" }; /** * __Theme provider__ * * Provides global theming configuration. */ export function ThemeProvider({ children, defaultColorMode = 'auto', defaultTheme }) { const [chosenColorMode, setChosenColorMode] = useState(defaultColorMode); const [reconciledColorMode, setReconciledColorMode] = useState(getReconciledColorMode(defaultColorMode)); const isInsideAppProvider = useIsInsideAppProvider(); const isAppProviderThemingEnabled = useIsAppProviderThemingEnabled(); const isInsideThemeProvider = useIsInsideThemeProvider(); /** * A top-level ThemeProvider is detected by being the first ThemeProvider inside an AppProvider. * * This will not use sub-tree theming but instead set the global theme state using the * `@atlaskit/tokens` APIs, as it's required for styling root `html` and `body` elements * for compatibility with `@atlaskit/css-reset`. * * In the future we could consider moving away from DOM mutations and require AppProvider to wrap * `html` in order to apply global theme state, which would allow a more consistent approach to * theme loading. */ const isRootThemeProvider = isInsideAppProvider && !isInsideThemeProvider && isAppProviderThemingEnabled; /** * Each provider owns its own override stylesheet, so nested providers don't overwrite or remove * each other's overrides. `useId` is stable between the server render and hydration. * * Subtree providers also scope their override selector to their own element. Otherwise every * subtree using the same custom theme would match every provider's overrides, and the last * stylesheet in the document would win. The root provider targets `html`, which subtrees * inherit from. */ const customThemeOverridesStyleId = useId(); const { theme: normalizedDefaultTheme, inlineStyles } = getThemeAndOverrides(defaultTheme, isRootThemeProvider ? undefined : customThemeOverridesStyleId); const [theme, setTheme] = useState(() => ({ ...defaultThemeSettings, ...normalizedDefaultTheme })); const [hasHoistedInlineThemeStyles, setHasHoistedInlineThemeStyles] = useState(false); const setColorMode = useCallback(colorMode => { setChosenColorMode(colorMode); setReconciledColorMode(getReconciledColorMode(colorMode)); }, []); const setPartialTheme = useCallback(nextTheme => { setTheme(theme => ({ ...theme, ...nextTheme })); }, []); const lastSetGlobalThemePromiseRef = useRef(null); /** * Custom theme overrides apply regardless of `platform-static-theme-loading`: that gate only * controls how the theme stylesheets themselves are loaded. */ const inlineThemeStylesOverrides = inlineStyles; useLayoutEffect(() => { return () => { var _document$head$queryS; (_document$head$queryS = document.head.querySelector(`style[data-theme-overrides="${customThemeOverridesStyleId}"]`)) === null || _document$head$queryS === void 0 ? void 0 : _document$head$queryS.remove(); }; }, [customThemeOverridesStyleId]); useLayoutEffect(() => { const isStaticThemeLoadingEnabled = fg('platform-static-theme-loading'); if (isStaticThemeLoadingEnabled && !hasHoistedInlineThemeStyles) { getInlineThemeStyles(theme, chosenColorMode).forEach(({ id, css }) => { if (document.head.querySelector(`style[data-theme="${id}"]`)) { return; } const style = document.createElement('style'); style.dataset.theme = id; style.textContent = css; document.head.appendChild(style); }); } const existingOverrideStyle = document.head.querySelector(`style[data-theme-overrides="${customThemeOverridesStyleId}"]`); if (inlineThemeStylesOverrides) { if (existingOverrideStyle) { existingOverrideStyle.textContent = inlineThemeStylesOverrides; } else { const style = document.createElement('style'); style.dataset.themeOverrides = customThemeOverridesStyleId; style.textContent = inlineThemeStylesOverrides; document.head.appendChild(style); } } else { existingOverrideStyle === null || existingOverrideStyle === void 0 ? void 0 : existingOverrideStyle.remove(); } // Only re-render to drop the inline styles when there are any to drop. if (!hasHoistedInlineThemeStyles && (isStaticThemeLoadingEnabled || inlineThemeStylesOverrides)) { setHasHoistedInlineThemeStyles(true); } }, [chosenColorMode, customThemeOverridesStyleId, hasHoistedInlineThemeStyles, inlineThemeStylesOverrides, theme]); useEffect(() => { if (isRootThemeProvider) { /** * We need to wait for any previous `setGlobalTheme` calls to finish before calling it again. * This is to prevent race conditions as `setGlobalTheme` is async and mutates the DOM (e.g. sets the * `data-color-mode` attribute on the root element). * * Since we can't safely abort the `setGlobalTheme` execution, we need to wait for it to properly finish before * applying the new theme. * * Without this, we can end up in the following scenario: * 1. app loads with the default 'light' theme, kicking off `setGlobalTheme` * 2. app switches to 'dark' theme after retrieving value persisted in local storage, calling `setGlobalTheme` again * 3. `setGlobalTheme` function execution for `dark` finishes before the initial `light` execution * 4. `setGlobalTheme` function execution for `light` then finishes, resulting in the 'light' theme being applied. */ const cleanupLastFnCall = async () => { if (lastSetGlobalThemePromiseRef.current) { const unbindFn = await lastSetGlobalThemePromiseRef.current; unbindFn(); lastSetGlobalThemePromiseRef.current = null; } }; const safelySetGlobalTheme = async () => { await cleanupLastFnCall(); const promise = setGlobalTheme({ ...theme, colorMode: reconciledColorMode }); lastSetGlobalThemePromiseRef.current = promise; }; safelySetGlobalTheme(); return function cleanup() { cleanupLastFnCall(); }; } // For other theme providers (whether outside AppProvider or nested inside a ThemeProvider), // we treat them as sub-tree themes that do not load global theme state. loadAndMountThemes(theme); }, [isInsideAppProvider, isInsideThemeProvider, isRootThemeProvider, reconciledColorMode, theme]); useEffect(() => { if (!prefersDarkModeMql) { return; } const unbindListener = bind(prefersDarkModeMql, { type: 'change', listener: event => { if (chosenColorMode === 'auto') { setReconciledColorMode(event.matches ? 'dark' : 'light'); } } }); return unbindListener; }, [chosenColorMode]); const attrs = { ...getThemeHtmlAttrs({ ...theme, colorMode: reconciledColorMode }), [SUBTREE_THEME_ATTRIBUTE]: true, // Matches this provider's scoped override selector. Only set when there are overrides. ...(inlineThemeStylesOverrides ? { [CUSTOM_THEME_SCOPE_ATTRIBUTE]: customThemeOverridesStyleId } : {}) }; const inlineThemeStyles = fg('platform-static-theme-loading') && !hasHoistedInlineThemeStyles ? getInlineThemeStyles(theme, chosenColorMode) : []; return /*#__PURE__*/React.createElement(InsideThemeProviderContext.Provider, { value: true }, /*#__PURE__*/React.createElement(ColorModeContext.Provider, { value: reconciledColorMode }, /*#__PURE__*/React.createElement(SetColorModeContext.Provider, { value: setColorMode }, /*#__PURE__*/React.createElement(ThemeContext.Provider, { value: theme }, /*#__PURE__*/React.createElement(SetThemeContext.Provider, { value: setPartialTheme }, !isRootThemeProvider ? /*#__PURE__*/React.createElement("div", _extends({}, attrs, { className: ax([contentStyles.body]) }), !hasHoistedInlineThemeStyles && inlineThemeStylesOverrides && /*#__PURE__*/ // eslint-disable-next-line @atlaskit/ui-styling-standard/no-global-styles React.createElement("style", { "data-theme-overrides": customThemeOverridesStyleId }, inlineThemeStylesOverrides), inlineThemeStyles.map(({ id, css }) => /*#__PURE__*/ // eslint-disable-next-line @atlaskit/ui-styling-standard/no-global-styles React.createElement("style", { "data-theme": id, key: id }, css)), children) : /*#__PURE__*/React.createElement(React.Fragment, null, !hasHoistedInlineThemeStyles && inlineThemeStylesOverrides && /*#__PURE__*/ // eslint-disable-next-line @atlaskit/ui-styling-standard/no-global-styles React.createElement("style", { "data-theme-overrides": customThemeOverridesStyleId }, inlineThemeStylesOverrides), inlineThemeStyles.map(({ id, css }) => /*#__PURE__*/ // eslint-disable-next-line @atlaskit/ui-styling-standard/no-global-styles React.createElement("style", { "data-theme": id, key: id }, css)), children)))))); }