mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-17 19:16:58 +02:00
feat(docs,theme-classic): docs breadcrumbs (#6517)
Co-authored-by: sebastienlorber <lorber.sebastien@gmail.com>
This commit is contained in:
parent
67918e35e2
commit
3629b5ab39
16 changed files with 341 additions and 1 deletions
|
@ -242,6 +242,7 @@ exports[`simple website content 5`] = `
|
|||
Object {
|
||||
"pluginName": Object {
|
||||
"pluginId": Object {
|
||||
"breadcrumbs": true,
|
||||
"path": "/docs",
|
||||
"versions": Array [
|
||||
Object {
|
||||
|
@ -955,6 +956,7 @@ exports[`simple website content: global data 1`] = `
|
|||
Object {
|
||||
"pluginName": Object {
|
||||
"pluginId": Object {
|
||||
"breadcrumbs": true,
|
||||
"path": "/docs",
|
||||
"versions": Array [
|
||||
Object {
|
||||
|
@ -2411,6 +2413,7 @@ exports[`versioned website (community) content: global data 1`] = `
|
|||
Object {
|
||||
"pluginName": Object {
|
||||
"pluginId": Object {
|
||||
"breadcrumbs": true,
|
||||
"path": "/community",
|
||||
"versions": Array [
|
||||
Object {
|
||||
|
@ -3450,6 +3453,7 @@ exports[`versioned website content: global data 1`] = `
|
|||
Object {
|
||||
"pluginName": Object {
|
||||
"pluginId": Object {
|
||||
"breadcrumbs": true,
|
||||
"path": "/docs",
|
||||
"versions": Array [
|
||||
Object {
|
||||
|
|
|
@ -56,6 +56,7 @@ describe('normalizeDocsPluginOptions', () => {
|
|||
rehypePlugins: [markdownPluginsFunctionStub],
|
||||
beforeDefaultRehypePlugins: [],
|
||||
beforeDefaultRemarkPlugins: [],
|
||||
breadcrumbs: true,
|
||||
showLastUpdateTime: true,
|
||||
showLastUpdateAuthor: true,
|
||||
admonitions: {},
|
||||
|
|
|
@ -217,6 +217,7 @@ export default async function pluginContentDocs(
|
|||
docLayoutComponent,
|
||||
docItemComponent,
|
||||
docCategoryGeneratedIndexComponent,
|
||||
breadcrumbs,
|
||||
} = options;
|
||||
const {addRoute, createData, setGlobalData} = actions;
|
||||
|
||||
|
@ -295,6 +296,7 @@ export default async function pluginContentDocs(
|
|||
setGlobalData<GlobalPluginData>({
|
||||
path: normalizeUrl([baseUrl, options.routeBasePath]),
|
||||
versions: loadedVersions.map(toGlobalDataVersion),
|
||||
breadcrumbs,
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -55,6 +55,7 @@ export const DEFAULT_OPTIONS: Omit<PluginOptions, 'id' | 'sidebarPath'> = {
|
|||
editLocalizedFiles: false,
|
||||
sidebarCollapsible: true,
|
||||
sidebarCollapsed: true,
|
||||
breadcrumbs: true,
|
||||
};
|
||||
|
||||
const VersionOptionsSchema = Joi.object({
|
||||
|
@ -139,6 +140,7 @@ export const OptionsSchema = Joi.object({
|
|||
disableVersioning: Joi.bool().default(DEFAULT_OPTIONS.disableVersioning),
|
||||
lastVersion: Joi.string().optional(),
|
||||
versions: VersionsOptionsSchema,
|
||||
breadcrumbs: Joi.bool().default(DEFAULT_OPTIONS.breadcrumbs),
|
||||
});
|
||||
|
||||
export function validateOptions({
|
||||
|
|
|
@ -38,6 +38,7 @@ declare module '@docusaurus/plugin-content-docs' {
|
|||
showLastUpdateTime?: boolean;
|
||||
showLastUpdateAuthor?: boolean;
|
||||
numberPrefixParser: NumberPrefixParser;
|
||||
breadcrumbs: boolean;
|
||||
};
|
||||
|
||||
export type PathOptions = {
|
||||
|
@ -126,6 +127,8 @@ declare module '@docusaurus/plugin-content-docs' {
|
|||
export type PropSidebarItemCategory =
|
||||
import('./sidebars/types').PropSidebarItemCategory;
|
||||
export type PropSidebarItem = import('./sidebars/types').PropSidebarItem;
|
||||
export type PropSidebarBreadcrumbsItem =
|
||||
import('./sidebars/types').PropSidebarBreadcrumbsItem;
|
||||
export type PropSidebar = import('./sidebars/types').PropSidebar;
|
||||
export type PropSidebars = import('./sidebars/types').PropSidebars;
|
||||
|
||||
|
@ -237,6 +240,10 @@ declare module '@theme/DocTagDocListPage' {
|
|||
export default function DocTagDocListPage(props: Props): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/DocBreadcrumbs' {
|
||||
export default function DocBreadcrumbs(): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/DocPage' {
|
||||
import type {PropVersionMetadata} from '@docusaurus/plugin-content-docs';
|
||||
import type {DocumentRoute} from '@theme/DocItem';
|
||||
|
@ -294,6 +301,7 @@ declare module '@docusaurus/plugin-content-docs/client' {
|
|||
export type GlobalPluginData = {
|
||||
path: string;
|
||||
versions: GlobalVersion[];
|
||||
breadcrumbs: boolean;
|
||||
};
|
||||
export type DocVersionSuggestions = {
|
||||
// suggest the latest version
|
||||
|
|
|
@ -195,6 +195,10 @@ export type PropSidebars = {
|
|||
[sidebarId: string]: PropSidebar;
|
||||
};
|
||||
|
||||
export type PropSidebarBreadcrumbsItem =
|
||||
| PropSidebarItemLink
|
||||
| PropSidebarItemCategory;
|
||||
|
||||
export type PropVersionDoc = {
|
||||
id: string;
|
||||
title: string;
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
/**
|
||||
* 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} from 'react';
|
||||
import {ThemeClassNames, useSidebarBreadcrumbs} from '@docusaurus/theme-common';
|
||||
import styles from './styles.module.css';
|
||||
import clsx from 'clsx';
|
||||
import Link from '@docusaurus/Link';
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
// TODO move to design system folder
|
||||
function BreadcrumbsItemLink({
|
||||
children,
|
||||
href,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
href?: string;
|
||||
}): JSX.Element {
|
||||
const className = clsx('breadcrumbs__link', styles.breadcrumbsItemLink);
|
||||
return href ? (
|
||||
<Link className={className} href={href}>
|
||||
{children}
|
||||
</Link>
|
||||
) : (
|
||||
<span className={className}>{children}</span>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO move to design system folder
|
||||
function BreadcrumbsItem({
|
||||
children,
|
||||
active,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
active?: boolean;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<li
|
||||
className={clsx('breadcrumbs__item', {
|
||||
'breadcrumbs__item--active': active,
|
||||
})}>
|
||||
{children}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function HomeBreadcrumbItem() {
|
||||
const homeHref = useBaseUrl('/');
|
||||
return (
|
||||
<BreadcrumbsItem>
|
||||
<BreadcrumbsItemLink href={homeHref}>🏠</BreadcrumbsItemLink>
|
||||
</BreadcrumbsItem>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DocBreadcrumbs(): JSX.Element | null {
|
||||
const breadcrumbs = useSidebarBreadcrumbs();
|
||||
|
||||
if (!breadcrumbs) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={clsx(
|
||||
ThemeClassNames.docs.docBreadcrumbs,
|
||||
styles.breadcrumbsContainer,
|
||||
)}
|
||||
aria-label="breadcrumbs">
|
||||
<ul className="breadcrumbs">
|
||||
<HomeBreadcrumbItem />
|
||||
{breadcrumbs.map((item, idx) => (
|
||||
<BreadcrumbsItem key={idx} active={idx === breadcrumbs.length - 1}>
|
||||
<BreadcrumbsItemLink href={item.href}>
|
||||
{item.label}
|
||||
</BreadcrumbsItemLink>
|
||||
</BreadcrumbsItem>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.breadcrumbsContainer {
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.breadcrumbsItemLink {
|
||||
--ifm-breadcrumb-size-multiplier: 0.7 !important;
|
||||
margin-bottom: 0.4rem;
|
||||
background: var(--ifm-color-gray-100);
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .breadcrumbsItemLink {
|
||||
background-color: var(--ifm-color-gray-900);
|
||||
}
|
||||
|
||||
@media (min-width: 997px) {
|
||||
.breadcrumbsItemLink {
|
||||
--ifm-breadcrumb-size-multiplier: 0.8;
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ import DocPaginator from '@theme/DocPaginator';
|
|||
import Seo from '@theme/Seo';
|
||||
import DocVersionBanner from '@theme/DocVersionBanner';
|
||||
import DocVersionBadge from '@theme/DocVersionBadge';
|
||||
import DocBreadcrumbs from '@theme/DocBreadcrumbs';
|
||||
import Heading from '@theme/Heading';
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
|
@ -33,6 +34,7 @@ export default function DocCategoryGeneratedIndexPage({
|
|||
/>
|
||||
<div className={styles.generatedIndexPage}>
|
||||
<DocVersionBanner />
|
||||
<DocBreadcrumbs />
|
||||
<DocVersionBadge />
|
||||
<header>
|
||||
<Heading as="h1" className={styles.title}>
|
||||
|
|
|
@ -18,6 +18,7 @@ import TOCCollapsible from '@theme/TOCCollapsible';
|
|||
import Heading from '@theme/Heading';
|
||||
import styles from './styles.module.css';
|
||||
import {ThemeClassNames, useWindowSize} from '@docusaurus/theme-common';
|
||||
import DocBreadcrumbs from '@theme/DocBreadcrumbs';
|
||||
|
||||
export default function DocItem(props: Props): JSX.Element {
|
||||
const {content: DocContent} = props;
|
||||
|
@ -58,6 +59,7 @@ export default function DocItem(props: Props): JSX.Element {
|
|||
<DocVersionBanner />
|
||||
<div className={styles.docItemContainer}>
|
||||
<article>
|
||||
<DocBreadcrumbs />
|
||||
<DocVersionBadge />
|
||||
|
||||
{canRenderTOC && (
|
||||
|
|
|
@ -49,6 +49,7 @@ export {
|
|||
findFirstCategoryLink,
|
||||
useCurrentSidebarCategory,
|
||||
isActiveSidebarItem,
|
||||
useSidebarBreadcrumbs,
|
||||
} from './utils/docsUtils';
|
||||
|
||||
export {isSamePath} from './utils/pathUtils';
|
||||
|
|
|
@ -43,6 +43,7 @@ export const ThemeClassNames = {
|
|||
docs: {
|
||||
docVersionBanner: 'theme-doc-version-banner',
|
||||
docVersionBadge: 'theme-doc-version-badge',
|
||||
docBreadcrumbs: 'theme-doc-breadcrumbs',
|
||||
docMarkdown: 'theme-doc-markdown',
|
||||
docTocMobile: 'theme-doc-toc-mobile',
|
||||
docTocDesktop: 'theme-doc-toc-desktop',
|
||||
|
|
|
@ -16,11 +16,13 @@ import {
|
|||
useDocsSidebar,
|
||||
DocsSidebarProvider,
|
||||
findSidebarCategory,
|
||||
getBreadcrumbs,
|
||||
} from '../docsUtils';
|
||||
import type {
|
||||
PropSidebar,
|
||||
PropSidebarItem,
|
||||
PropSidebarItemCategory,
|
||||
PropSidebarItemLink,
|
||||
PropVersionMetadata,
|
||||
} from '@docusaurus/plugin-content-docs';
|
||||
|
||||
|
@ -39,6 +41,15 @@ function testCategory(
|
|||
};
|
||||
}
|
||||
|
||||
function testLink(data?: Partial<PropSidebarItemLink>): PropSidebarItemLink {
|
||||
return {
|
||||
type: 'link',
|
||||
href: '/testLinkHref',
|
||||
label: 'Link label',
|
||||
...data,
|
||||
};
|
||||
}
|
||||
|
||||
function testVersion(data?: Partial<PropVersionMetadata>): PropVersionMetadata {
|
||||
return {
|
||||
version: 'versionName',
|
||||
|
@ -330,4 +341,123 @@ describe('docsUtils', () => {
|
|||
expect(isActiveSidebarItem(item, '/sub-sub-link-path/')).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBreadcrumbs', () => {
|
||||
test('should return empty for empty sidebar', () => {
|
||||
expect(
|
||||
getBreadcrumbs({
|
||||
sidebar: [],
|
||||
pathname: '/doesNotExist',
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
test('should return empty for sidebar but unknown pathname', () => {
|
||||
const sidebar = [testCategory(), testLink()];
|
||||
expect(
|
||||
getBreadcrumbs({
|
||||
sidebar,
|
||||
pathname: '/doesNotExist',
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
test('should return first level category', () => {
|
||||
const pathname = '/somePathName';
|
||||
const sidebar = [testCategory({href: pathname}), testLink()];
|
||||
|
||||
expect(
|
||||
getBreadcrumbs({
|
||||
sidebar,
|
||||
pathname,
|
||||
}),
|
||||
).toEqual([sidebar[0]]);
|
||||
});
|
||||
|
||||
test('should return first level link', () => {
|
||||
const pathname = '/somePathName';
|
||||
const sidebar = [testCategory(), testLink({href: pathname})];
|
||||
|
||||
expect(
|
||||
getBreadcrumbs({
|
||||
sidebar,
|
||||
pathname,
|
||||
}),
|
||||
).toEqual([sidebar[1]]);
|
||||
});
|
||||
|
||||
test('should return nested category', () => {
|
||||
const pathname = '/somePathName';
|
||||
|
||||
const categoryLevel3 = testCategory({
|
||||
href: pathname,
|
||||
});
|
||||
|
||||
const categoryLevel2 = testCategory({
|
||||
items: [
|
||||
testCategory(),
|
||||
categoryLevel3,
|
||||
testLink({href: pathname}),
|
||||
testLink(),
|
||||
],
|
||||
});
|
||||
|
||||
const categoryLevel1 = testCategory({
|
||||
items: [testLink(), categoryLevel2],
|
||||
});
|
||||
|
||||
const sidebar = [
|
||||
testLink(),
|
||||
testCategory(),
|
||||
categoryLevel1,
|
||||
testLink(),
|
||||
testCategory(),
|
||||
];
|
||||
|
||||
expect(
|
||||
getBreadcrumbs({
|
||||
sidebar,
|
||||
pathname,
|
||||
}),
|
||||
).toEqual([categoryLevel1, categoryLevel2, categoryLevel3]);
|
||||
});
|
||||
});
|
||||
|
||||
test('should return nested link', () => {
|
||||
const pathname = '/somePathName';
|
||||
|
||||
const link = testLink({href: pathname});
|
||||
|
||||
const categoryLevel3 = testCategory({
|
||||
items: [testLink(), link, testLink()],
|
||||
});
|
||||
|
||||
const categoryLevel2 = testCategory({
|
||||
items: [
|
||||
testCategory(),
|
||||
categoryLevel3,
|
||||
testLink({href: pathname}),
|
||||
testLink(),
|
||||
],
|
||||
});
|
||||
|
||||
const categoryLevel1 = testCategory({
|
||||
items: [testLink(), categoryLevel2],
|
||||
});
|
||||
|
||||
const sidebar = [
|
||||
testLink(),
|
||||
testCategory(),
|
||||
categoryLevel1,
|
||||
testLink(),
|
||||
testCategory(),
|
||||
];
|
||||
|
||||
expect(
|
||||
getBreadcrumbs({
|
||||
sidebar,
|
||||
pathname,
|
||||
}),
|
||||
).toEqual([categoryLevel1, categoryLevel2, categoryLevel3, link]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,13 +6,17 @@
|
|||
*/
|
||||
|
||||
import React, {createContext, type ReactNode, useContext} from 'react';
|
||||
import {useAllDocsData} from '@docusaurus/plugin-content-docs/client';
|
||||
import {
|
||||
useActivePlugin,
|
||||
useAllDocsData,
|
||||
} from '@docusaurus/plugin-content-docs/client';
|
||||
import type {
|
||||
PropSidebar,
|
||||
PropSidebarItem,
|
||||
PropSidebarItemCategory,
|
||||
PropVersionDoc,
|
||||
PropVersionMetadata,
|
||||
PropSidebarBreadcrumbsItem,
|
||||
} from '@docusaurus/plugin-content-docs';
|
||||
import {isSamePath} from './pathUtils';
|
||||
import {useLocation} from '@docusaurus/router';
|
||||
|
@ -181,3 +185,46 @@ export function isActiveSidebarItem(
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getBreadcrumbs({
|
||||
sidebar,
|
||||
pathname,
|
||||
}: {
|
||||
sidebar: PropSidebar;
|
||||
pathname: string;
|
||||
}): PropSidebarBreadcrumbsItem[] {
|
||||
const breadcrumbs: PropSidebarBreadcrumbsItem[] = [];
|
||||
|
||||
function extract(items: PropSidebar) {
|
||||
for (const item of items) {
|
||||
if (
|
||||
item.type === 'category' &&
|
||||
(isSamePath(item.href, pathname) || extract(item.items))
|
||||
) {
|
||||
breadcrumbs.push(item);
|
||||
return true;
|
||||
} else if (item.type === 'link' && isSamePath(item.href, pathname)) {
|
||||
breadcrumbs.push(item);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
extract(sidebar);
|
||||
|
||||
return breadcrumbs.reverse();
|
||||
}
|
||||
|
||||
export function useSidebarBreadcrumbs(): PropSidebarBreadcrumbsItem[] | null {
|
||||
const sidebar = useDocsSidebar();
|
||||
const {pathname} = useLocation();
|
||||
const breadcrumbsOption = useActivePlugin()?.pluginData.breadcrumbs;
|
||||
|
||||
if (breadcrumbsOption === false || !sidebar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getBreadcrumbs({sidebar, pathname});
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ Accepted fields:
|
|||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `path` | `string` | `'docs'` | Path to data on filesystem relative to site dir. |
|
||||
| `breadcrumbs` | `boolean` | `true` | To enable or disable the breadcrumbs on docs pages. |
|
||||
| `editUrl` | <code>string \| EditUrlFunction</code> | `undefined` | Base URL to edit your site. The final URL is computed by `editUrl + relativeDocPath`. Using a function allows more nuanced control for each file. Omitting this variable entirely will disable edit links. |
|
||||
| `editLocalizedFiles` | `boolean` | `false` | The edit URL will target the localized file, instead of the original unlocalized file. Ignored when `editUrl` is a function. |
|
||||
| `editCurrentVersion` | `boolean` | `false` | The edit URL will always target the current version doc instead of older versions. Ignored when `editUrl` is a function. |
|
||||
|
@ -127,6 +128,7 @@ Most Docusaurus users configure this plugin through the preset options.
|
|||
|
||||
const config = {
|
||||
path: 'docs',
|
||||
breadcrumbs: true,
|
||||
// Simple use-case: string editUrl
|
||||
// editUrl: 'https://github.com/facebook/docusaurus/edit/main/website/',
|
||||
// Advanced use-case: functional editUrl
|
||||
|
|
|
@ -160,6 +160,28 @@ To pass in custom props to a swizzled sidebar item, add the optional `customProp
|
|||
};
|
||||
```
|
||||
|
||||
## Sidebar Breadcrumbs {#sidebar-breadcrumbs}
|
||||
|
||||
By default, breadcrumbs are rendered at the top, using the "sidebar path" of the current page.
|
||||
|
||||
This behavior can be disabled with plugin options:
|
||||
|
||||
```js title="docusaurus.config.js"
|
||||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
'@docusaurus/preset-classic',
|
||||
{
|
||||
docs: {
|
||||
// highlight-next-line
|
||||
breadcrumbs: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
## Complex sidebars example {#complex-sidebars-example}
|
||||
|
||||
A real-world example from the Docusaurus site:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue