test: improve test coverage; reorder theme-common files (#6956)

* test: improve test coverage; reorder theme-common files

* no need for this
This commit is contained in:
Joshua Chen 2022-03-22 15:33:55 +08:00 committed by GitHub
parent 0a5354dc32
commit 948271a0ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 555 additions and 317 deletions

View file

@ -34,7 +34,7 @@ export type DetailsProps = {
summary?: ReactElement;
} & ComponentProps<'details'>;
export default function Details({
export function Details({
summary,
children,
...props

View file

@ -14,9 +14,9 @@ import React, {
type ReactNode,
} from 'react';
import useIsBrowser from '@docusaurus/useIsBrowser';
import {createStorageSlot} from './storageUtils';
import {ReactContextError} from './reactUtils';
import {useThemeConfig} from './useThemeConfig';
import {createStorageSlot} from '../utils/storageUtils';
import {ReactContextError} from '../utils/reactUtils';
import {useThemeConfig} from '../utils/useThemeConfig';
export const AnnouncementBarDismissStorageKey =
'docusaurus.announcement.dismiss';

View file

@ -14,11 +14,11 @@ import React, {
useRef,
type ReactNode,
} from 'react';
import {ReactContextError} from './reactUtils';
import {ReactContextError} from '../utils/reactUtils';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import {createStorageSlot} from './storageUtils';
import {useThemeConfig} from './useThemeConfig';
import {createStorageSlot} from '../utils/storageUtils';
import {useThemeConfig} from '../utils/useThemeConfig';
type ColorModeContextValue = {
readonly colorMode: ColorMode;
@ -171,9 +171,7 @@ export function ColorModeProvider({
}
export function useColorMode(): ColorModeContextValue {
const context = useContext<ColorModeContextValue | undefined>(
ColorModeContext,
);
const context = useContext(ColorModeContext);
if (context == null) {
throw new ReactContextError(
'ColorModeProvider',

View file

@ -6,7 +6,7 @@
*/
import React, {type ReactNode, useMemo, useState, useContext} from 'react';
import {ReactContextError} from './reactUtils';
import {ReactContextError} from '../utils/reactUtils';
const EmptyContext: unique symbol = Symbol('EmptyContext');
const Context = React.createContext<

View file

@ -10,18 +10,47 @@ import React, {
useEffect,
useMemo,
useState,
useCallback,
type ReactNode,
} from 'react';
import {useThemeConfig, type DocsVersionPersistence} from '../useThemeConfig';
import {isDocsPluginEnabled} from '../docsUtils';
import {ReactContextError} from '../reactUtils';
import {
useThemeConfig,
type DocsVersionPersistence,
} from '../utils/useThemeConfig';
import {isDocsPluginEnabled} from '../utils/docsUtils';
import {ReactContextError} from '../utils/reactUtils';
import {createStorageSlot} from '../utils/storageUtils';
import {
useAllDocsData,
useDocsData,
type GlobalPluginData,
type GlobalVersion,
} from '@docusaurus/plugin-content-docs/client';
import DocsPreferredVersionStorage from './DocsPreferredVersionStorage';
import {DEFAULT_PLUGIN_ID} from '@docusaurus/constants';
const storageKey = (pluginId: string) => `docs-preferred-version-${pluginId}`;
const DocsPreferredVersionStorage = {
save: (
pluginId: string,
persistence: DocsVersionPersistence,
versionName: string,
): void => {
createStorageSlot(storageKey(pluginId), {persistence}).set(versionName);
},
read: (
pluginId: string,
persistence: DocsVersionPersistence,
): string | null =>
createStorageSlot(storageKey(pluginId), {persistence}).get(),
clear: (pluginId: string, persistence: DocsVersionPersistence): void => {
createStorageSlot(storageKey(pluginId), {persistence}).del();
},
};
type DocsPreferredVersionName = string | null;
@ -158,10 +187,64 @@ function DocsPreferredVersionContextProviderUnsafe({
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
}
export function useDocsPreferredVersionContext(): DocsPreferredVersionContextValue {
function useDocsPreferredVersionContext(): DocsPreferredVersionContextValue {
const value = useContext(Context);
if (!value) {
throw new ReactContextError('DocsPreferredVersionContextProvider');
}
return value;
}
// Note, the preferredVersion attribute will always be null before mount
export function useDocsPreferredVersion(
pluginId: string | undefined = DEFAULT_PLUGIN_ID,
): {
preferredVersion: GlobalVersion | null | undefined;
savePreferredVersionName: (versionName: string) => void;
} {
const docsData = useDocsData(pluginId);
const [state, api] = useDocsPreferredVersionContext();
const {preferredVersionName} = state[pluginId]!;
const preferredVersion = preferredVersionName
? docsData.versions.find((version) => version.name === preferredVersionName)
: null;
const savePreferredVersionName = useCallback(
(versionName: string) => {
api.savePreferredVersion(pluginId, versionName);
},
[api, pluginId],
);
return {preferredVersion, savePreferredVersionName} as const;
}
export function useDocsPreferredVersionByPluginId(): Record<
string,
GlobalVersion | null | undefined
> {
const allDocsData = useAllDocsData();
const [state] = useDocsPreferredVersionContext();
function getPluginIdPreferredVersion(pluginId: string) {
const docsData = allDocsData[pluginId]!;
const {preferredVersionName} = state[pluginId]!;
return preferredVersionName
? docsData.versions.find(
(version) => version.name === preferredVersionName,
)
: null;
}
const pluginIds = Object.keys(allDocsData);
const result: Record<string, GlobalVersion | null | undefined> = {};
pluginIds.forEach((pluginId) => {
result[pluginId] = getPluginIdPreferredVersion(pluginId);
});
return result;
}

View file

@ -0,0 +1,98 @@
/**
* 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, {
type ReactNode,
useCallback,
useEffect,
useState,
useMemo,
} from 'react';
import {useWindowSize} from '../hooks/useWindowSize';
import {useHistoryPopHandler} from '../utils/historyUtils';
import {useActivePlugin} from '@docusaurus/plugin-content-docs/client';
import {useThemeConfig} from '../utils/useThemeConfig';
import {ReactContextError} from '../utils/reactUtils';
type NavbarMobileSidebarContextValue = {
disabled: boolean;
shouldRender: boolean;
toggle: () => void;
shown: boolean;
};
const Context = React.createContext<
NavbarMobileSidebarContextValue | undefined
>(undefined);
// Mobile sidebar can be disabled in case it would lead to an empty sidebar
// In this case it's not useful to display a navbar sidebar toggle button
function useNavbarMobileSidebarDisabled() {
const activeDocPlugin = useActivePlugin();
const {items} = useThemeConfig().navbar;
return items.length === 0 && !activeDocPlugin;
}
function useNavbarMobileSidebarContextValue(): NavbarMobileSidebarContextValue {
const disabled = useNavbarMobileSidebarDisabled();
const windowSize = useWindowSize();
// Mobile sidebar not visible until user interaction: can avoid SSR rendering
const shouldRender = !disabled && windowSize === 'mobile'; // || windowSize === 'ssr';
const [shown, setShown] = useState(false);
// Close mobile sidebar on navigation pop
// Most likely firing when using the Android back button (but not only)
useHistoryPopHandler(() => {
if (shown) {
setShown(false);
// Should we prevent the navigation here?
// See https://github.com/facebook/docusaurus/pull/5462#issuecomment-911699846
return false; // prevent pop navigation
}
return undefined;
});
const toggle = useCallback(() => {
setShown((s) => !s);
}, []);
useEffect(() => {
if (windowSize === 'desktop') {
setShown(false);
}
}, [windowSize]);
// Return stable context value
return useMemo(
() => ({
disabled,
shouldRender,
toggle,
shown,
}),
[disabled, shouldRender, toggle, shown],
);
}
export function NavbarMobileSidebarProvider({
children,
}: {
children: ReactNode;
}): JSX.Element {
const value = useNavbarMobileSidebarContextValue();
return <Context.Provider value={value}>{children}</Context.Provider>;
}
export function useNavbarMobileSidebar(): NavbarMobileSidebarContextValue {
const context = React.useContext(Context);
if (context == null) {
throw new ReactContextError('NavbarMobileSidebarProvider');
}
return context;
}

View file

@ -13,8 +13,8 @@ import React, {
useContext,
type ReactNode,
} from 'react';
import {createStorageSlot, listStorageKeys} from './storageUtils';
import {ReactContextError} from './reactUtils';
import {createStorageSlot, listStorageKeys} from '../utils/storageUtils';
import {ReactContextError} from '../utils/reactUtils';
const TAB_CHOICE_PREFIX = 'docusaurus.tab.';

View file

@ -9,14 +9,10 @@ import {useState, useCallback, useRef} from 'react';
import {useLocationChange} from '../utils/useLocationChange';
import {useScrollPosition} from '../utils/scrollUtils';
type UseHideableNavbarReturns = {
export function useHideableNavbar(hideOnScroll: boolean): {
readonly navbarRef: (node: HTMLElement | null) => void;
readonly isNavbarVisible: boolean;
};
export default function useHideableNavbar(
hideOnScroll: boolean,
): UseHideableNavbarReturns {
} {
const [isNavbarVisible, setIsNavbarVisible] = useState(hideOnScroll);
const isFocusedAnchor = useRef(false);
const navbarHeight = useRef(0);

View file

@ -11,9 +11,11 @@ import './styles.css';
export const keyboardFocusedClassName = 'navigation-with-keyboard';
// This hook detect keyboard focus indicator to not show outline for mouse users
// Inspired by https://hackernoon.com/removing-that-ugly-focus-ring-and-keeping-it-too-6c8727fefcd2
export default function useKeyboardNavigation(): void {
/**
* Detect keyboard focus indicator to not show outline for mouse users
* Inspired by https://hackernoon.com/removing-that-ugly-focus-ring-and-keeping-it-too-6c8727fefcd2
*/
export function useKeyboardNavigation(): void {
useEffect(() => {
function handleOutlineStyles(e: MouseEvent | KeyboardEvent) {
if (e.type === 'keydown' && (e as KeyboardEvent).key === 'Tab') {

View file

@ -7,7 +7,7 @@
import {useEffect} from 'react';
export default function useLockBodyScroll(lock: boolean = true): void {
export function useLockBodyScroll(lock: boolean = true): void {
useEffect(() => {
document.body.style.overflow = lock ? 'hidden' : 'visible';

View file

@ -6,10 +6,10 @@
*/
import defaultTheme from 'prism-react-renderer/themes/palenight';
import {useColorMode} from '../utils/colorModeUtils';
import {useColorMode} from '../contexts/colorMode';
import {useThemeConfig} from '../utils/useThemeConfig';
export default function usePrismTheme(): typeof defaultTheme {
export function usePrismTheme(): typeof defaultTheme {
const {prism} = useThemeConfig();
const {colorMode} = useColorMode();
const lightModeTheme = prism.theme || defaultTheme;

View file

@ -11,13 +11,11 @@ import {useCallback, useEffect, useState} from 'react';
const SEARCH_PARAM_QUERY = 'q';
interface UseSearchPageReturn {
export function useSearchPage(): {
searchQuery: string;
setSearchQuery: (newSearchQuery: string) => void;
generateSearchPageLink: (targetSearchQuery: string) => string;
}
export default function useSearchPage(): UseSearchPageReturn {
} {
const history = useHistory();
const {
siteConfig: {baseUrl},

View file

@ -6,7 +6,7 @@
*/
import {useEffect, useRef} from 'react';
import {useThemeConfig} from './useThemeConfig';
import {useThemeConfig} from '../utils/useThemeConfig';
// TODO make the hardcoded theme-classic classnames configurable (or add them
// to ThemeClassNames?)
@ -120,9 +120,7 @@ export type TOCHighlightConfig = {
maxHeadingLevel: number;
};
export default function useTOCHighlight(
config: TOCHighlightConfig | undefined,
): void {
export function useTOCHighlight(config: TOCHighlightConfig | undefined): void {
const lastActiveLinkRef = useRef<HTMLAnchorElement | undefined>(undefined);
const anchorTopOffsetRef = useAnchorTopOffsetRef();

View file

@ -41,7 +41,7 @@ const DevSimulateSSR = process.env.NODE_ENV === 'development' && true;
// This hook returns an enum value on purpose!
// We don't want it to return the actual width value, for resize perf reasons
// We only want to re-render once a breakpoint is crossed
export default function useWindowSize(): WindowSize {
export function useWindowSize(): WindowSize {
const [windowSize, setWindowSize] = useState<WindowSize>(() => {
if (DevSimulateSSR) {
return 'ssr';

View file

@ -22,7 +22,7 @@ export {
export {
DocSidebarItemsExpandedStateProvider,
useDocSidebarItemsExpandedState,
} from './utils/docSidebarItemsExpandedState';
} from './contexts/docSidebarItemsExpandedState';
export {createStorageSlot, listStorageKeys} from './utils/storageUtils';
@ -60,8 +60,6 @@ export {usePluralForm} from './utils/usePluralForm';
export {useLocationChange} from './utils/useLocationChange';
export {usePrevious} from './utils/usePrevious';
export {
useCollapsible,
Collapsible,
@ -69,23 +67,22 @@ export {
type UseCollapsibleReturns,
} from './components/Collapsible';
export {default as Details, type DetailsProps} from './components/Details';
export {Details, type DetailsProps} from './components/Details';
export {
useDocsPreferredVersion,
useDocsPreferredVersionByPluginId,
} from './utils/docsPreferredVersion/useDocsPreferredVersion';
DocsPreferredVersionContextProvider,
} from './contexts/docsPreferredVersion';
export {duplicates, uniq} from './utils/jsUtils';
export {DocsPreferredVersionContextProvider} from './utils/docsPreferredVersion/DocsPreferredVersionProvider';
export {ThemeClassNames} from './utils/ThemeClassNames';
export {
AnnouncementBarProvider,
useAnnouncementBar,
} from './utils/announcementBarUtils';
} from './contexts/announcementBar';
export {useLocalPathname} from './utils/useLocalPathname';
@ -99,9 +96,9 @@ export {
export {useHistoryPopHandler} from './utils/historyUtils';
export {
default as useTOCHighlight,
useTOCHighlight,
type TOCHighlightConfig,
} from './utils/useTOCHighlight';
} from './hooks/useTOCHighlight';
export {
useFilteredAndTreeifiedTOC,
@ -121,6 +118,7 @@ export {
export {
useIsomorphicLayoutEffect,
useDynamicCallback,
usePrevious,
ReactContextError,
} from './utils/reactUtils';
@ -138,30 +136,28 @@ export {
useColorMode,
ColorModeProvider,
type ColorMode,
} from './utils/colorModeUtils';
} from './contexts/colorMode';
export {splitNavbarItems, NavbarProvider} from './utils/navbarUtils';
export {
useTabGroupChoice,
TabGroupChoiceProvider,
} from './utils/tabGroupChoiceUtils';
} from './contexts/tabGroupChoice';
export {
splitNavbarItems,
NavbarProvider,
useNavbarMobileSidebar,
} from './utils/navbarUtils';
export {useNavbarMobileSidebar} from './contexts/navbarMobileSidebar';
export {
useNavbarSecondaryMenu,
NavbarSecondaryMenuFiller,
} from './utils/navbarSecondaryMenuUtils';
export type {NavbarSecondaryMenuComponent} from './utils/navbarSecondaryMenuUtils';
} from './utils/navbarSecondaryMenu';
export type {NavbarSecondaryMenuComponent} from './utils/navbarSecondaryMenu';
export {default as useHideableNavbar} from './hooks/useHideableNavbar';
export {useHideableNavbar} from './hooks/useHideableNavbar';
export {
default as useKeyboardNavigation,
useKeyboardNavigation,
keyboardFocusedClassName,
} from './hooks/useKeyboardNavigation';
export {default as usePrismTheme} from './hooks/usePrismTheme';
export {default as useLockBodyScroll} from './hooks/useLockBodyScroll';
export {default as useWindowSize} from './hooks/useWindowSize';
export {default as useSearchPage} from './hooks/useSearchPage';
export {usePrismTheme} from './hooks/usePrismTheme';
export {useLockBodyScroll} from './hooks/useLockBodyScroll';
export {useWindowSize} from './hooks/useWindowSize';
export {useSearchPage} from './hooks/useSearchPage';

View file

@ -0,0 +1,22 @@
/**
* 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 {usePrevious} from '../reactUtils';
import {renderHook} from '@testing-library/react-hooks';
describe('usePrevious', () => {
it('returns the previous value of a variable', () => {
const {result, rerender} = renderHook((val) => usePrevious(val), {
initialProps: 1,
});
expect(result.current).toBeUndefined();
rerender(2);
expect(result.current).toBe(1);
rerender(3);
expect(result.current).toBe(2);
});
});

View file

@ -1,33 +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 {createStorageSlot} from '../storageUtils';
import type {DocsVersionPersistence} from '../useThemeConfig';
const storageKey = (pluginId: string) => `docs-preferred-version-${pluginId}`;
const DocsPreferredVersionStorage = {
save: (
pluginId: string,
persistence: DocsVersionPersistence,
versionName: string,
): void => {
createStorageSlot(storageKey(pluginId), {persistence}).set(versionName);
},
read: (
pluginId: string,
persistence: DocsVersionPersistence,
): string | null =>
createStorageSlot(storageKey(pluginId), {persistence}).get(),
clear: (pluginId: string, persistence: DocsVersionPersistence): void => {
createStorageSlot(storageKey(pluginId), {persistence}).del();
},
};
export default DocsPreferredVersionStorage;

View file

@ -1,70 +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 {useCallback} from 'react';
import {useDocsPreferredVersionContext} from './DocsPreferredVersionProvider';
import {
useAllDocsData,
useDocsData,
type GlobalVersion,
} from '@docusaurus/plugin-content-docs/client';
import {DEFAULT_PLUGIN_ID} from '@docusaurus/constants';
// Note, the preferredVersion attribute will always be null before mount
export function useDocsPreferredVersion(
pluginId: string | undefined = DEFAULT_PLUGIN_ID,
): {
preferredVersion: GlobalVersion | null | undefined;
savePreferredVersionName: (versionName: string) => void;
} {
const docsData = useDocsData(pluginId);
const [state, api] = useDocsPreferredVersionContext();
const {preferredVersionName} = state[pluginId]!;
const preferredVersion = preferredVersionName
? docsData.versions.find((version) => version.name === preferredVersionName)
: null;
const savePreferredVersionName = useCallback(
(versionName: string) => {
api.savePreferredVersion(pluginId, versionName);
},
[api, pluginId],
);
return {preferredVersion, savePreferredVersionName} as const;
}
export function useDocsPreferredVersionByPluginId(): Record<
string,
GlobalVersion | null | undefined
> {
const allDocsData = useAllDocsData();
const [state] = useDocsPreferredVersionContext();
function getPluginIdPreferredVersion(pluginId: string) {
const docsData = allDocsData[pluginId]!;
const {preferredVersionName} = state[pluginId]!;
return preferredVersionName
? docsData.versions.find(
(version) => version.name === preferredVersionName,
)
: null;
}
const pluginIds = Object.keys(allDocsData);
const result: Record<string, GlobalVersion | null | undefined> = {};
pluginIds.forEach((pluginId) => {
result[pluginId] = getPluginIdPreferredVersion(pluginId);
});
return result;
}

View file

@ -14,9 +14,8 @@ import React, {
type ReactNode,
type ComponentType,
} from 'react';
import {ReactContextError} from './reactUtils';
import {usePrevious} from './usePrevious';
import {useNavbarMobileSidebar} from './navbarUtils';
import {ReactContextError, usePrevious} from './reactUtils';
import {useNavbarMobileSidebar} from '../contexts/navbarMobileSidebar';
/*
The idea behind all this is that a specific component must be able to fill a

View file

@ -5,24 +5,15 @@
* LICENSE file in the root directory of this source tree.
*/
import React, {
type ReactNode,
useCallback,
useEffect,
useState,
useMemo,
} from 'react';
import useWindowSize from '../hooks/useWindowSize';
import {useHistoryPopHandler} from './historyUtils';
import {NavbarSecondaryMenuProvider} from './navbarSecondaryMenuUtils';
import {useActivePlugin} from '@docusaurus/plugin-content-docs/client';
import {useThemeConfig} from './useThemeConfig';
import {ReactContextError} from './reactUtils';
import React, {type ReactNode} from 'react';
import {NavbarMobileSidebarProvider} from '../contexts/navbarMobileSidebar';
import {NavbarSecondaryMenuProvider} from './navbarSecondaryMenu';
const DefaultNavItemPosition = 'right';
// If split links by left/right
// if position is unspecified, fallback to right
/**
* Split links by left/right. If position is unspecified, fallback to right.
*/
export function splitNavbarItems<T extends {position?: 'left' | 'right'}>(
items: T[],
): [leftItems: T[], rightItems: T[]] {
@ -36,90 +27,10 @@ export function splitNavbarItems<T extends {position?: 'left' | 'right'}>(
return [leftItems, rightItems];
}
type NavbarMobileSidebarContextValue = {
disabled: boolean;
shouldRender: boolean;
toggle: () => void;
shown: boolean;
};
const NavbarMobileSidebarContext = React.createContext<
NavbarMobileSidebarContextValue | undefined
>(undefined);
// Mobile sidebar can be disabled in case it would lead to an empty sidebar
// In this case it's not useful to display a navbar sidebar toggle button
function useNavbarMobileSidebarDisabled() {
const activeDocPlugin = useActivePlugin();
const {items} = useThemeConfig().navbar;
return items.length === 0 && !activeDocPlugin;
}
function useNavbarMobileSidebarContextValue(): NavbarMobileSidebarContextValue {
const disabled = useNavbarMobileSidebarDisabled();
const windowSize = useWindowSize();
// Mobile sidebar not visible until user interaction: can avoid SSR rendering
const shouldRender = !disabled && windowSize === 'mobile'; // || windowSize === 'ssr';
const [shown, setShown] = useState(false);
// Close mobile sidebar on navigation pop
// Most likely firing when using the Android back button (but not only)
useHistoryPopHandler(() => {
if (shown) {
setShown(false);
// Should we prevent the navigation here?
// See https://github.com/facebook/docusaurus/pull/5462#issuecomment-911699846
return false; // prevent pop navigation
}
return undefined;
});
const toggle = useCallback(() => {
setShown((s) => !s);
}, []);
useEffect(() => {
if (windowSize === 'desktop') {
setShown(false);
}
}, [windowSize]);
// Return stable context value
return useMemo(
() => ({
disabled,
shouldRender,
toggle,
shown,
}),
[disabled, shouldRender, toggle, shown],
);
}
function NavbarMobileSidebarProvider({
children,
}: {
children: ReactNode;
}): JSX.Element {
const value = useNavbarMobileSidebarContextValue();
return (
<NavbarMobileSidebarContext.Provider value={value}>
{children}
</NavbarMobileSidebarContext.Provider>
);
}
export function useNavbarMobileSidebar(): NavbarMobileSidebarContextValue {
const context = React.useContext(NavbarMobileSidebarContext);
if (context == null) {
throw new ReactContextError('NavbarMobileSidebarProvider');
}
return context;
}
// Add all Navbar providers at once
/**
* Composes the `NavbarMobileSidebarProvider` and `NavbarSecondaryMenuProvider`.
* Because the latter depends on the former, they can't be re-ordered.
*/
export function NavbarProvider({children}: {children: ReactNode}): JSX.Element {
return (
<NavbarMobileSidebarProvider>

View file

@ -5,7 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/
// Compare the 2 paths, case insensitive and ignoring trailing slash
/**
* Compare the 2 paths, case insensitive and ignoring trailing slash
*/
export const isSamePath = (
path1: string | undefined,
path2: string | undefined,

View file

@ -6,6 +6,7 @@
*/
import {useCallback, useEffect, useLayoutEffect, useRef} from 'react';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
/**
* This hook is like useLayoutEffect, but without the SSR warning
@ -14,12 +15,13 @@ import {useCallback, useEffect, useLayoutEffect, useRef} from 'react';
* It is useful when you need to update a ref as soon as possible after a React
* render (before `useEffect`)
*/
export const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
export const useIsomorphicLayoutEffect = ExecutionEnvironment.canUseDOM
? useLayoutEffect
: useEffect;
/**
* Permits to transform an unstable callback (like an arrow function provided as
* props) to a "stable" callback that is safe to use in a useEffect dependency
* props) to a "stable" callback that is safe to use in a `useEffect` dependency
* array. Useful to avoid React stale closure problems + avoid useless effect
* re-executions
*
@ -42,6 +44,16 @@ export function useDynamicCallback<T extends (...args: never[]) => unknown>(
return useCallback<T>((...args) => ref.current(...args), []);
}
export function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useIsomorphicLayoutEffect(() => {
ref.current = value;
});
return ref.current;
}
export class ReactContextError extends Error {
constructor(providerName: string, additionalInfo?: string) {
super();

View file

@ -10,8 +10,11 @@ import {useMemo} from 'react';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import type {Route} from '@docusaurus/types';
// Note that all sites don't always have a homepage in practice
// See https://github.com/facebook/docusaurus/pull/6517#issuecomment-1048709116
/**
* Note that sites don't always have a homepage in practice, so we can't assume
* that linking to '/' is always safe.
* @see https://github.com/facebook/docusaurus/pull/6517#issuecomment-1048709116
*/
export function findHomePageRoute({
baseUrl,
routes: initialRoutes,

View file

@ -9,7 +9,7 @@ import {
useAllDocsData,
useActivePluginAndVersion,
} from '@docusaurus/plugin-content-docs/client';
import {useDocsPreferredVersionByPluginId} from './docsPreferredVersion/useDocsPreferredVersion';
import {useDocsPreferredVersionByPluginId} from '../contexts/docsPreferredVersion';
import {docVersionSearchTag, DEFAULT_SEARCH_TAG} from './searchUtils';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';

View file

@ -8,9 +8,11 @@
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import {useLocation} from '@docusaurus/router';
// Get the pathname of current route, without the optional site baseUrl
// - /docs/myDoc => /docs/myDoc
// - /baseUrl/docs/myDoc => /docs/myDoc
/**
* Get the pathname of current route, without the optional site baseUrl
* - /docs/myDoc => /docs/myDoc
* - /baseUrl/docs/myDoc => /docs/myDoc
*/
export function useLocalPathname(): string {
const {
siteConfig: {baseUrl},

View file

@ -8,8 +8,7 @@
import {useEffect} from 'react';
import {useLocation} from '@docusaurus/router';
import type {Location} from 'history';
import {usePrevious} from './usePrevious';
import {useDynamicCallback} from './reactUtils';
import {useDynamicCallback, usePrevious} from './reactUtils';
type LocationChangeEvent = {
location: Location;

View file

@ -50,17 +50,17 @@ function createLocalePluralForms(locale: string): LocalePluralForms {
}
/**
* Poor man's PluralSelector implementation, using an english fallback. We want
* a lightweight, future-proof and good-enough solution. We don't want a perfect
* and heavy solution.
* Poor man's `PluralSelector` implementation, using an English fallback. We
* want a lightweight, future-proof and good-enough solution. We don't want a
* perfect and heavy solution.
*
* Docusaurus classic theme has only 2 deeply nested labels requiring complex
* plural rules. We don't want to use Intl + PluralRules polyfills + full ICU
* syntax (react-intl) just for that.
* plural rules. We don't want to use `Intl` + `PluralRules` polyfills + full
* ICU syntax (react-intl) just for that.
*
* Notes:
* - 2021: 92+% Browsers support Intl.PluralRules, and support will increase in
* the future
* - 2021: 92+% Browsers support `Intl.PluralRules`, and support will increase
* in the future
* - NodeJS >= 13 has full ICU support by default
* - In case of "mismatch" between SSR and Browser ICU support, React keeps
* working!

View file

@ -1,19 +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 {useRef} from 'react';
import {useIsomorphicLayoutEffect} from './reactUtils';
export function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useIsomorphicLayoutEffect(() => {
ref.current = value;
});
return ref.current;
}