Skip to content

Commit

Permalink
feat(theme): ability to use <DocCardList> without items prop, on any …
Browse files Browse the repository at this point in the history
…doc page (#8008)
  • Loading branch information
slorber committed Sep 1, 2022
1 parent 27e3b98 commit 6b65355
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 60 deletions.
8 changes: 8 additions & 0 deletions packages/docusaurus-theme-classic/src/getSwizzleConfig.ts
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/docusaurus-theme-classic/src/theme-classic.d.ts
Expand Up @@ -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;
}

Expand Down
26 changes: 14 additions & 12 deletions packages/docusaurus-theme-classic/src/theme/DocCardList/index.tsx
Expand Up @@ -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>
Expand Down
5 changes: 4 additions & 1 deletion packages/docusaurus-theme-common/src/index.ts
Expand Up @@ -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';

Expand Down
Expand Up @@ -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',
Expand Down
86 changes: 66 additions & 20 deletions packages/docusaurus-theme-common/src/utils/docsUtils.tsx
Expand Up @@ -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) =>
Expand Down Expand Up @@ -149,39 +152,67 @@ 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;
}
}

return false;
}

extract(sidebar.items);
extract(sidebarItems);

return breadcrumbs;
}

return breadcrumbs.reverse();
/**
* 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});
}

/**
Expand Down Expand Up @@ -332,3 +363,18 @@ export function useDocRouteMetadata({
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;
});
}
3 changes: 1 addition & 2 deletions website/docs/advanced/index.md
Expand Up @@ -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😉
3 changes: 1 addition & 2 deletions website/docs/guides/docs/sidebar/index.md
Expand Up @@ -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}
Expand Down
20 changes: 13 additions & 7 deletions website/docs/guides/docs/sidebar/items.md
Expand Up @@ -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.
Expand Down Expand Up @@ -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}

Expand Down

0 comments on commit 6b65355

Please sign in to comment.