mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-15 18:17:35 +02:00
feat(theme): ability to use <DocCardList> without items prop, on any doc page (#8008)
This commit is contained in:
parent
ff8ef774d6
commit
c811d6249e
9 changed files with 191 additions and 67 deletions
|
@ -28,6 +28,14 @@ export default function getSwizzleConfig(): SwizzleConfig {
|
|||
description:
|
||||
'The color mode toggle to switch between light and dark mode.',
|
||||
},
|
||||
DocCardList: {
|
||||
actions: {
|
||||
eject: 'safe',
|
||||
wrap: 'safe',
|
||||
},
|
||||
description:
|
||||
'The component responsible for rendering a list of sidebar items cards.\nNotable used on the category generated-index pages.',
|
||||
},
|
||||
DocSidebar: {
|
||||
actions: {
|
||||
eject: 'unsafe', // Too much technical code in sidebar, not very safe atm
|
||||
|
|
|
@ -336,7 +336,7 @@ declare module '@theme/DocCardList' {
|
|||
import type {PropSidebarItem} from '@docusaurus/plugin-content-docs';
|
||||
|
||||
export interface Props {
|
||||
readonly items: PropSidebarItem[];
|
||||
readonly items?: PropSidebarItem[];
|
||||
readonly className?: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,25 +7,27 @@
|
|||
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {findFirstCategoryLink} from '@docusaurus/theme-common/internal';
|
||||
import {
|
||||
useCurrentSidebarCategory,
|
||||
filterDocCardListItems,
|
||||
} from '@docusaurus/theme-common';
|
||||
import DocCard from '@theme/DocCard';
|
||||
import type {Props} from '@theme/DocCardList';
|
||||
import type {PropSidebarItem} from '@docusaurus/plugin-content-docs';
|
||||
|
||||
// Filter categories that don't have a link.
|
||||
function filterItems(items: PropSidebarItem[]): PropSidebarItem[] {
|
||||
return items.filter((item) => {
|
||||
if (item.type === 'category') {
|
||||
return !!findFirstCategoryLink(item);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
function DocCardListForCurrentSidebarCategory({className}: Props) {
|
||||
const category = useCurrentSidebarCategory();
|
||||
return <DocCardList items={category.items} className={className} />;
|
||||
}
|
||||
|
||||
export default function DocCardList({items, className}: Props): JSX.Element {
|
||||
export default function DocCardList(props: Props): JSX.Element {
|
||||
const {items, className} = props;
|
||||
if (!items) {
|
||||
return <DocCardListForCurrentSidebarCategory {...props} />;
|
||||
}
|
||||
const filteredItems = filterDocCardListItems(items);
|
||||
return (
|
||||
<section className={clsx('row', className)}>
|
||||
{filterItems(items).map((item, index) => (
|
||||
{filteredItems.map((item, index) => (
|
||||
<article key={index} className="col col--6 margin-bottom--lg">
|
||||
<DocCard item={item} />
|
||||
</article>
|
||||
|
|
|
@ -28,7 +28,10 @@ export {createStorageSlot, listStorageKeys} from './utils/storageUtils';
|
|||
|
||||
export {useContextualSearchFilters} from './utils/searchUtils';
|
||||
|
||||
export {useCurrentSidebarCategory} from './utils/docsUtils';
|
||||
export {
|
||||
useCurrentSidebarCategory,
|
||||
filterDocCardListItems,
|
||||
} from './utils/docsUtils';
|
||||
|
||||
export {usePluralForm} from './utils/usePluralForm';
|
||||
|
||||
|
|
|
@ -441,26 +441,87 @@ describe('useCurrentSidebarCategory', () => {
|
|||
</DocsSidebarProvider>
|
||||
),
|
||||
}).result.current;
|
||||
it('works', () => {
|
||||
const category: PropSidebarItemCategory = {
|
||||
type: 'category',
|
||||
label: 'Category',
|
||||
|
||||
it('works for sidebar category', () => {
|
||||
const category: PropSidebarItemCategory = testCategory({
|
||||
href: '/cat',
|
||||
collapsible: true,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{type: 'link', href: '/cat/foo', label: 'Foo'},
|
||||
{type: 'link', href: '/cat/bar', label: 'Bar'},
|
||||
{type: 'link', href: '/baz', label: 'Baz'},
|
||||
],
|
||||
};
|
||||
const mockUseCurrentSidebarCategory = createUseCurrentSidebarCategoryMock([
|
||||
{type: 'link', href: '/cat/fake', label: 'Fake'},
|
||||
});
|
||||
const sidebar: PropSidebar = [
|
||||
testLink(),
|
||||
testLink(),
|
||||
category,
|
||||
]);
|
||||
testCategory(),
|
||||
];
|
||||
|
||||
const mockUseCurrentSidebarCategory =
|
||||
createUseCurrentSidebarCategoryMock(sidebar);
|
||||
|
||||
expect(mockUseCurrentSidebarCategory('/cat')).toEqual(category);
|
||||
});
|
||||
|
||||
it('works for nested sidebar category', () => {
|
||||
const category2: PropSidebarItemCategory = testCategory({
|
||||
href: '/cat2',
|
||||
});
|
||||
const category1: PropSidebarItemCategory = testCategory({
|
||||
href: '/cat1',
|
||||
items: [testLink(), testLink(), category2, testCategory()],
|
||||
});
|
||||
const sidebar: PropSidebar = [
|
||||
testLink(),
|
||||
testLink(),
|
||||
category1,
|
||||
testCategory(),
|
||||
];
|
||||
|
||||
const mockUseCurrentSidebarCategory =
|
||||
createUseCurrentSidebarCategoryMock(sidebar);
|
||||
|
||||
expect(mockUseCurrentSidebarCategory('/cat2')).toEqual(category2);
|
||||
});
|
||||
|
||||
it('works for category link item', () => {
|
||||
const link = testLink({href: '/my/link/path'});
|
||||
const category: PropSidebarItemCategory = testCategory({
|
||||
href: '/cat1',
|
||||
items: [testLink(), testLink(), link, testCategory()],
|
||||
});
|
||||
const sidebar: PropSidebar = [
|
||||
testLink(),
|
||||
testLink(),
|
||||
category,
|
||||
testCategory(),
|
||||
];
|
||||
|
||||
const mockUseCurrentSidebarCategory =
|
||||
createUseCurrentSidebarCategoryMock(sidebar);
|
||||
|
||||
expect(mockUseCurrentSidebarCategory('/my/link/path')).toEqual(category);
|
||||
});
|
||||
|
||||
it('works for nested category link item', () => {
|
||||
const link = testLink({href: '/my/link/path'});
|
||||
const category2: PropSidebarItemCategory = testCategory({
|
||||
href: '/cat2',
|
||||
items: [testLink(), testLink(), link, testCategory()],
|
||||
});
|
||||
const category1: PropSidebarItemCategory = testCategory({
|
||||
href: '/cat1',
|
||||
items: [testLink(), testLink(), category2, testCategory()],
|
||||
});
|
||||
const sidebar: PropSidebar = [
|
||||
testLink(),
|
||||
testLink(),
|
||||
category1,
|
||||
testCategory(),
|
||||
];
|
||||
|
||||
const mockUseCurrentSidebarCategory =
|
||||
createUseCurrentSidebarCategoryMock(sidebar);
|
||||
|
||||
expect(mockUseCurrentSidebarCategory('/my/link/path')).toEqual(category2);
|
||||
});
|
||||
|
||||
it('throws for non-category index page', () => {
|
||||
const category: PropSidebarItemCategory = {
|
||||
type: 'category',
|
||||
|
|
|
@ -110,15 +110,18 @@ export function useCurrentSidebarCategory(): PropSidebarItemCategory {
|
|||
if (!sidebar) {
|
||||
throw new Error('Unexpected: cant find current sidebar in context');
|
||||
}
|
||||
const category = findSidebarCategory(sidebar.items, (item) =>
|
||||
isSamePath(item.href, pathname),
|
||||
);
|
||||
if (!category) {
|
||||
const categoryBreadcrumbs = getSidebarBreadcrumbs({
|
||||
sidebarItems: sidebar.items,
|
||||
pathname,
|
||||
onlyCategories: true,
|
||||
});
|
||||
const deepestCategory = categoryBreadcrumbs.slice(-1)[0];
|
||||
if (!deepestCategory) {
|
||||
throw new Error(
|
||||
`${pathname} is not associated with a category. useCurrentSidebarCategory() should only be used on category index pages.`,
|
||||
);
|
||||
}
|
||||
return category;
|
||||
return deepestCategory;
|
||||
}
|
||||
|
||||
const isActive = (testedPath: string | undefined, activePath: string) =>
|
||||
|
@ -149,6 +152,55 @@ export function isActiveSidebarItem(
|
|||
return false;
|
||||
}
|
||||
|
||||
function getSidebarBreadcrumbs(param: {
|
||||
sidebarItems: PropSidebar;
|
||||
pathname: string;
|
||||
onlyCategories: true;
|
||||
}): PropSidebarItemCategory[];
|
||||
|
||||
function getSidebarBreadcrumbs(param: {
|
||||
sidebarItems: PropSidebar;
|
||||
pathname: string;
|
||||
}): PropSidebarBreadcrumbsItem[];
|
||||
|
||||
/**
|
||||
* Get the sidebar the breadcrumbs for a given pathname
|
||||
* Ordered from top to bottom
|
||||
*/
|
||||
function getSidebarBreadcrumbs({
|
||||
sidebarItems,
|
||||
pathname,
|
||||
onlyCategories = false,
|
||||
}: {
|
||||
sidebarItems: PropSidebar;
|
||||
pathname: string;
|
||||
onlyCategories?: boolean;
|
||||
}): PropSidebarBreadcrumbsItem[] {
|
||||
const breadcrumbs: PropSidebarBreadcrumbsItem[] = [];
|
||||
|
||||
function extract(items: PropSidebarItem[]) {
|
||||
for (const item of items) {
|
||||
if (
|
||||
(item.type === 'category' &&
|
||||
(isSamePath(item.href, pathname) || extract(item.items))) ||
|
||||
(item.type === 'link' && isSamePath(item.href, pathname))
|
||||
) {
|
||||
const filtered = onlyCategories && item.type !== 'category';
|
||||
if (!filtered) {
|
||||
breadcrumbs.unshift(item);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
extract(sidebarItems);
|
||||
|
||||
return breadcrumbs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the breadcrumbs of the current doc page, based on its sidebar location.
|
||||
* Returns `null` if there's no sidebar or breadcrumbs are disabled.
|
||||
|
@ -157,31 +209,10 @@ export function useSidebarBreadcrumbs(): PropSidebarBreadcrumbsItem[] | null {
|
|||
const sidebar = useDocsSidebar();
|
||||
const {pathname} = useLocation();
|
||||
const breadcrumbsOption = useActivePlugin()?.pluginData.breadcrumbs;
|
||||
|
||||
if (breadcrumbsOption === false || !sidebar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const breadcrumbs: PropSidebarBreadcrumbsItem[] = [];
|
||||
|
||||
function extract(items: PropSidebar) {
|
||||
for (const item of items) {
|
||||
if (
|
||||
(item.type === 'category' &&
|
||||
(isSamePath(item.href, pathname) || extract(item.items))) ||
|
||||
(item.type === 'link' && isSamePath(item.href, pathname))
|
||||
) {
|
||||
breadcrumbs.push(item);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
extract(sidebar.items);
|
||||
|
||||
return breadcrumbs.reverse();
|
||||
return getSidebarBreadcrumbs({sidebarItems: sidebar.items, pathname});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -330,3 +361,18 @@ export function useDocRootMetadata({route}: DocRootProps): null | {
|
|||
sidebarItems,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter categories that don't have a link.
|
||||
* @param items
|
||||
*/
|
||||
export function filterDocCardListItems(
|
||||
items: PropSidebarItem[],
|
||||
): PropSidebarItem[] {
|
||||
return items.filter((item) => {
|
||||
if (item.type === 'category') {
|
||||
return !!findFirstCategoryLink(item);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -4,9 +4,8 @@ This section is not going to be very structured, but we will cover the following
|
|||
|
||||
```mdx-code-block
|
||||
import DocCardList from '@theme/DocCardList';
|
||||
import {useCurrentSidebarCategory} from '@docusaurus/theme-common';
|
||||
|
||||
<DocCardList items={useCurrentSidebarCategory().items}/>
|
||||
<DocCardList />
|
||||
```
|
||||
|
||||
We will assume that you have finished the guides, and know the basics like how to configure plugins, how to write React components, etc. These sections will have plugin authors and code contributors in mind, so we may occasionally refer to [plugin APIs](../api/plugin-methods/README.md) or other architecture details. Don't panic if you don't understand everything😉
|
||||
|
|
|
@ -35,9 +35,8 @@ This section serves as an overview of miscellaneous features of the doc sidebar.
|
|||
|
||||
```mdx-code-block
|
||||
import DocCardList from '@theme/DocCardList';
|
||||
import {useCurrentSidebarCategory} from '@docusaurus/theme-common';
|
||||
|
||||
<DocCardList items={useCurrentSidebarCategory().items}/>
|
||||
<DocCardList />
|
||||
```
|
||||
|
||||
## Default sidebar {#default-sidebar}
|
||||
|
|
|
@ -8,6 +8,7 @@ slug: /sidebar/items
|
|||
```mdx-code-block
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import BrowserWindow from '@site/src/components/BrowserWindow';
|
||||
```
|
||||
|
||||
We have introduced three types of item types in the example in the previous section: `doc`, `category`, and `link`, whose usages are fairly intuitive. We will formally introduce their APIs. There's also a fourth type: `autogenerated`, which we will explain in detail later.
|
||||
|
@ -291,18 +292,23 @@ See it in action on the [i18n introduction page](../../../i18n/i18n-introduction
|
|||
|
||||
#### Embedding generated index in doc page {#embedding-generated-index-in-doc-page}
|
||||
|
||||
You can embed the generated cards list in a normal doc page as well, as long as the doc is used as a category index page. To do so, you need to use the `DocCardList` component, paired with the `useCurrentSidebarCategory` hook.
|
||||
You can embed the generated cards list in a normal doc page as well with the `DocCardList` component. It will display all the sidebar items of the parent category of the current document.
|
||||
|
||||
```jsx title="a-category-index-page.md"
|
||||
```md title="docs/sidebar/index.md"
|
||||
import DocCardList from '@theme/DocCardList';
|
||||
import {useCurrentSidebarCategory} from '@docusaurus/theme-common';
|
||||
|
||||
In this section, we will introduce the following concepts:
|
||||
|
||||
<DocCardList items={useCurrentSidebarCategory().items}/>
|
||||
<DocCardList />
|
||||
```
|
||||
|
||||
See this in action on the [sidebar guides page](index.md).
|
||||
```mdx-code-block
|
||||
<BrowserWindow>
|
||||
|
||||
import DocCardList from '@theme/DocCardList';
|
||||
|
||||
<DocCardList />
|
||||
|
||||
</BrowserWindow>
|
||||
```
|
||||
|
||||
### Collapsible categories {#collapsible-categories}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue