refactor: mitigate FOUC when applying Prism theme

This commit is contained in:
Alexey Pyltsyn 2022-05-08 13:59:39 +03:00
parent 7c9892888d
commit 3094b9f4fa
No known key found for this signature in database
GPG key ID: 43C3EEBF02122A7A
9 changed files with 48 additions and 45 deletions

View file

@ -15,6 +15,8 @@ import rtlcss from 'rtlcss';
import {readDefaultCodeTranslationMessages} from '@docusaurus/theme-translations'; import {readDefaultCodeTranslationMessages} from '@docusaurus/theme-translations';
import type {Options} from '@docusaurus/theme-classic'; import type {Options} from '@docusaurus/theme-classic';
import type webpack from 'webpack'; import type webpack from 'webpack';
import type {PrismTheme} from 'prism-react-renderer';
import type {CSSProperties} from 'react';
const requireFromDocusaurusCore = createRequire( const requireFromDocusaurusCore = createRequire(
require.resolve('@docusaurus/core/package.json'), require.resolve('@docusaurus/core/package.json'),
@ -22,6 +24,22 @@ const requireFromDocusaurusCore = createRequire(
const ContextReplacementPlugin: typeof webpack.ContextReplacementPlugin = const ContextReplacementPlugin: typeof webpack.ContextReplacementPlugin =
requireFromDocusaurusCore('webpack/lib/ContextReplacementPlugin'); requireFromDocusaurusCore('webpack/lib/ContextReplacementPlugin');
const getPrismCssVariables = (prismTheme: PrismTheme): CSSProperties => {
const mapping: {[name: keyof PrismTheme['plain']]: string} = {
color: '--prism-color',
backgroundColor: '--prism-background-color',
};
const properties: {[key: string]: string} = {};
Object.entries(prismTheme.plain).forEach(([key, value]) => {
const varName = mapping[key];
if (varName && typeof value === 'string') {
properties[varName] = value;
}
});
return properties;
};
// Need to be inlined to prevent dark mode FOUC // Need to be inlined to prevent dark mode FOUC
// Make sure the key is the same as the one in `/theme/hooks/useTheme.js` // Make sure the key is the same as the one in `/theme/hooks/useTheme.js`
const ThemeStorageKey = 'theme'; const ThemeStorageKey = 'theme';
@ -103,10 +121,14 @@ export default function themeClassic(
const { const {
announcementBar, announcementBar,
colorMode, colorMode,
prism: {additionalLanguages}, prism: {additionalLanguages, theme, darkTheme},
} = themeConfig; } = themeConfig;
const {customCss} = options ?? {}; const {customCss} = options ?? {};
const {direction} = localeConfigs[currentLocale]!; const {direction} = localeConfigs[currentLocale]!;
const prismBaseStyles = {
':root': getPrismCssVariables(theme),
'[data-theme="dark"]': getPrismCssVariables(darkTheme),
};
return { return {
name: 'docusaurus-theme-classic', name: 'docusaurus-theme-classic',
@ -201,6 +223,17 @@ ${noFlashColorMode(colorMode)}
${announcementBar ? AnnouncementBarInlineJavaScript : ''} ${announcementBar ? AnnouncementBarInlineJavaScript : ''}
`, `,
}, },
{
tagName: 'style',
innerHTML: Object.entries(prismBaseStyles)
.map(
([selector, properties]) =>
`${selector} {${Object.entries(properties)
.map(([name, value]) => `${name}:${value};`)
.join('')}}`,
)
.join(' '),
},
], ],
}; };
}, },

View file

