@atlaskit/app-provider
Version:
A top level provider for the Design System.
254 lines (250 loc) • 11.2 kB
JavaScript
/* 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))))));
}