From ef627f813b6b65b3b7134a4dc4bd589a11ead38e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Thu, 16 May 2024 18:14:34 +0200 Subject: [PATCH] fix(theme): fix announcement bar layout shift due to missing storage key namespace (#10144) --- .../docusaurus-theme-classic/src/index.ts | 110 ++--------------- .../src/inlineScripts.ts | 114 ++++++++++++++++++ .../src/contexts/announcementBar.tsx | 10 +- 3 files changed, 126 insertions(+), 108 deletions(-) create mode 100644 packages/docusaurus-theme-classic/src/inlineScripts.ts diff --git a/packages/docusaurus-theme-classic/src/index.ts b/packages/docusaurus-theme-classic/src/index.ts index f366c5a4a318..9ffb4e3e87db 100644 --- a/packages/docusaurus-theme-classic/src/index.ts +++ b/packages/docusaurus-theme-classic/src/index.ts @@ -10,7 +10,12 @@ import {createRequire} from 'module'; import rtlcss from 'rtlcss'; import {readDefaultCodeTranslationMessages} from '@docusaurus/theme-translations'; import {getTranslationFiles, translateThemeConfig} from './translations'; -import type {LoadContext, Plugin, SiteStorage} from '@docusaurus/types'; +import { + getThemeInlineScript, + getAnnouncementBarInlineScript, + DataAttributeQueryStringInlineJavaScript, +} from './inlineScripts'; +import type {LoadContext, Plugin} from '@docusaurus/types'; import type {ThemeConfig} from '@docusaurus/theme-common'; import type {Plugin as PostCssPlugin} from 'postcss'; import type {PluginOptions} from '@docusaurus/theme-classic'; @@ -23,105 +28,6 @@ const ContextReplacementPlugin = requireFromDocusaurusCore( 'webpack/lib/ContextReplacementPlugin', ) as typeof webpack.ContextReplacementPlugin; -// Support for ?docusaurus-theme=dark -const ThemeQueryStringKey = 'docusaurus-theme'; -// Support for ?docusaurus-data-mode=embed&docusaurus-data-myAttr=42 -const DataQueryStringPrefixKey = 'docusaurus-data-'; - -const noFlashColorMode = ({ - colorMode: {defaultMode, respectPrefersColorScheme}, - siteStorage, -}: { - colorMode: ThemeConfig['colorMode']; - siteStorage: SiteStorage; -}) => { - // Need to be inlined to prevent dark mode FOUC - // Make sure the key is the same as the one in the color mode React context - // Currently defined in: `docusaurus-theme-common/src/contexts/colorMode.tsx` - const themeStorageKey = `theme${siteStorage.namespace}`; - - /* language=js */ - return `(function() { - var defaultMode = '${defaultMode}'; - var respectPrefersColorScheme = ${respectPrefersColorScheme}; - - function setDataThemeAttribute(theme) { - document.documentElement.setAttribute('data-theme', theme); - } - - function getQueryStringTheme() { - try { - return new URLSearchParams(window.location.search).get('${ThemeQueryStringKey}') - } catch (e) { - } - } - - function getStoredTheme() { - try { - return window['${siteStorage.type}'].getItem('${themeStorageKey}'); - } catch (err) { - } - } - - var initialTheme = getQueryStringTheme() || getStoredTheme(); - if (initialTheme !== null) { - setDataThemeAttribute(initialTheme); - } else { - if ( - respectPrefersColorScheme && - window.matchMedia('(prefers-color-scheme: dark)').matches - ) { - setDataThemeAttribute('dark'); - } else if ( - respectPrefersColorScheme && - window.matchMedia('(prefers-color-scheme: light)').matches - ) { - setDataThemeAttribute('light'); - } else { - setDataThemeAttribute(defaultMode === 'dark' ? 'dark' : 'light'); - } - } - })();`; -}; - -/* language=js */ -const DataAttributeQueryStringInlineJavaScript = ` -(function() { - try { - const entries = new URLSearchParams(window.location.search).entries(); - for (var [searchKey, value] of entries) { - if (searchKey.startsWith('${DataQueryStringPrefixKey}')) { - var key = searchKey.replace('${DataQueryStringPrefixKey}',"data-") - document.documentElement.setAttribute(key, value); - } - } - } catch(e) {} -})(); -`; - -// Duplicated constant. Unfortunately we can't import it from theme-common, as -// we need to support older nodejs versions without ESM support -// TODO: import from theme-common once we only support Node.js with ESM support -// + move all those announcementBar stuff there too -export const AnnouncementBarDismissStorageKey = - 'docusaurus.announcement.dismiss'; -const AnnouncementBarDismissDataAttribute = - 'data-announcement-bar-initially-dismissed'; -// We always render the announcement bar html on the server, to prevent layout -// shifts on React hydration. The theme can use CSS + the data attribute to hide -// the announcement bar asap (before React hydration) -/* language=js */ -const AnnouncementBarInlineJavaScript = ` -(function() { - function isDismissed() { - try { - return localStorage.getItem('${AnnouncementBarDismissStorageKey}') === 'true'; - } catch (err) {} - return false; - } - document.documentElement.setAttribute('${AnnouncementBarDismissDataAttribute}', isDismissed()); -})();`; - function getInfimaCSSFile(direction: string) { return `infima/dist/css/default/default${ direction === 'rtl' ? '-rtl' : '' @@ -227,9 +133,9 @@ export default function themeClassic( { tagName: 'script', innerHTML: ` -${noFlashColorMode({colorMode, siteStorage})} +${getThemeInlineScript({colorMode, siteStorage})} ${DataAttributeQueryStringInlineJavaScript} -${announcementBar ? AnnouncementBarInlineJavaScript : ''} +${announcementBar ? getAnnouncementBarInlineScript({siteStorage}) : ''} `, }, ], diff --git a/packages/docusaurus-theme-classic/src/inlineScripts.ts b/packages/docusaurus-theme-classic/src/inlineScripts.ts new file mode 100644 index 000000000000..6d5db7a8eba2 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/inlineScripts.ts @@ -0,0 +1,114 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import type {SiteStorage} from '@docusaurus/types'; +import type {ThemeConfig} from '@docusaurus/theme-common'; + +// Support for ?docusaurus-theme=dark +const ThemeQueryStringKey = 'docusaurus-theme'; + +// Support for ?docusaurus-data-mode=embed&docusaurus-data-myAttr=42 +const DataQueryStringPrefixKey = 'docusaurus-data-'; + +export function getThemeInlineScript({ + colorMode: {defaultMode, respectPrefersColorScheme}, + siteStorage, +}: { + colorMode: ThemeConfig['colorMode']; + siteStorage: SiteStorage; +}): string { + // Need to be inlined to prevent dark mode FOUC + // Make sure the key is the same as the one in the color mode React context + // Currently defined in: `docusaurus-theme-common/src/contexts/colorMode.tsx` + const themeStorageKey = `theme${siteStorage.namespace}`; + + /* language=js */ + return `(function() { + var defaultMode = '${defaultMode}'; + var respectPrefersColorScheme = ${respectPrefersColorScheme}; + + function setDataThemeAttribute(theme) { + document.documentElement.setAttribute('data-theme', theme); + } + + function getQueryStringTheme() { + try { + return new URLSearchParams(window.location.search).get('${ThemeQueryStringKey}') + } catch (e) { + } + } + + function getStoredTheme() { + try { + return window['${siteStorage.type}'].getItem('${themeStorageKey}'); + } catch (err) { + } + } + + var initialTheme = getQueryStringTheme() || getStoredTheme(); + if (initialTheme !== null) { + setDataThemeAttribute(initialTheme); + } else { + if ( + respectPrefersColorScheme && + window.matchMedia('(prefers-color-scheme: dark)').matches + ) { + setDataThemeAttribute('dark'); + } else if ( + respectPrefersColorScheme && + window.matchMedia('(prefers-color-scheme: light)').matches + ) { + setDataThemeAttribute('light'); + } else { + setDataThemeAttribute(defaultMode === 'dark' ? 'dark' : 'light'); + } + } + })();`; +} + +/* language=js */ +export const DataAttributeQueryStringInlineJavaScript = ` +(function() { + try { + const entries = new URLSearchParams(window.location.search).entries(); + for (var [searchKey, value] of entries) { + if (searchKey.startsWith('${DataQueryStringPrefixKey}')) { + var key = searchKey.replace('${DataQueryStringPrefixKey}',"data-") + document.documentElement.setAttribute(key, value); + } + } + } catch(e) {} +})(); +`; + +// We always render the announcement bar html on the server, to prevent layout +// shifts on React hydration. The theme can use CSS + the data attribute to hide +// the announcement bar asap (before React hydration) +export function getAnnouncementBarInlineScript({ + siteStorage, +}: { + siteStorage: SiteStorage; +}): string { + // Duplicated constant. Unfortunately we can't import it from theme-common, as + // we need to support older nodejs versions without ESM support + // TODO: import from theme-common once we support Node.js with ESM support + // + move all those announcementBar stuff there too + const AnnouncementBarDismissStorageKey = `docusaurus.announcement.dismiss${siteStorage.namespace}`; + const AnnouncementBarDismissDataAttribute = + 'data-announcement-bar-initially-dismissed'; + + /* language=js */ + return `(function() { + function isDismissed() { + try { + return localStorage.getItem('${AnnouncementBarDismissStorageKey}') === 'true'; + } catch (err) {} + return false; + } + document.documentElement.setAttribute('${AnnouncementBarDismissDataAttribute}', isDismissed()); +})();`; +} diff --git a/packages/docusaurus-theme-common/src/contexts/announcementBar.tsx b/packages/docusaurus-theme-common/src/contexts/announcementBar.tsx index 1adf70b3f279..b004a3241d0e 100644 --- a/packages/docusaurus-theme-common/src/contexts/announcementBar.tsx +++ b/packages/docusaurus-theme-common/src/contexts/announcementBar.tsx @@ -18,14 +18,12 @@ import {createStorageSlot} from '../utils/storageUtils'; import {ReactContextError} from '../utils/reactUtils'; import {useThemeConfig} from '../utils/useThemeConfig'; -export const AnnouncementBarDismissStorageKey = - 'docusaurus.announcement.dismiss'; -const AnnouncementBarIdStorageKey = 'docusaurus.announcement.id'; - +// Keep these keys in sync with the inlined script +// See packages/docusaurus-theme-classic/src/inlineScripts.ts const AnnouncementBarDismissStorage = createStorageSlot( - AnnouncementBarDismissStorageKey, + 'docusaurus.announcement.dismiss', ); -const IdStorage = createStorageSlot(AnnouncementBarIdStorageKey); +const IdStorage = createStorageSlot('docusaurus.announcement.id'); const isDismissedInStorage = () => AnnouncementBarDismissStorage.get() === 'true';