From a27335be4888d2c2de55459cee5c91146daa5e7a Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Thu, 25 Aug 2022 17:47:20 +0200 Subject: [PATCH 1/3] simplify usage of DocCardList --- .../src/theme-classic.d.ts | 2 +- .../src/theme/DocCardList/index.tsx | 12 ++- .../src/utils/__tests__/docsUtils.test.tsx | 91 ++++++++++++++++--- .../src/utils/docsUtils.tsx | 71 +++++++++++---- website/docs/advanced/index.md | 3 +- website/docs/guides/docs/sidebar/index.md | 3 +- website/docs/guides/docs/sidebar/items.md | 18 +++- 7 files changed, 154 insertions(+), 46 deletions(-) diff --git a/packages/docusaurus-theme-classic/src/theme-classic.d.ts b/packages/docusaurus-theme-classic/src/theme-classic.d.ts index 8cf475b8400c..86a25cb4e811 100644 --- a/packages/docusaurus-theme-classic/src/theme-classic.d.ts +++ b/packages/docusaurus-theme-classic/src/theme-classic.d.ts @@ -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; } diff --git a/packages/docusaurus-theme-classic/src/theme/DocCardList/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocCardList/index.tsx index c4ace5f5ee6c..31085c2e7724 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocCardList/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocCardList/index.tsx @@ -8,6 +8,7 @@ import React from 'react'; import clsx from 'clsx'; import {findFirstCategoryLink} from '@docusaurus/theme-common/internal'; +import {useCurrentSidebarCategory} from '@docusaurus/theme-common'; import DocCard from '@theme/DocCard'; import type {Props} from '@theme/DocCardList'; import type {PropSidebarItem} from '@docusaurus/plugin-content-docs'; @@ -22,7 +23,16 @@ function filterItems(items: PropSidebarItem[]): PropSidebarItem[] { }); } -export default function DocCardList({items, className}: Props): JSX.Element { +function DocCardListForCurrentSidebarCategory({className}: Props) { + const category = useCurrentSidebarCategory(); + return ; +} + +export default function DocCardList(props: Props): JSX.Element { + const {items, className} = props; + if (!items) { + return ; + } return (
{filterItems(items).map((item, index) => ( diff --git a/packages/docusaurus-theme-common/src/utils/__tests__/docsUtils.test.tsx b/packages/docusaurus-theme-common/src/utils/__tests__/docsUtils.test.tsx index 8f4c48011299..45724b2b2ec5 100644 --- a/packages/docusaurus-theme-common/src/utils/__tests__/docsUtils.test.tsx +++ b/packages/docusaurus-theme-common/src/utils/__tests__/docsUtils.test.tsx @@ -441,26 +441,87 @@ describe('useCurrentSidebarCategory', () => { ), }).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', diff --git a/packages/docusaurus-theme-common/src/utils/docsUtils.tsx b/packages/docusaurus-theme-common/src/utils/docsUtils.tsx index d480172b87c8..a458337aba14 100644 --- a/packages/docusaurus-theme-common/src/utils/docsUtils.tsx +++ b/packages/docusaurus-theme-common/src/utils/docsUtils.tsx @@ -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,29 +152,43 @@ export function isActiveSidebarItem( return false; } -/** - * Gets the breadcrumbs of the current doc page, based on its sidebar location. - * Returns `null` if there's no sidebar or breadcrumbs are disabled. - */ -export function useSidebarBreadcrumbs(): PropSidebarBreadcrumbsItem[] | null { - const sidebar = useDocsSidebar(); - const {pathname} = useLocation(); - const breadcrumbsOption = useActivePlugin()?.pluginData.breadcrumbs; +function getSidebarBreadcrumbs(param: { + sidebarItems: PropSidebar; + pathname: string; + onlyCategories: true; +}): PropSidebarItemCategory[]; - if (breadcrumbsOption === false || !sidebar) { - return null; - } +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: PropSidebar) { + 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)) ) { - breadcrumbs.push(item); + const filtered = onlyCategories && item.type !== 'category'; + if (!filtered) { + breadcrumbs.unshift(item); + } return true; } } @@ -179,9 +196,23 @@ export function useSidebarBreadcrumbs(): PropSidebarBreadcrumbsItem[] | null { return false; } - extract(sidebar.items); + extract(sidebarItems); - return breadcrumbs.reverse(); + 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. + */ +export function useSidebarBreadcrumbs(): PropSidebarBreadcrumbsItem[] | null { + const sidebar = useDocsSidebar(); + const {pathname} = useLocation(); + const breadcrumbsOption = useActivePlugin()?.pluginData.breadcrumbs; + if (breadcrumbsOption === false || !sidebar) { + return null; + } + return getSidebarBreadcrumbs({sidebarItems: sidebar.items, pathname}); } /** diff --git a/website/docs/advanced/index.md b/website/docs/advanced/index.md index afe7f1a57050..47fc3f8bc902 100644 --- a/website/docs/advanced/index.md +++ b/website/docs/advanced/index.md @@ -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'; - + ``` 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😉 diff --git a/website/docs/guides/docs/sidebar/index.md b/website/docs/guides/docs/sidebar/index.md index 06eb5a91b3b9..8e8817b54375 100644 --- a/website/docs/guides/docs/sidebar/index.md +++ b/website/docs/guides/docs/sidebar/index.md @@ -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'; - + ``` ## Default sidebar {#default-sidebar} diff --git a/website/docs/guides/docs/sidebar/items.md b/website/docs/guides/docs/sidebar/items.md index fa7af72ca9be..60c2a752eda3 100644 --- a/website/docs/guides/docs/sidebar/items.md +++ b/website/docs/guides/docs/sidebar/items.md @@ -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,15 +292,22 @@ 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: + +``` + +```mdx-code-block + + +import DocCardList from '@theme/DocCardList'; + + - + ``` See this in action on the [sidebar guides page](index.md). From 4044d000c04a30efc4859358cc10071b76e028f7 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Thu, 25 Aug 2022 17:51:29 +0200 Subject: [PATCH 2/3] remove useless doc --- website/docs/guides/docs/sidebar/items.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/website/docs/guides/docs/sidebar/items.md b/website/docs/guides/docs/sidebar/items.md index 60c2a752eda3..1e5d8ffdf9ab 100644 --- a/website/docs/guides/docs/sidebar/items.md +++ b/website/docs/guides/docs/sidebar/items.md @@ -310,8 +310,6 @@ import DocCardList from '@theme/DocCardList'; ``` -See this in action on the [sidebar guides page](index.md). - ### Collapsible categories {#collapsible-categories} We support the option to expand/collapse categories. Categories are collapsible by default, but you can disable collapsing with `collapsible: false`. From 7aed44ef8b8ebf22bbd198e5f819163198c8230c Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Thu, 25 Aug 2022 18:08:54 +0200 Subject: [PATCH 3/3] mark DocCardList as safe to swizzle --- .../src/getSwizzleConfig.ts | 8 ++++++++ .../src/theme/DocCardList/index.tsx | 20 ++++++------------- packages/docusaurus-theme-common/src/index.ts | 5 ++++- .../src/utils/docsUtils.tsx | 15 ++++++++++++++ 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts b/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts index 72356f78e7c7..29f89dbb4903 100644 --- a/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts +++ b/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts @@ -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 diff --git a/packages/docusaurus-theme-classic/src/theme/DocCardList/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocCardList/index.tsx index 31085c2e7724..cde4b22a74da 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocCardList/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocCardList/index.tsx @@ -7,21 +7,12 @@ import React from 'react'; import clsx from 'clsx'; -import {findFirstCategoryLink} from '@docusaurus/theme-common/internal'; -import {useCurrentSidebarCategory} from '@docusaurus/theme-common'; +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(); @@ -33,9 +24,10 @@ export default function DocCardList(props: Props): JSX.Element { if (!items) { return ; } + const filteredItems = filterDocCardListItems(items); return (
- {filterItems(items).map((item, index) => ( + {filteredItems.map((item, index) => (
diff --git a/packages/docusaurus-theme-common/src/index.ts b/packages/docusaurus-theme-common/src/index.ts index 1b450b8182b3..7f7427c41409 100644 --- a/packages/docusaurus-theme-common/src/index.ts +++ b/packages/docusaurus-theme-common/src/index.ts @@ -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'; diff --git a/packages/docusaurus-theme-common/src/utils/docsUtils.tsx b/packages/docusaurus-theme-common/src/utils/docsUtils.tsx index a458337aba14..3c476d16231d 100644 --- a/packages/docusaurus-theme-common/src/utils/docsUtils.tsx +++ b/packages/docusaurus-theme-common/src/utils/docsUtils.tsx @@ -361,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; + }); +}