Skip to content

Commit

Permalink
fix(theme): fix announcement bar layout shift due to missing storage …
Browse files Browse the repository at this point in the history
…key namespace (#10144)
  • Loading branch information
slorber committed May 16, 2024
1 parent 87f0023 commit ef627f8
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 108 deletions.
110 changes: 8 additions & 102 deletions packages/docusaurus-theme-classic/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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' : ''
Expand Down Expand Up @@ -227,9 +133,9 @@ export default function themeClassic(
{
tagName: 'script',
innerHTML: `
${noFlashColorMode({colorMode, siteStorage})}
${getThemeInlineScript({colorMode, siteStorage})}
${DataAttributeQueryStringInlineJavaScript}
${announcementBar ? AnnouncementBarInlineJavaScript : ''}
${announcementBar ? getAnnouncementBarInlineScript({siteStorage}) : ''}
`,
},
],
Expand Down
114 changes: 114 additions & 0 deletions packages/docusaurus-theme-classic/src/inlineScripts.ts
Original file line number Diff line number Diff line change
@@ -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());
})();`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit ef627f8

Please sign in to comment.