From 13e7de853e0f0968916b60bae769bd49d224a19d Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Wed, 30 Mar 2022 14:50:04 +0800 Subject: [PATCH] refactor(theme-classic): extract doc-related navbar items' logic to theme-common (#7067) --- .../src/theme-classic.d.ts | 9 +- .../src/theme/NavbarItem/DocNavbarItem.tsx | 39 ++------ .../theme/NavbarItem/DocSidebarNavbarItem.tsx | 58 ++--------- .../DocsVersionDropdownNavbarItem.tsx | 59 +++++------- .../NavbarItem/DocsVersionNavbarItem.tsx | 13 +-- .../LocaleDropdownNavbarItem/index.tsx | 8 +- .../src/theme/NavbarItem/utils.ts | 7 +- .../src/contexts/docsPreferredVersion.tsx | 7 +- packages/docusaurus-theme-common/src/index.ts | 3 + .../src/utils/docsUtils.tsx | 96 +++++++++++++++++++ .../src/utils/searchUtils.ts | 2 + 11 files changed, 166 insertions(+), 135 deletions(-) diff --git a/packages/docusaurus-theme-classic/src/theme-classic.d.ts b/packages/docusaurus-theme-classic/src/theme-classic.d.ts index 04e8b6ccd9..9f3c48ed3d 100644 --- a/packages/docusaurus-theme-classic/src/theme-classic.d.ts +++ b/packages/docusaurus-theme-classic/src/theme-classic.d.ts @@ -780,7 +780,14 @@ declare module '@theme/NavbarItem' { } declare module '@theme/NavbarItem/utils' { - export function getInfimaActiveClassName(mobile?: boolean): string; + /** + * On desktop and mobile, we would apply different class names for dropdown + * items. + * @see https://github.com/facebook/docusaurus/pull/5431 + */ + export function getInfimaActiveClassName( + mobile?: boolean, + ): `${'menu' | 'navbar'}__link--active`; } declare module '@theme/PaginatorNavLink' { diff --git a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocNavbarItem.tsx b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocNavbarItem.tsx index 8c56cebf24..a5e9b06f02 100644 --- a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocNavbarItem.tsx +++ b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocNavbarItem.tsx @@ -7,30 +7,11 @@ import React from 'react'; import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem'; -import { - useLatestVersion, - useActiveDocContext, -} from '@docusaurus/plugin-content-docs/client'; +import {useActiveDocContext} from '@docusaurus/plugin-content-docs/client'; import clsx from 'clsx'; import {getInfimaActiveClassName} from '@theme/NavbarItem/utils'; import type {Props} from '@theme/NavbarItem/DocNavbarItem'; -import {useDocsPreferredVersion, uniq} from '@docusaurus/theme-common'; -import type {GlobalVersion} from '@docusaurus/plugin-content-docs/client'; - -function getDocInVersions(versions: GlobalVersion[], docId: string) { - const allDocs = versions.flatMap((version) => version.docs); - const doc = allDocs.find((versionDoc) => versionDoc.id === docId); - if (!doc) { - const docIds = allDocs.map((versionDoc) => versionDoc.id).join('\n- '); - throw new Error( - `DocNavbarItem: couldn't find any doc with id "${docId}" in version${ - versions.length ? 's' : '' - } ${versions.map((version) => version.name).join(', ')}". -Available doc ids are:\n- ${docIds}`, - ); - } - return doc; -} +import {useLayoutDoc} from '@docusaurus/theme-common'; export default function DocNavbarItem({ docId, @@ -38,17 +19,8 @@ export default function DocNavbarItem({ docsPluginId, ...props }: Props): JSX.Element { - const {activeVersion, activeDoc} = useActiveDocContext(docsPluginId); - const {preferredVersion} = useDocsPreferredVersion(docsPluginId); - const latestVersion = useLatestVersion(docsPluginId); - - // Versions used to look for the doc to link to, ordered + no duplicate - const versions = uniq( - [activeVersion, preferredVersion, latestVersion].filter( - Boolean, - ) as GlobalVersion[], - ); - const doc = getDocInVersions(versions, docId); + const {activeDoc} = useActiveDocContext(docsPluginId); + const doc = useLayoutDoc(docId, docsPluginId); const activeDocInfimaClassName = getInfimaActiveClassName(props.mobile); return ( @@ -57,6 +29,9 @@ export default function DocNavbarItem({ {...props} className={clsx(props.className, { [activeDocInfimaClassName]: + // Do not make the item active if the active doc doesn't have sidebar. + // If `activeDoc === doc` react-router will make it active anyways, + // regardless of the existence of a sidebar activeDoc?.sidebar && activeDoc.sidebar === doc.sidebar, })} activeClassName={activeDocInfimaClassName} diff --git a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocSidebarNavbarItem.tsx b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocSidebarNavbarItem.tsx index ca6ec3d866..9656648556 100644 --- a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocSidebarNavbarItem.tsx +++ b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocSidebarNavbarItem.tsx @@ -7,48 +7,12 @@ import React from 'react'; import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem'; -import { - useLatestVersion, - useActiveDocContext, -} from '@docusaurus/plugin-content-docs/client'; +import {useActiveDocContext} from '@docusaurus/plugin-content-docs/client'; import clsx from 'clsx'; import {getInfimaActiveClassName} from '@theme/NavbarItem/utils'; -import {useDocsPreferredVersion, uniq} from '@docusaurus/theme-common'; +import {useLayoutDocsSidebar} from '@docusaurus/theme-common'; import type {Props} from '@theme/NavbarItem/DocSidebarNavbarItem'; -import type { - GlobalVersion, - GlobalSidebar, -} from '@docusaurus/plugin-content-docs/client'; - -function getSidebarLink(versions: GlobalVersion[], sidebarId: string) { - const allSidebars = versions - .flatMap((version) => { - if (version.sidebars) { - return Object.entries(version.sidebars); - } - return undefined; - }) - .filter( - (sidebarItem): sidebarItem is [string, GlobalSidebar] => !!sidebarItem, - ); - const sidebarEntry = allSidebars.find((sidebar) => sidebar[0] === sidebarId); - if (!sidebarEntry) { - throw new Error( - `DocSidebarNavbarItem: couldn't find any sidebar with id "${sidebarId}" in version${ - versions.length ? 's' : '' - } ${versions.map((version) => version.name).join(', ')}". -Available sidebar ids are: -- ${Object.keys(allSidebars).join('\n- ')}`, - ); - } - if (!sidebarEntry[1].link) { - throw new Error( - `DocSidebarNavbarItem: couldn't find any document for sidebar with id "${sidebarId}"`, - ); - } - return sidebarEntry[1].link; -} export default function DocSidebarNavbarItem({ sidebarId, @@ -56,17 +20,13 @@ export default function DocSidebarNavbarItem({ docsPluginId, ...props }: Props): JSX.Element { - const {activeVersion, activeDoc} = useActiveDocContext(docsPluginId); - const {preferredVersion} = useDocsPreferredVersion(docsPluginId); - const latestVersion = useLatestVersion(docsPluginId); - - // Versions used to look for the doc to link to, ordered + no duplicate - const versions = uniq( - [activeVersion, preferredVersion, latestVersion].filter( - Boolean, - ) as GlobalVersion[], - ); - const sidebarLink = getSidebarLink(versions, sidebarId); + const {activeDoc} = useActiveDocContext(docsPluginId); + const sidebarLink = useLayoutDocsSidebar(sidebarId, docsPluginId).link; + if (!sidebarLink) { + throw new Error( + `DocSidebarNavbarItem: Sidebar with ID "${sidebarId}" doesn't have anything to be linked to.`, + ); + } const activeDocInfimaClassName = getInfimaActiveClassName(props.mobile); return ( diff --git a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocsVersionDropdownNavbarItem.tsx b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocsVersionDropdownNavbarItem.tsx index 2cf1747835..9624dc3dde 100644 --- a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocsVersionDropdownNavbarItem.tsx +++ b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocsVersionDropdownNavbarItem.tsx @@ -10,14 +10,15 @@ import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem'; import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem'; import { useVersions, - useLatestVersion, useActiveDocContext, } from '@docusaurus/plugin-content-docs/client'; -import type {Props} from '@theme/NavbarItem/DocsVersionDropdownNavbarItem'; -import {useDocsPreferredVersion} from '@docusaurus/theme-common'; +import { + useDocsPreferredVersion, + useDocsVersionCandidates, +} from '@docusaurus/theme-common'; import {translate} from '@docusaurus/Translate'; import type {GlobalVersion} from '@docusaurus/plugin-content-docs/client'; -import type {LinkLikeNavbarItemProps} from '@theme/NavbarItem'; +import type {Props} from '@theme/NavbarItem/DocsVersionDropdownNavbarItem'; const getVersionMainDoc = (version: GlobalVersion) => version.docs.find((doc) => doc.id === version.mainDocId)!; @@ -32,36 +33,28 @@ export default function DocsVersionDropdownNavbarItem({ }: Props): JSX.Element { const activeDocContext = useActiveDocContext(docsPluginId); const versions = useVersions(docsPluginId); - const latestVersion = useLatestVersion(docsPluginId); + const {savePreferredVersionName} = useDocsPreferredVersion(docsPluginId); + const versionLinks = versions.map((version) => { + // We try to link to the same doc, in another version + // When not possible, fallback to the "main doc" of the version + const versionDoc = + activeDocContext?.alternateDocVersions[version.name] ?? + getVersionMainDoc(version); + return { + isNavLink: true, + label: version.label, + to: versionDoc.path, + isActive: () => version === activeDocContext?.activeVersion, + onClick: () => savePreferredVersionName(version.name), + }; + }); + const items = [ + ...dropdownItemsBefore, + ...versionLinks, + ...dropdownItemsAfter, + ]; - const {preferredVersion, savePreferredVersionName} = - useDocsPreferredVersion(docsPluginId); - - function getItems(): LinkLikeNavbarItemProps[] { - const versionLinks = versions.map((version) => { - // We try to link to the same doc, in another version - // When not possible, fallback to the "main doc" of the version - const versionDoc = - activeDocContext?.alternateDocVersions[version.name] || - getVersionMainDoc(version); - return { - isNavLink: true, - label: version.label, - to: versionDoc.path, - isActive: () => version === activeDocContext?.activeVersion, - onClick: () => { - savePreferredVersionName(version.name); - }, - }; - }); - - return [...dropdownItemsBefore, ...versionLinks, ...dropdownItemsAfter]; - } - - const items = getItems(); - - const dropdownVersion = - activeDocContext.activeVersion ?? preferredVersion ?? latestVersion; + const dropdownVersion = useDocsVersionCandidates(docsPluginId)[0]; // Mobile dropdown is handled a bit differently const dropdownLabel = diff --git a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocsVersionNavbarItem.tsx b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocsVersionNavbarItem.tsx index 5d88a534ae..db68737643 100644 --- a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocsVersionNavbarItem.tsx +++ b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocsVersionNavbarItem.tsx @@ -7,13 +7,9 @@ import React from 'react'; import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem'; -import { - useActiveVersion, - useLatestVersion, - type GlobalVersion, -} from '@docusaurus/plugin-content-docs/client'; +import {useDocsVersionCandidates} from '@docusaurus/theme-common'; +import type {GlobalVersion} from '@docusaurus/plugin-content-docs/client'; import type {Props} from '@theme/NavbarItem/DocsVersionNavbarItem'; -import {useDocsPreferredVersion} from '@docusaurus/theme-common'; const getVersionMainDoc = (version: GlobalVersion) => version.docs.find((doc) => doc.id === version.mainDocId)!; @@ -24,10 +20,7 @@ export default function DocsVersionNavbarItem({ docsPluginId, ...props }: Props): JSX.Element { - const activeVersion = useActiveVersion(docsPluginId); - const {preferredVersion} = useDocsPreferredVersion(docsPluginId); - const latestVersion = useLatestVersion(docsPluginId); - const version = activeVersion ?? preferredVersion ?? latestVersion; + const version = useDocsVersionCandidates(docsPluginId)[0]; const label = staticLabel ?? version.label; const path = staticTo ?? getVersionMainDoc(version).path; return ; diff --git a/packages/docusaurus-theme-classic/src/theme/NavbarItem/LocaleDropdownNavbarItem/index.tsx b/packages/docusaurus-theme-classic/src/theme/NavbarItem/LocaleDropdownNavbarItem/index.tsx index 13e014c4ca..bec77b1a30 100644 --- a/packages/docusaurus-theme-classic/src/theme/NavbarItem/LocaleDropdownNavbarItem/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/NavbarItem/LocaleDropdownNavbarItem/index.tsx @@ -27,10 +27,6 @@ export default function LocaleDropdownNavbarItem({ } = useDocusaurusContext(); const alternatePageUtils = useAlternatePageUtils(); - function getLocaleLabel(locale: string) { - return localeConfigs[locale]!.label; - } - const localeItems = locales.map((locale): LinkLikeNavbarItemProps => { const to = `pathname://${alternatePageUtils.createUrl({ locale, @@ -38,7 +34,7 @@ export default function LocaleDropdownNavbarItem({ })}`; return { isNavLink: true, - label: getLocaleLabel(locale), + label: localeConfigs[locale]!.label, to, target: '_self', autoAddBaseUrl: false, @@ -55,7 +51,7 @@ export default function LocaleDropdownNavbarItem({ id: 'theme.navbar.mobileLanguageDropdown.label', description: 'The label for the mobile language switcher dropdown', }) - : getLocaleLabel(currentLocale); + : localeConfigs[currentLocale]!.label; return ( +/* eslint-disable import/no-named-export */ + +export const getInfimaActiveClassName = ( + mobile?: boolean, +): `${'menu' | 'navbar'}__link--active` => mobile ? 'menu__link--active' : 'navbar__link--active'; diff --git a/packages/docusaurus-theme-common/src/contexts/docsPreferredVersion.tsx b/packages/docusaurus-theme-common/src/contexts/docsPreferredVersion.tsx index a2da2c71b6..0922c2c33c 100644 --- a/packages/docusaurus-theme-common/src/contexts/docsPreferredVersion.tsx +++ b/packages/docusaurus-theme-common/src/contexts/docsPreferredVersion.tsx @@ -198,8 +198,11 @@ function useDocsPreferredVersionContext(): ContextValue { } /** - * Returns a read-write interface to a plugin's preferred version. - * Note, the `preferredVersion` attribute will always be `null` before mount. + * Returns a read-write interface to a plugin's preferred version. The + * "preferred version" is defined as the last version that the user visited. + * For example, if a user is using v3, even when v4 is later published, the user + * would still be browsing v3 docs when she opens the website next time. Note, + * the `preferredVersion` attribute will always be `null` before mount. */ export function useDocsPreferredVersion( pluginId: string | undefined = DEFAULT_PLUGIN_ID, diff --git a/packages/docusaurus-theme-common/src/index.ts b/packages/docusaurus-theme-common/src/index.ts index 18820aa13f..77afa2fdfd 100644 --- a/packages/docusaurus-theme-common/src/index.ts +++ b/packages/docusaurus-theme-common/src/index.ts @@ -50,6 +50,9 @@ export { useCurrentSidebarCategory, isActiveSidebarItem, useSidebarBreadcrumbs, + useDocsVersionCandidates, + useLayoutDoc, + useLayoutDocsSidebar, } from './utils/docsUtils'; export {useTitleFormatter} from './utils/generalUtils'; diff --git a/packages/docusaurus-theme-common/src/utils/docsUtils.tsx b/packages/docusaurus-theme-common/src/utils/docsUtils.tsx index e5e54478b4..64e700a487 100644 --- a/packages/docusaurus-theme-common/src/utils/docsUtils.tsx +++ b/packages/docusaurus-theme-common/src/utils/docsUtils.tsx @@ -5,9 +5,15 @@ * LICENSE file in the root directory of this source tree. */ +import {useMemo} from 'react'; import { useAllDocsData, useActivePlugin, + useActiveDocContext, + useLatestVersion, + type GlobalVersion, + type GlobalSidebar, + type GlobalDoc, } from '@docusaurus/plugin-content-docs/client'; import type { PropSidebar, @@ -16,8 +22,10 @@ import type { PropVersionDoc, PropSidebarBreadcrumbsItem, } from '@docusaurus/plugin-content-docs'; +import {useDocsPreferredVersion} from '../contexts/docsPreferredVersion'; import {useDocsVersion} from '../contexts/docsVersion'; import {useDocsSidebar} from '../contexts/docsSidebar'; +import {uniq} from './jsUtils'; import {isSamePath} from './routesUtils'; import {useLocation} from '@docusaurus/router'; @@ -178,3 +186,91 @@ export function useSidebarBreadcrumbs(): PropSidebarBreadcrumbsItem[] | null { return breadcrumbs.reverse(); } + +/** + * "Version candidates" are mostly useful for the layout components, which must + * be able to work on all pages. For example, if a user has `{ type: "doc", + * docId: "intro" }` as a navbar item, which version does that refer to? We + * believe that it could refer to at most three version candidates: + * + * 1. The **active version**, the one that the user is currently browsing. See + * {@link useActiveDocContext}. + * 2. The **preferred version**, the one that the user last visited. See + * {@link useDocsPreferredVersion}. + * 3. The **latest version**, the "default". See {@link useLatestVersion}. + * + * @param docsPluginId The plugin ID to get versions from. + * @returns An array of 1~3 versions with priorities defined above, guaranteed + * to be unique and non-sparse. Will be memoized, hence stable for deps array. + */ +export function useDocsVersionCandidates( + docsPluginId?: string, +): [GlobalVersion, ...GlobalVersion[]] { + const {activeVersion} = useActiveDocContext(docsPluginId); + const {preferredVersion} = useDocsPreferredVersion(docsPluginId); + const latestVersion = useLatestVersion(docsPluginId); + return useMemo( + () => + uniq( + [activeVersion, preferredVersion, latestVersion].filter(Boolean), + ) as [GlobalVersion, ...GlobalVersion[]], + [activeVersion, preferredVersion, latestVersion], + ); +} + +/** + * The layout components, like navbar items, must be able to work on all pages, + * even on non-doc ones. This hook would always return a sidebar to be linked + * to. See also {@link useDocsVersionCandidates} for how this selection is done. + * + * @throws This hook throws if a sidebar with said ID is not found. + */ +export function useLayoutDocsSidebar( + sidebarId: string, + docsPluginId?: string, +): GlobalSidebar { + const versions = useDocsVersionCandidates(docsPluginId); + return useMemo(() => { + const allSidebars = versions.flatMap((version) => + version.sidebars ? Object.entries(version.sidebars) : [], + ); + const sidebarEntry = allSidebars.find( + (sidebar) => sidebar[0] === sidebarId, + ); + if (!sidebarEntry) { + throw new Error( + `Can't find any sidebar with id "${sidebarId}" in version${ + versions.length > 1 ? 's' : '' + } ${versions.map((version) => version.name).join(', ')}". + Available sidebar ids are: + - ${Object.keys(allSidebars).join('\n- ')}`, + ); + } + return sidebarEntry[1]; + }, [sidebarId, versions]); +} + +/** + * The layout components, like navbar items, must be able to work on all pages, + * even on non-doc ones. This hook would always return a doc to be linked + * to. See also {@link useDocsVersionCandidates} for how this selection is done. + * + * @throws This hook throws if a doc with said ID is not found. + */ +export function useLayoutDoc(docId: string, docsPluginId?: string): GlobalDoc { + const versions = useDocsVersionCandidates(docsPluginId); + return useMemo(() => { + const allDocs = versions.flatMap((version) => version.docs); + const doc = allDocs.find((versionDoc) => versionDoc.id === docId); + if (!doc) { + throw new Error( + `DocNavbarItem: couldn't find any doc with id "${docId}" in version${ + versions.length > 1 ? 's' : '' + } ${versions.map((version) => version.name).join(', ')}". +Available doc ids are: +- ${uniq(allDocs.map((versionDoc) => versionDoc.id)).join('\n- ')}`, + ); + } + return doc; + }, [docId, versions]); +} diff --git a/packages/docusaurus-theme-common/src/utils/searchUtils.ts b/packages/docusaurus-theme-common/src/utils/searchUtils.ts index 57fdb3635f..463c69a90f 100644 --- a/packages/docusaurus-theme-common/src/utils/searchUtils.ts +++ b/packages/docusaurus-theme-common/src/utils/searchUtils.ts @@ -35,6 +35,8 @@ export function useContextualSearchFilters(): {locale: string; tags: string[]} { const activePluginAndVersion = useActivePluginAndVersion(); const docsPreferredVersionByPluginId = useDocsPreferredVersionByPluginId(); + // This can't use more specialized hooks because we are mapping over all + // plugin instances. function getDocPluginTags(pluginId: string) { const activeVersion = activePluginAndVersion?.activePlugin?.pluginId === pluginId