mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-05 04:12:53 +02:00
fix(v2): Fix announcementBar layout shifts (#5040)
* Fix announcementBar layout shift * useAnnouncementBar should return correct state after hydration * refactor announcementBar => move utils to theme-common * restore previous announcementBar * typo
This commit is contained in:
parent
814455f88e
commit
9916a0b4a4
11 changed files with 179 additions and 110 deletions
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import {DocusaurusContext, Plugin} from '@docusaurus/types';
|
||||
import {ThemeConfig} from '@docusaurus/theme-common';
|
||||
import type {ThemeConfig} from '@docusaurus/theme-common';
|
||||
import {getTranslationFiles, translateThemeConfig} from './translations';
|
||||
import path from 'path';
|
||||
import Module from 'module';
|
||||
|
@ -24,7 +24,7 @@ const ContextReplacementPlugin = requireFromDocusaurusCore(
|
|||
|
||||
// Need to be inlined to prevent dark mode FOUC
|
||||
// Make sure that the 'storageKey' is the same as the one in `/theme/hooks/useTheme.js`
|
||||
const storageKey = 'theme';
|
||||
const ThemeStorageKey = 'theme';
|
||||
const noFlashColorMode = ({defaultMode, respectPrefersColorScheme}) => {
|
||||
return `(function() {
|
||||
var defaultMode = '${defaultMode}';
|
||||
|
@ -37,7 +37,7 @@ const noFlashColorMode = ({defaultMode, respectPrefersColorScheme}) => {
|
|||
function getStoredTheme() {
|
||||
var theme = null;
|
||||
try {
|
||||
theme = localStorage.getItem('${storageKey}');
|
||||
theme = localStorage.getItem('${ThemeStorageKey}');
|
||||
} catch (err) {}
|
||||
return theme;
|
||||
}
|
||||
|
@ -63,6 +63,26 @@ const noFlashColorMode = ({defaultMode, respectPrefersColorScheme}) => {
|
|||
})();`;
|
||||
};
|
||||
|
||||
// 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)
|
||||
const AnnouncementBarInlineJavaScript = `
|
||||
(function() {
|
||||
function isDismissed() {
|
||||
try {
|
||||
return localStorage.getItem('${AnnouncementBarDismissStorageKey}') === 'true';
|
||||
} catch (err) {}
|
||||
return false;
|
||||
}
|
||||
document.documentElement.setAttribute('${AnnouncementBarDismissDataAttribute}', isDismissed());
|
||||
})();`;
|
||||
|
||||
function getInfimaCSSFile(direction) {
|
||||
return `infima/dist/css/default/default${
|
||||
direction === 'rtl' ? '-rtl' : ''
|
||||
|
@ -82,7 +102,11 @@ export default function docusaurusThemeClassic(
|
|||
i18n: {currentLocale, localeConfigs},
|
||||
} = context;
|
||||
const themeConfig = (roughlyTypedThemeConfig || {}) as ThemeConfig;
|
||||
const {colorMode, prism: {additionalLanguages = []} = {}} = themeConfig;
|
||||
const {
|
||||
announcementBar,
|
||||
colorMode,
|
||||
prism: {additionalLanguages = []} = {},
|
||||
} = themeConfig;
|
||||
const {customCss} = options || {};
|
||||
const {direction} = localeConfigs[currentLocale];
|
||||
|
||||
|
@ -178,7 +202,10 @@ export default function docusaurusThemeClassic(
|
|||
preBodyTags: [
|
||||
{
|
||||
tagName: 'script',
|
||||
innerHTML: noFlashColorMode(colorMode),
|
||||
innerHTML: `
|
||||
${noFlashColorMode(colorMode)}
|
||||
${announcementBar ? AnnouncementBarInlineJavaScript : ''}
|
||||
`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -7,17 +7,13 @@
|
|||
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {useThemeConfig} from '@docusaurus/theme-common';
|
||||
import useUserPreferencesContext from '@theme/hooks/useUserPreferencesContext';
|
||||
import {useThemeConfig, useAnnouncementBar} from '@docusaurus/theme-common';
|
||||
import {translate} from '@docusaurus/Translate';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
function AnnouncementBar(): JSX.Element | null {
|
||||
const {
|
||||
isAnnouncementBarClosed,
|
||||
closeAnnouncementBar,
|
||||
} = useUserPreferencesContext();
|
||||
const {isClosed, close} = useAnnouncementBar();
|
||||
const {announcementBar} = useThemeConfig();
|
||||
|
||||
if (!announcementBar) {
|
||||
|
@ -25,7 +21,8 @@ function AnnouncementBar(): JSX.Element | null {
|
|||
}
|
||||
|
||||
const {content, backgroundColor, textColor, isCloseable} = announcementBar;
|
||||
if (!content || (isCloseable && isAnnouncementBarClosed)) {
|
||||
|
||||
if (!content || (isCloseable && isClosed)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -46,7 +43,7 @@ function AnnouncementBar(): JSX.Element | null {
|
|||
<button
|
||||
type="button"
|
||||
className={clsx(styles.announcementBarClose, 'clean-btn')}
|
||||
onClick={closeAnnouncementBar}
|
||||
onClick={close}
|
||||
aria-label={translate({
|
||||
id: 'theme.AnnouncementBar.closeButtonAriaLabel',
|
||||
message: 'Close',
|
||||
|
|
|
@ -18,6 +18,10 @@
|
|||
border-bottom: 1px solid var(--ifm-color-emphasis-100);
|
||||
}
|
||||
|
||||
html[data-announcement-bar-initially-dismissed='true'] .announcementBar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.announcementBar {
|
||||
display: none;
|
||||
|
|
|
@ -11,8 +11,8 @@ import {
|
|||
useThemeConfig,
|
||||
isSamePath,
|
||||
usePrevious,
|
||||
useAnnouncementBar,
|
||||
} from '@docusaurus/theme-common';
|
||||
import useUserPreferencesContext from '@theme/hooks/useUserPreferencesContext';
|
||||
import useLockBodyScroll from '@theme/hooks/useLockBodyScroll';
|
||||
import useWindowSize, {windowSizes} from '@theme/hooks/useWindowSize';
|
||||
import useScrollPosition from '@theme/hooks/useScrollPosition';
|
||||
|
@ -198,12 +198,10 @@ function DocSidebarItemLink({
|
|||
}
|
||||
|
||||
function useShowAnnouncementBar() {
|
||||
const {isAnnouncementBarClosed} = useUserPreferencesContext();
|
||||
const [showAnnouncementBar, setShowAnnouncementBar] = useState(
|
||||
!isAnnouncementBarClosed,
|
||||
);
|
||||
const {isClosed} = useAnnouncementBar();
|
||||
const [showAnnouncementBar, setShowAnnouncementBar] = useState(!isClosed);
|
||||
useScrollPosition(({scrollY}) => {
|
||||
if (!isAnnouncementBarClosed) {
|
||||
if (!isClosed) {
|
||||
setShowAnnouncementBar(scrollY === 0);
|
||||
}
|
||||
});
|
||||
|
@ -314,7 +312,7 @@ function DocSidebar({
|
|||
navbar: {hideOnScroll},
|
||||
hideableSidebar,
|
||||
} = useThemeConfig();
|
||||
const {isAnnouncementBarClosed} = useUserPreferencesContext();
|
||||
const {isClosed: isAnnouncementBarClosed} = useAnnouncementBar();
|
||||
|
||||
const {
|
||||
showResponsiveSidebar,
|
||||
|
|
|
@ -8,17 +8,22 @@
|
|||
import React from 'react';
|
||||
import ThemeProvider from '@theme/ThemeProvider';
|
||||
import UserPreferencesProvider from '@theme/UserPreferencesProvider';
|
||||
import {DocsPreferredVersionContextProvider} from '@docusaurus/theme-common';
|
||||
import {
|
||||
AnnouncementBarProvider,
|
||||
DocsPreferredVersionContextProvider,
|
||||
} from '@docusaurus/theme-common';
|
||||
import type {Props} from '@theme/LayoutProviders';
|
||||
|
||||
export default function LayoutProviders({children}: Props): JSX.Element {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<AnnouncementBarProvider>
|
||||
<UserPreferencesProvider>
|
||||
<DocsPreferredVersionContextProvider>
|
||||
{children}
|
||||
</DocsPreferredVersionContextProvider>
|
||||
</UserPreferencesProvider>
|
||||
</AnnouncementBarProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,21 +8,16 @@
|
|||
import React from 'react';
|
||||
|
||||
import useTabGroupChoice from '@theme/hooks/useTabGroupChoice';
|
||||
import useAnnouncementBar from '@theme/hooks/useAnnouncementBar';
|
||||
import UserPreferencesContext from '@theme/UserPreferencesContext';
|
||||
import type {Props} from '@theme/UserPreferencesProvider';
|
||||
|
||||
function UserPreferencesProvider(props: Props): JSX.Element {
|
||||
const {tabGroupChoices, setTabGroupChoices} = useTabGroupChoice();
|
||||
const {isAnnouncementBarClosed, closeAnnouncementBar} = useAnnouncementBar();
|
||||
|
||||
return (
|
||||
<UserPreferencesContext.Provider
|
||||
value={{
|
||||
tabGroupChoices,
|
||||
setTabGroupChoices,
|
||||
isAnnouncementBarClosed,
|
||||
closeAnnouncementBar,
|
||||
}}>
|
||||
{props.children}
|
||||
</UserPreferencesContext.Provider>
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
/**
|
||||
* 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 {useState, useEffect, useCallback} from 'react';
|
||||
import {useThemeConfig, createStorageSlot} from '@docusaurus/theme-common';
|
||||
import type {useAnnouncementBarReturns} from '@theme/hooks/useAnnouncementBar';
|
||||
|
||||
const DismissStorage = createStorageSlot('docusaurus.announcement.dismiss');
|
||||
const IdStorage = createStorageSlot('docusaurus.announcement.id');
|
||||
|
||||
const useAnnouncementBar = (): useAnnouncementBarReturns => {
|
||||
const {announcementBar} = useThemeConfig();
|
||||
|
||||
const [isClosed, setClosed] = useState(true);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
DismissStorage.set('true');
|
||||
setClosed(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!announcementBar) {
|
||||
return;
|
||||
}
|
||||
const {id} = announcementBar;
|
||||
|
||||
let viewedId = IdStorage.get();
|
||||
|
||||
// retrocompatibility due to spelling mistake of default id
|
||||
// see https://github.com/facebook/docusaurus/issues/3338
|
||||
if (viewedId === 'annoucement-bar') {
|
||||
viewedId = 'announcement-bar';
|
||||
}
|
||||
|
||||
const isNewAnnouncement = id !== viewedId;
|
||||
|
||||
IdStorage.set(id);
|
||||
|
||||
if (isNewAnnouncement) {
|
||||
DismissStorage.set('false');
|
||||
}
|
||||
|
||||
if (isNewAnnouncement || DismissStorage.get() === 'false') {
|
||||
setClosed(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isAnnouncementBarClosed: isClosed,
|
||||
closeAnnouncementBar: handleClose,
|
||||
};
|
||||
};
|
||||
|
||||
export default useAnnouncementBar;
|
12
packages/docusaurus-theme-classic/src/types.d.ts
vendored
12
packages/docusaurus-theme-classic/src/types.d.ts
vendored
|
@ -117,16 +117,6 @@ declare module '@theme/Heading' {
|
|||
export const MainHeading: (props: Props) => JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/hooks/useAnnouncementBar' {
|
||||
export type useAnnouncementBarReturns = {
|
||||
readonly isAnnouncementBarClosed: boolean;
|
||||
readonly closeAnnouncementBar: () => void;
|
||||
};
|
||||
|
||||
const useAnnouncementBar: () => useAnnouncementBarReturns;
|
||||
export default useAnnouncementBar;
|
||||
}
|
||||
|
||||
declare module '@theme/hooks/useHideableNavbar' {
|
||||
export type useHideableNavbarReturns = {
|
||||
readonly navbarRef: (node: HTMLElement | null) => void;
|
||||
|
@ -214,8 +204,6 @@ declare module '@theme/hooks/useUserPreferencesContext' {
|
|||
export type UserPreferencesContextProps = {
|
||||
tabGroupChoices: {readonly [groupId: string]: string};
|
||||
setTabGroupChoices: (groupId: string, newChoice: string) => void;
|
||||
isAnnouncementBarClosed: boolean;
|
||||
closeAnnouncementBar: () => void;
|
||||
};
|
||||
|
||||
export default function useUserPreferencesContext(): UserPreferencesContextProps;
|
||||
|
|
|
@ -45,3 +45,8 @@ export {
|
|||
export {DocsPreferredVersionContextProvider} from './utils/docsPreferredVersion/DocsPreferredVersionProvider';
|
||||
|
||||
export {ThemeClassNames} from './utils/ThemeClassNames';
|
||||
|
||||
export {
|
||||
AnnouncementBarProvider,
|
||||
useAnnouncementBar,
|
||||
} from './utils/announcementBarUtils';
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
/**
|
||||
* 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 React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useMemo,
|
||||
ReactNode,
|
||||
useContext,
|
||||
createContext,
|
||||
} from 'react';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import {createStorageSlot} from './storageUtils';
|
||||
import {useThemeConfig} from './useThemeConfig';
|
||||
|
||||
export const AnnouncementBarDismissStorageKey =
|
||||
'docusaurus.announcement.dismiss';
|
||||
const AnnouncementBarIdStorageKey = 'docusaurus.announcement.id';
|
||||
|
||||
const AnnouncementBarDismissStorage = createStorageSlot(
|
||||
AnnouncementBarDismissStorageKey,
|
||||
);
|
||||
const IdStorage = createStorageSlot(AnnouncementBarIdStorageKey);
|
||||
|
||||
const isDismissedInStorage = () =>
|
||||
AnnouncementBarDismissStorage.get() === 'true';
|
||||
const setDismissedInStorage = (bool: boolean) =>
|
||||
AnnouncementBarDismissStorage.set(String(bool));
|
||||
|
||||
type AnnouncementBarAPI = {
|
||||
readonly isClosed: boolean;
|
||||
readonly close: () => void;
|
||||
};
|
||||
|
||||
const useAnnouncementBarContextValue = (): AnnouncementBarAPI => {
|
||||
const {announcementBar} = useThemeConfig();
|
||||
const {isClient} = useDocusaurusContext();
|
||||
|
||||
const [isClosed, setClosed] = useState(() => {
|
||||
return isClient
|
||||
? // On client navigation: init with localstorage value
|
||||
isDismissedInStorage()
|
||||
: // On server/hydration: always visible to prevent layout shifts (will be hidden with css if needed)
|
||||
false;
|
||||
});
|
||||
// Update state after hydration
|
||||
useEffect(() => {
|
||||
setClosed(isDismissedInStorage());
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setDismissedInStorage(true);
|
||||
setClosed(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!announcementBar) {
|
||||
return;
|
||||
}
|
||||
const {id} = announcementBar;
|
||||
|
||||
let viewedId = IdStorage.get();
|
||||
|
||||
// retrocompatibility due to spelling mistake of default id
|
||||
// see https://github.com/facebook/docusaurus/issues/3338
|
||||
if (viewedId === 'annoucement-bar') {
|
||||
viewedId = 'announcement-bar';
|
||||
}
|
||||
|
||||
const isNewAnnouncement = id !== viewedId;
|
||||
|
||||
IdStorage.set(id);
|
||||
|
||||
if (isNewAnnouncement) {
|
||||
setDismissedInStorage(false);
|
||||
}
|
||||
|
||||
if (isNewAnnouncement || !isDismissedInStorage()) {
|
||||
setClosed(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
isClosed,
|
||||
close: handleClose,
|
||||
};
|
||||
}, [isClosed]);
|
||||
};
|
||||
|
||||
const AnnouncementBarContext = createContext<AnnouncementBarAPI | null>(null);
|
||||
|
||||
export const AnnouncementBarProvider = ({children}: {children: ReactNode}) => {
|
||||
const value = useAnnouncementBarContextValue();
|
||||
return (
|
||||
<AnnouncementBarContext.Provider value={value}>
|
||||
{children}
|
||||
</AnnouncementBarContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAnnouncementBar = (): AnnouncementBarAPI => {
|
||||
const api = useContext(AnnouncementBarContext);
|
||||
if (!api) {
|
||||
throw new Error(
|
||||
'useAnnouncementBar(): AnnouncementBar not found in React context: make sure to use the AnnouncementBarProvider on top of the tree',
|
||||
);
|
||||
}
|
||||
return api;
|
||||
};
|
|
@ -305,17 +305,10 @@ const isVersioningDisabled = !!process.env.DISABLE_VERSIONING || isI18nStaging;
|
|||
respectPrefersColorScheme: true,
|
||||
},
|
||||
announcementBar: {
|
||||
id: 'v1-new-domain',
|
||||
id: 'announcementBar-1', // Increment on change
|
||||
content:
|
||||
'➡️ Docusaurus v1 documentation has moved to <a target="_blank" rel="noopener noreferrer" href="https://v1.docusaurus.io/">v1.docusaurus.io</a>! 🔄',
|
||||
'⭐️ If you like Docusaurus, give it a star on <a target="_blank" rel="noopener noreferrer" href="https://github.com/facebook/docusaurus">GitHub</a>! ⭐',
|
||||
},
|
||||
/*
|
||||
announcementBar: {
|
||||
id: 'supportus',
|
||||
content:
|
||||
'⭐️ If you like Docusaurus, give it a star on <a target="_blank" rel="noopener noreferrer" href="https://github.com/facebook/docusaurus">GitHub</a>! ⭐️',
|
||||
},
|
||||
*/
|
||||
prism: {
|
||||
theme: require('prism-react-renderer/themes/github'),
|
||||
darkTheme: require('prism-react-renderer/themes/dracula'),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue