feat(theme-classic, plugin-docs): sidebar item level-specific className + allow customization (#5642)

* Initial work

* Complete function

* Avoid duplication

* More dedupe

* Make everything constants

* Change casing & docs
This commit is contained in:
Joshua Chen 2021-10-07 22:59:02 +08:00 committed by GitHub
parent f6ec757aa0
commit eaacb0e98a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 79 additions and 11 deletions

View file

@ -29,6 +29,7 @@ const DocFrontMatterSchema = Joi.object<DocFrontMatter>({
slug: Joi.string(),
sidebar_label: Joi.string(),
sidebar_position: Joi.number(),
sidebar_class_name: Joi.string(),
tags: FrontMatterTagsSchema,
pagination_label: Joi.string(),
custom_edit_url: URISchema.allow('', null),

View file

@ -30,6 +30,7 @@ declare module '@docusaurus/plugin-content-docs-types' {
};
type PropsSidebarItemBase = {
className?: string;
customProps?: Record<string, unknown>;
};

View file

@ -47,6 +47,7 @@ Available document ids are:
type: 'link',
label: sidebarLabel || item.label || title,
href: permalink,
className: item.className,
customProps: item.customProps,
};
};

View file

@ -30,6 +30,7 @@ export type CategoryMetadatasFile = {
position?: number;
collapsed?: boolean;
collapsible?: boolean;
className?: string;
// TODO should we allow "items" here? how would this work? would an "autogenerated" type be allowed?
// This mkdocs plugin do something like that: https://github.com/lukasgeiter/mkdocs-awesome-pages-plugin/
@ -44,6 +45,7 @@ const CategoryMetadatasFileSchema = Joi.object<CategoryMetadatasFile>({
position: Joi.number(),
collapsed: Joi.boolean(),
collapsible: Joi.boolean(),
className: Joi.string(),
});
// TODO I now believe we should read all the category metadata files ahead of time: we may need this metadata to customize docs metadata
@ -177,6 +179,9 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
...(doc.frontMatter.sidebar_label && {
label: doc.frontMatter.sidebar_label,
}),
...(doc.frontMatter.sidebar_class_name && {
className: doc.frontMatter.sidebar_class_name,
}),
...(typeof doc.sidebarPosition !== 'undefined' && {
position: doc.sidebarPosition,
}),
@ -205,6 +210,7 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
const collapsible =
categoryMetadatas?.collapsible ?? options.sidebarCollapsible;
const collapsed = categoryMetadatas?.collapsed ?? options.sidebarCollapsed;
const className = categoryMetadatas?.className;
return {
type: 'category',
@ -213,6 +219,7 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
collapsed,
collapsible,
...(typeof position !== 'undefined' && {position}),
...(typeof className !== 'undefined' && {className}),
};
}
@ -311,5 +318,5 @@ function sortSidebarItems(
['asc'],
);
return sortedSidebarItems.map(({position: _removed, ...item}) => item);
return sortedSidebarItems.map(({position, ...item}) => item);
}

View file

