diff --git a/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts b/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts index 72356f78e7..29f89dbb49 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-classic.d.ts b/packages/docusaurus-theme-classic/src/theme-classic.d.ts index 8cf475b840..86a25cb4e8 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 c4ace5f5ee..cde4b22a74 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocCardList/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocCardList/index.tsx @@ -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 ; } -export default function DocCardList({items, className}: Props): JSX.Element { +export default function DocCardList(props: Props): JSX.Element { + const {items, className} = props; + 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 1b450b8182..7f7427c414 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/__tests__/docsUtils.test.tsx b/packages/docusaurus-theme-common/src/utils/__tests__/docsUtils.test.tsx index 8f4c480112..45724b2b2e 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 d480172b87..3c476d1623 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,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; + }); +} diff --git a/website/docs/advanced/index.md b/website/docs/advanced/index.md index afe7f1a570..47fc3f8bc9 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 06eb5a91b3..8e8817b543 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 fa7af72ca9..1e5d8ffdf9 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,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: - - + ``` -See this in action on the [sidebar guides page](index.md). +```mdx-code-block + + +import DocCardList from '@theme/DocCardList'; + + + + +``` ### Collapsible categories {#collapsible-categories}