refactor(theme-classic): extract doc-related navbar items' logic to theme-common (#7067)

This commit is contained in:
Joshua Chen 2022-03-30 14:50:04 +08:00 committed by GitHub
parent fd24bd180d
commit 13e7de853e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 166 additions and 135 deletions

View file

@ -780,7 +780,14 @@ declare module '@theme/NavbarItem' {
} }
declare module '@theme/NavbarItem/utils' { 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' { declare module '@theme/PaginatorNavLink' {

View file

@ -7,30 +7,11 @@
import React from 'react'; import React from 'react';
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem'; import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
import { import {useActiveDocContext} from '@docusaurus/plugin-content-docs/client';
useLatestVersion,
useActiveDocContext,
} from '@docusaurus/plugin-content-docs/client';
import clsx from 'clsx'; import clsx from 'clsx';
import {getInfimaActiveClassName} from '@theme/NavbarItem/utils'; import {getInfimaActiveClassName} from '@theme/NavbarItem/utils';
import type {Props} from '@theme/NavbarItem/DocNavbarItem'; import type {Props} from '@theme/NavbarItem/DocNavbarItem';
import {useDocsPreferredVersion, uniq} from '@docusaurus/theme-common'; import {useLayoutDoc} 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;
}
export default function DocNavbarItem({ export default function DocNavbarItem({
docId, docId,
@ -38,17 +19,8 @@ export default function DocNavbarItem({
docsPluginId, docsPluginId,
...props ...props
}: Props): JSX.Element { }: Props): JSX.Element {
const {activeVersion, activeDoc} = useActiveDocContext(docsPluginId); const {activeDoc} = useActiveDocContext(docsPluginId);
const {preferredVersion} = useDocsPreferredVersion(docsPluginId); const doc = useLayoutDoc(docId, 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 activeDocInfimaClassName = getInfimaActiveClassName(props.mobile); const activeDocInfimaClassName = getInfimaActiveClassName(props.mobile);
return ( return (
@ -57,6 +29,9 @@ export default function DocNavbarItem({
{...props} {...props}
className={clsx(props.className, { className={clsx(props.className, {
[activeDocInfimaClassName]: [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, activeDoc?.sidebar && activeDoc.sidebar === doc.sidebar,
})} })}
activeClassName={activeDocInfimaClassName} activeClassName={activeDocInfimaClassName}

View file

@ -7,48 +7,12 @@
import React from 'react'; import React from 'react';
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem'; import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
import { import {useActiveDocContext} from '@docusaurus/plugin-content-docs/client';
useLatestVersion,
useActiveDocContext,
} from '@docusaurus/plugin-content-docs/client';
import clsx from 'clsx'; import clsx from 'clsx';
import {getInfimaActiveClassName} from '@theme/NavbarItem/utils'; 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 {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({ export default function DocSidebarNavbarItem({
sidebarId, sidebarId,
@ -56,17 +20,13 @@ export default function DocSidebarNavbarItem({
docsPluginId, docsPluginId,
...props ...props
}: Props): JSX.Element { }: Props): JSX.Element {
const {activeVersion, activeDoc} = useActiveDocContext(docsPluginId); const {activeDoc} = useActiveDocContext(docsPluginId);
const {preferredVersion} = useDocsPreferredVersion(docsPluginId); const sidebarLink = useLayoutDocsSidebar(sidebarId, docsPluginId).link;
const latestVersion = useLatestVersion(docsPluginId); if (!sidebarLink) {
throw new Error(
// Versions used to look for the doc to link to, ordered + no duplicate `DocSidebarNavbarItem: Sidebar with ID "${sidebarId}" doesn't have anything to be linked to.`,
const versions = uniq( );
[activeVersion, preferredVersion, latestVersion].filter( }
Boolean,
) as GlobalVersion[],
);
const sidebarLink = getSidebarLink(versions, sidebarId);
const activeDocInfimaClassName = getInfimaActiveClassName(props.mobile); const activeDocInfimaClassName = getInfimaActiveClassName(props.mobile);
return ( return (

View file

@ -10,14 +10,15 @@ import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem'; import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem';
import { import {
useVersions, useVersions,
useLatestVersion,
useActiveDocContext, useActiveDocContext,
} from '@docusaurus/plugin-content-docs/client'; } from '@docusaurus/plugin-content-docs/client';
import type {Props} from '@theme/NavbarItem/DocsVersionDropdownNavbarItem'; import {
import {useDocsPreferredVersion} from '@docusaurus/theme-common'; useDocsPreferredVersion,
useDocsVersionCandidates,
} from '@docusaurus/theme-common';
import {translate} from '@docusaurus/Translate'; import {translate} from '@docusaurus/Translate';
import type {GlobalVersion} from '@docusaurus/plugin-content-docs/client'; 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) => const getVersionMainDoc = (version: GlobalVersion) =>
version.docs.find((doc) => doc.id === version.mainDocId)!; version.docs.find((doc) => doc.id === version.mainDocId)!;
@ -32,36 +33,28 @@ export default function DocsVersionDropdownNavbarItem({
}: Props): JSX.Element { }: Props): JSX.Element {
const activeDocContext = useActiveDocContext(docsPluginId); const activeDocContext = useActiveDocContext(docsPluginId);
const versions = useVersions(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} = const dropdownVersion = useDocsVersionCandidates(docsPluginId)[0];
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;
// Mobile dropdown is handled a bit differently // Mobile dropdown is handled a bit differently
const dropdownLabel = const dropdownLabel =

View file

@ -7,13 +7,9 @@
import React from 'react'; import React from 'react';
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem'; import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
import { import {useDocsVersionCandidates} from '@docusaurus/theme-common';
useActiveVersion, import type {GlobalVersion} from '@docusaurus/plugin-content-docs/client';
useLatestVersion,
type GlobalVersion,
} from '@docusaurus/plugin-content-docs/client';
import type {Props} from '@theme/NavbarItem/DocsVersionNavbarItem'; import type {Props} from '@theme/NavbarItem/DocsVersionNavbarItem';
import {useDocsPreferredVersion} from '@docusaurus/theme-common';
const getVersionMainDoc = (version: GlobalVersion) => const getVersionMainDoc = (version: GlobalVersion) =>
version.docs.find((doc) => doc.id === version.mainDocId)!; version.docs.find((doc) => doc.id === version.mainDocId)!;
@ -24,10 +20,7 @@ export default function DocsVersionNavbarItem({
docsPluginId, docsPluginId,
...props ...props
}: Props): JSX.Element { }: Props): JSX.Element {
const activeVersion = useActiveVersion(docsPluginId); const version = useDocsVersionCandidates(docsPluginId)[0];
const {preferredVersion} = useDocsPreferredVersion(docsPluginId);
const latestVersion = useLatestVersion(docsPluginId);
const version = activeVersion ?? preferredVersion ?? latestVersion;
const label = staticLabel ?? version.label; const label = staticLabel ?? version.label;
const path = staticTo ?? getVersionMainDoc(version).path; const path = staticTo ?? getVersionMainDoc(version).path;
return <DefaultNavbarItem {...props} label={label} to={path} />; return <DefaultNavbarItem {...props} label={label} to={path} />;

View file

@ -27,10 +27,6 @@ export default function LocaleDropdownNavbarItem({
} = useDocusaurusContext(); } = useDocusaurusContext();
const alternatePageUtils = useAlternatePageUtils(); const alternatePageUtils = useAlternatePageUtils();
function getLocaleLabel(locale: string) {
return localeConfigs[locale]!.label;
}
const localeItems = locales.map((locale): LinkLikeNavbarItemProps => { const localeItems = locales.map((locale): LinkLikeNavbarItemProps => {
const to = `pathname://${alternatePageUtils.createUrl({ const to = `pathname://${alternatePageUtils.createUrl({
locale, locale,
@ -38,7 +34,7 @@ export default function LocaleDropdownNavbarItem({
})}`; })}`;
return { return {
isNavLink: true, isNavLink: true,
label: getLocaleLabel(locale), label: localeConfigs[locale]!.label,
to, to,
target: '_self', target: '_self',
autoAddBaseUrl: false, autoAddBaseUrl: false,
@ -55,7 +51,7 @@ export default function LocaleDropdownNavbarItem({
id: 'theme.navbar.mobileLanguageDropdown.label', id: 'theme.navbar.mobileLanguageDropdown.label',
description: 'The label for the mobile language switcher dropdown', description: 'The label for the mobile language switcher dropdown',
}) })
: getLocaleLabel(currentLocale); : localeConfigs[currentLocale]!.label;
return ( return (
<DropdownNavbarItem <DropdownNavbarItem

View file

@ -5,6 +5,9 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
// eslint-disable-next-line import/no-named-export /* eslint-disable import/no-named-export */
export const getInfimaActiveClassName = (mobile?: boolean): string =>
export const getInfimaActiveClassName = (
mobile?: boolean,
): `${'menu' | 'navbar'}__link--active` =>
mobile ? 'menu__link--active' : 'navbar__link--active'; mobile ? 'menu__link--active' : 'navbar__link--active';

View file

@ -198,8 +198,11 @@ function useDocsPreferredVersionContext(): ContextValue {
} }
/** /**
* Returns a read-write interface to a plugin's preferred version. * Returns a read-write interface to a plugin's preferred version. The
* Note, the `preferredVersion` attribute will always be `null` before mount. * "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( export function useDocsPreferredVersion(
pluginId: string | undefined = DEFAULT_PLUGIN_ID, pluginId: string | undefined = DEFAULT_PLUGIN_ID,

View file

@ -50,6 +50,9 @@ export {
useCurrentSidebarCategory, useCurrentSidebarCategory,
isActiveSidebarItem, isActiveSidebarItem,
useSidebarBreadcrumbs, useSidebarBreadcrumbs,
useDocsVersionCandidates,
useLayoutDoc,
useLayoutDocsSidebar,
} from './utils/docsUtils'; } from './utils/docsUtils';
export {useTitleFormatter} from './utils/generalUtils'; export {useTitleFormatter} from './utils/generalUtils';

View file

@ -5,9 +5,15 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import {useMemo} from 'react';
import { import {
useAllDocsData, useAllDocsData,
useActivePlugin, useActivePlugin,
useActiveDocContext,
useLatestVersion,
type GlobalVersion,
type GlobalSidebar,
type GlobalDoc,
} from '@docusaurus/plugin-content-docs/client'; } from '@docusaurus/plugin-content-docs/client';
import type { import type {
PropSidebar, PropSidebar,
@ -16,8 +22,10 @@ import type {
PropVersionDoc, PropVersionDoc,
PropSidebarBreadcrumbsItem, PropSidebarBreadcrumbsItem,
} from '@docusaurus/plugin-content-docs'; } from '@docusaurus/plugin-content-docs';
import {useDocsPreferredVersion} from '../contexts/docsPreferredVersion';
import {useDocsVersion} from '../contexts/docsVersion'; import {useDocsVersion} from '../contexts/docsVersion';
import {useDocsSidebar} from '../contexts/docsSidebar'; import {useDocsSidebar} from '../contexts/docsSidebar';
import {uniq} from './jsUtils';
import {isSamePath} from './routesUtils'; import {isSamePath} from './routesUtils';
import {useLocation} from '@docusaurus/router'; import {useLocation} from '@docusaurus/router';
@ -178,3 +186,91 @@ export function useSidebarBreadcrumbs(): PropSidebarBreadcrumbsItem[] | null {
return breadcrumbs.reverse(); 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]);
}

View file

@ -35,6 +35,8 @@ export function useContextualSearchFilters(): {locale: string; tags: string[]} {
const activePluginAndVersion = useActivePluginAndVersion(); const activePluginAndVersion = useActivePluginAndVersion();
const docsPreferredVersionByPluginId = useDocsPreferredVersionByPluginId(); const docsPreferredVersionByPluginId = useDocsPreferredVersionByPluginId();
// This can't use more specialized hooks because we are mapping over all
// plugin instances.
function getDocPluginTags(pluginId: string) { function getDocPluginTags(pluginId: string) {
const activeVersion = const activeVersion =
activePluginAndVersion?.activePlugin?.pluginId === pluginId activePluginAndVersion?.activePlugin?.pluginId === pluginId