@ -40,6 +40,7 @@ type SidebarItemCategoryJSON = SidebarItemBase & {
items: SidebarItemJSON[];
collapsed?: boolean;
collapsible?: boolean;
className?: string;
};
type SidebarItemAutogeneratedJSON = SidebarItemBase & {
@ -100,8 +101,7 @@ function assertItem<K extends string>(
keys: K[],
): asserts item is Record<K, unknown> {
const unknownKeys = Object.keys(item).filter(
// @ts-expect-error: key is always string
(key) => !keys.includes(key as string) && key !== 'type',
(key) => !keys.includes(key as K) && key !== 'type',
);
if (unknownKeys.length) {
@ -121,6 +121,7 @@ function assertIsCategory(
'label',
'collapsed',
'collapsible',
'className',
'customProps',
]);
if (typeof item.label !== 'string') {
@ -150,6 +151,14 @@ function assertIsCategory(
`Error loading ${JSON.stringify(item)}: "collapsible" must be a boolean.`,
);
}
if (
typeof item.className !== 'undefined' &&
typeof item.className !== 'string'
) {
throw new Error(
`Error loading ${JSON.stringify(item)}: "className" must be a string.`,
);
}
}
function assertIsAutogenerated(
@ -173,24 +182,33 @@ function assertIsAutogenerated(
function assertIsDoc(
item: Record<string, unknown>,
): asserts item is SidebarItemDoc {
assertItem(item, ['id', 'label', 'customProps']);
assertItem(item, ['id', 'label', 'className', 'customProps']);
if (typeof item.id !== 'string') {
throw new Error(
`Error loading ${JSON.stringify(item)}: "id" must be a string.`,
);
}
if (item.label && typeof item.label !== 'string') {
if (typeof item.label !== 'undefined' && typeof item.label !== 'string') {
throw new Error(
`Error loading ${JSON.stringify(item)}: "label" must be a string.`,
);
}
if (
typeof item.className !== 'undefined' &&
typeof item.className !== 'string'
) {
throw new Error(
`Error loading ${JSON.stringify(item)}: "className" must be a string.`,
);
}
}
function assertIsLink(
item: Record<string, unknown>,
): asserts item is SidebarItemLink {
assertItem(item, ['href', 'label', 'customProps']);
assertItem(item, ['href', 'label', 'className', 'customProps']);
if (typeof item.href !== 'string') {
throw new Error(
`Error loading ${JSON.stringify(item)}: "href" must be a string.`,
@ -201,6 +219,14 @@ function assertIsLink(
`Error loading ${JSON.stringify(item)}: "label" must be a string.`,
);
}
if (
typeof item.className !== 'undefined' &&
typeof item.className !== 'string'
) {
throw new Error(
`Error loading ${JSON.stringify(item)}: "className" must be a string.`,
);
}
}
/**

View file

@ -105,6 +105,7 @@ export type PluginOptions = MetadataOptions &
};
export type SidebarItemBase = {
className?: string;
customProps?: Record<string, unknown>;
};
@ -217,6 +218,7 @@ export type DocFrontMatter = {
slug?: string;
sidebar_label?: string;
sidebar_position?: number;
sidebar_class_name?: string;
pagination_label?: string;
custom_edit_url?: string | null;
parse_number_prefixes?: boolean;

View file

@ -80,7 +80,7 @@ function DocSidebarDesktop({path, sidebar, onCollapse, isHidden}: Props) {
!isAnnouncementBarClosed && showAnnouncementBar,
})}>
<ul className={clsx(ThemeClassNames.docs.docSidebarMenu, 'menu__list')}>
<DocSidebarItems items={sidebar} activePath={path} />
<DocSidebarItems items={sidebar} activePath={path} level={1} />
</ul>
</nav>
{hideableSidebar && <HideableSidebarButton onClick={onCollapse} />}
@ -99,6 +99,7 @@ const DocSidebarMobileSecondaryMenu: MobileSecondaryMenuComponent<Props> = ({
items={sidebar}
activePath={path}
onItemClick={() => toggleSidebar()}
level={1}
/>
</ul>
);

View file

@ -100,9 +100,10 @@ function DocSidebarItemCategory({
item,
onItemClick,
activePath,
level,
...props
}: Props & {item: PropSidebarItemCategory}) {
const {items, label, collapsible} = item;
const {items, label, collapsible, className} = item;
const isActive = isActiveSidebarItem(item, activePath);
@ -123,10 +124,12 @@ function DocSidebarItemCategory({
<li
className={clsx(
ThemeClassNames.docs.docSidebarItemCategory,
ThemeClassNames.docs.docSidebarItemCategoryLevel(level),
'menu__list-item',
{
'menu__list-item--collapsed': collapsed,
},
className,
)}>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a
@ -154,6 +157,7 @@ function DocSidebarItemCategory({
tabIndex={collapsed ? -1 : 0}
onItemClick={onItemClick}
activePath={activePath}
level={level + 1}
/>
</Collapsible>
</li>
@ -164,15 +168,18 @@ function DocSidebarItemLink({
item,
onItemClick,
activePath,
level,
...props
}: Props & {item: PropSidebarItemLink}) {
const {href, label} = item;
const {href, label, className} = item;
const isActive = isActiveSidebarItem(item, activePath);
return (
<li
className={clsx(
ThemeClassNames.docs.docSidebarItemLink,
ThemeClassNames.docs.docSidebarItemLinkLevel(level),
'menu__list-item',
className,
)}
key={label}>
<Link

View file

@ -140,6 +140,7 @@ declare module '@theme/DocSidebarItem' {
type DocSidebarPropsBase = {
readonly activePath: string;
readonly onItemClick?: () => void;
readonly level: number;
readonly tabIndex?: number;
};

View file

@ -50,9 +50,13 @@ export const ThemeClassNames = {
docSidebarMenu: 'theme-doc-sidebar-menu',
docSidebarItemCategory: 'theme-doc-sidebar-item-category',
docSidebarItemLink: 'theme-doc-sidebar-item-link',
docSidebarItemCategoryLevel: (level: number) =>
`theme-doc-sidebar-item-category-level-${level}` as const,
docSidebarItemLinkLevel: (level: number) =>
`theme-doc-sidebar-item-link-level-${level}` as const,
// TODO add other stable classNames here
},
blog: {
// TODO add other stable classNames here
},
};
} as const;

View file

@ -10,6 +10,7 @@ module.exports = {
{
type: 'doc',
id: 'index',
className: 'red',
label: 'Index',
},
{
@ -23,9 +24,16 @@ module.exports = {
label: 'Huge sidebar category',
items: generateHugeSidebarItems(4),
},
{
type: 'link',
label: 'External link',
href: 'https://github.com/facebook/docusaurus',
className: 'red',
},
{
type: 'category',
label: 'TOC tests',
className: 'red',
items: [
{
type: 'autogenerated',

View file

@ -247,6 +247,7 @@ Accepted fields:
| `pagination_label` | `string` | `sidebar_label` or `title` | The text used in the document next/previous buttons for this document. |
| `sidebar_label` | `string` | `title` | The text shown in the document sidebar for this document. |
| `sidebar_position` | `number` | Default ordering | Controls the position of a doc inside the generated sidebar slice when using `autogenerated` sidebar items. See also [Autogenerated sidebar metadatas](/docs/sidebar#autogenerated-sidebar-metadatas). |
| `sidebar_class_name` | `string` | `undefined` | Gives the corresponding sidebar label a special class name when using autogenerated sidebars. |
| `hide_title` | `boolean` | `false` | Whether to hide the title at the top of the doc. It only hides a title declared through the frontmatter, and have no effect on a Markdown title at the top of your document. |
| `hide_table_of_contents` | `boolean` | `false` | Whether to hide the table of contents to the right. |
| `toc_min_heading_level` | `number` | `2` | The minimum heading level shown in the table of contents. Must be between 2 and 6 and lower or equal to the max value. |

View file

@ -173,6 +173,7 @@ type SidebarItemDoc =
type: 'doc';
id: string;
label: string; // Sidebar label text
className?: string; // Class name for sidebar label
}
// Shorthand syntax
@ -244,6 +245,7 @@ type SidebarItemLink = {
type: 'link';
label: string;
href: string;
className?: string;
};
```
@ -282,6 +284,7 @@ type SidebarItemCategory = {
type: 'category';
label: string; // Sidebar label text.
items: SidebarItem[]; // Array of sidebar items.
className?: string;
// Category options:
collapsible: boolean; // Set the category to be collapsible
@ -519,7 +522,8 @@ This is the easy tutorial!
```json title="docs/tutorials/_category_.json"
{
"label": "Tutorial",
"position": 3
"position": 3,
"className": "red"
}
```

View file

@ -146,3 +146,7 @@ div[class^='announcementBar_'] {
);
font-weight: bold;
}
.red > a {
color: red;
}