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:
Sébastien Lorber 2021-06-24 11:35:35 +02:00 committed by GitHub
parent 814455f88e
commit 9916a0b4a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 179 additions and 110 deletions

View file

@ -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 : ''}
`,
},
],
};

View file

@ -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',

View file

@ -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;

View file

@ -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,

View file

@ -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>
);
}

View file

@ -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>

View file

@ -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;

View file

@ -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;

View file

@ -45,3 +45,8 @@ export {
export {DocsPreferredVersionContextProvider} from './utils/docsPreferredVersion/DocsPreferredVersionProvider';
export {ThemeClassNames} from './utils/ThemeClassNames';
export {
AnnouncementBarProvider,
useAnnouncementBar,
} from './utils/announcementBarUtils';

View file

@ -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;
};

View file

@ -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'),