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' {
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' {

View file

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

View file

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

View file

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

View file

@ -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 <DefaultNavbarItem {...props} label={label} to={path} />;

View file

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

View file

@ -5,6 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/
// eslint-disable-next-line import/no-named-export
export const getInfimaActiveClassName = (mobile?: boolean): string =>
/* eslint-disable import/no-named-export */
export const getInfimaActiveClassName = (
mobile?: boolean,
): `${'menu' | '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.
* 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,

View file

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

View file

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

View file

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