@ -7,24 +7,17 @@
import React, {type ComponentProps} from 'react'; import React, {type ComponentProps} from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import { import {ThemeClassNames} from '@docusaurus/theme-common';
usePrismTheme,
getPrismCssVariables,
ThemeClassNames,
} from '@docusaurus/theme-common';
import styles from './styles.module.css'; import styles from './styles.module.css';
export default function CodeBlockContainer<T extends 'div' | 'pre'>({ export default function CodeBlockContainer<T extends 'div' | 'pre'>({
as: As, as: As,
...props ...props
}: {as: T} & ComponentProps<T>): JSX.Element { }: {as: T} & ComponentProps<T>): JSX.Element {
const prismTheme = usePrismTheme();
const prismCssVariables = getPrismCssVariables(prismTheme);
return ( return (
<As <As
// Polymorphic components are hard to type, without `oneOf` generics // Polymorphic components are hard to type, without `oneOf` generics
{...(props as never)} {...(props as never)}
style={prismCssVariables}
className={clsx( className={clsx(
props.className, props.className,
styles.codeBlockContainer, styles.codeBlockContainer,

View file

@ -9,6 +9,7 @@ import React from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import type {Props} from '@theme/CodeBlock/Line'; import type {Props} from '@theme/CodeBlock/Line';
import styles from './styles.module.css'; import styles from './styles.module.css';
import useIsBrowser from '@docusaurus/useIsBrowser';
export default function CodeBlockLine({ export default function CodeBlockLine({
line, line,
@ -17,6 +18,8 @@ export default function CodeBlockLine({
getLineProps, getLineProps,
getTokenProps, getTokenProps,
}: Props): JSX.Element { }: Props): JSX.Element {
const isBrowser = useIsBrowser();
if (line.length === 1 && line[0]!.content === '\n') { if (line.length === 1 && line[0]!.content === '\n') {
line[0]!.content = ''; line[0]!.content = '';
} }
@ -27,11 +30,15 @@ export default function CodeBlockLine({
}); });
const lineTokens = line.map((token, key) => ( const lineTokens = line.map((token, key) => (
<span key={key} {...getTokenProps({token, key})} /> <span
key={key}
{...getTokenProps({token, key})}
{...(!isBrowser && {style: undefined})}
/>
)); ));
return ( return (
<span {...lineProps}> <span {...lineProps} {...(!isBrowser && {style: undefined})}>
{showLineNumbers ? ( {showLineNumbers ? (
<> <>
<span className={styles.codeLineNumber} /> <span className={styles.codeLineNumber} />

View file

@ -6,7 +6,6 @@
*/ */
import React, {isValidElement, type ReactNode} from 'react'; import React, {isValidElement, type ReactNode} from 'react';
import useIsBrowser from '@docusaurus/useIsBrowser';
import type {Props} from '@theme/CodeBlock'; import type {Props} from '@theme/CodeBlock';
import ElementContent from '@theme/CodeBlock/Content/Element'; import ElementContent from '@theme/CodeBlock/Content/Element';
import StringContent from '@theme/CodeBlock/Content/String'; import StringContent from '@theme/CodeBlock/Content/String';
@ -29,17 +28,8 @@ export default function CodeBlock({
children: rawChildren, children: rawChildren,
...props ...props
}: Props): JSX.Element { }: Props): JSX.Element {
// The Prism theme on SSR is always the default theme but the site theme can
// be in a different mode. React hydration doesn't update DOM styles that come
// from SSR. Hence force a re-render after mounting to apply the current
// relevant styles.
const isBrowser = useIsBrowser();
const children = maybeStringifyChildren(rawChildren); const children = maybeStringifyChildren(rawChildren);
const CodeBlockComp = const CodeBlockComp =
typeof children === 'string' ? StringContent : ElementContent; typeof children === 'string' ? StringContent : ElementContent;
return ( return <CodeBlockComp {...props}>{children as string}</CodeBlockComp>;
<CodeBlockComp key={String(isBrowser)} {...props}>
{children as string}
</CodeBlockComp>
);
} }

View file

@ -44,6 +44,7 @@ export const DEFAULT_CONFIG = {
prism: { prism: {
additionalLanguages: [], additionalLanguages: [],
theme: defaultPrismTheme, theme: defaultPrismTheme,
darkTheme: defaultPrismTheme,
magicComments: [ magicComments: [
{ {
className: 'theme-code-block-highlighted-line', className: 'theme-code-block-highlighted-line',

View file

@ -16,9 +16,7 @@ import {useThemeConfig} from '../utils/useThemeConfig';
export function usePrismTheme(): PrismTheme { export function usePrismTheme(): PrismTheme {
const {prism} = useThemeConfig(); const {prism} = useThemeConfig();
const {colorMode} = useColorMode(); const {colorMode} = useColorMode();
const lightModeTheme = prism.theme; const prismTheme = colorMode === 'dark' ? prism.darkTheme : prism.theme;
const darkModeTheme = prism.darkTheme || lightModeTheme;
const prismTheme = colorMode === 'dark' ? darkModeTheme : lightModeTheme;
return prismTheme; return prismTheme;
} }

View file

@ -35,7 +35,6 @@ export {
parseLanguage, parseLanguage,
parseLines, parseLines,
containsLineNumbers, containsLineNumbers,
getPrismCssVariables,
} from './utils/codeBlockUtils'; } from './utils/codeBlockUtils';
export { export {

View file

@ -6,8 +6,6 @@
*/ */
import rangeParser from 'parse-numeric-range'; import rangeParser from 'parse-numeric-range';
import type {PrismTheme} from 'prism-react-renderer';
import type {CSSProperties} from 'react';
const codeBlockTitleRegex = /title=(?<quote>["'])(?<title>.*?)\1/; const codeBlockTitleRegex = /title=(?<quote>["'])(?<title>.*?)\1/;
const metastringLinesRangeRegex = /\{(?<range>[\d,-]+)\}/; const metastringLinesRangeRegex = /\{(?<range>[\d,-]+)\}/;
@ -231,19 +229,3 @@ export function parseLines(
}); });
return {lineClassNames, code}; return {lineClassNames, code};
} }
export function getPrismCssVariables(prismTheme: PrismTheme): CSSProperties {
const mapping: {[name: keyof PrismTheme['plain']]: string} = {
color: '--prism-color',
backgroundColor: '--prism-background-color',
};
const properties: {[key: string]: string} = {};
Object.entries(prismTheme.plain).forEach(([key, value]) => {
const varName = mapping[key];
if (varName && typeof value === 'string') {
properties[varName] = value;
}
});
return properties;
}

View file

@ -55,7 +55,7 @@ export type AnnouncementBarConfig = {
export type PrismConfig = { export type PrismConfig = {
theme: PrismTheme; theme: PrismTheme;
darkTheme?: PrismTheme; darkTheme: PrismTheme;
defaultLanguage?: string; defaultLanguage?: string;
additionalLanguages: string[]; additionalLanguages: string[];
magicComments: MagicCommentConfig[]; magicComments: MagicCommentConfig[];