mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-10 15:47:23 +02:00
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:
parent
f6ec757aa0
commit
eaacb0e98a
14 changed files with 79 additions and 11 deletions
|
@ -29,6 +29,7 @@ const DocFrontMatterSchema = Joi.object<DocFrontMatter>({
|
||||||
slug: Joi.string(),
|
slug: Joi.string(),
|
||||||
sidebar_label: Joi.string(),
|
sidebar_label: Joi.string(),
|
||||||
sidebar_position: Joi.number(),
|
sidebar_position: Joi.number(),
|
||||||
|
sidebar_class_name: Joi.string(),
|
||||||
tags: FrontMatterTagsSchema,
|
tags: FrontMatterTagsSchema,
|
||||||
pagination_label: Joi.string(),
|
pagination_label: Joi.string(),
|
||||||
custom_edit_url: URISchema.allow('', null),
|
custom_edit_url: URISchema.allow('', null),
|
||||||
|
|
|
@ -30,6 +30,7 @@ declare module '@docusaurus/plugin-content-docs-types' {
|
||||||
};
|
};
|
||||||
|
|
||||||
type PropsSidebarItemBase = {
|
type PropsSidebarItemBase = {
|
||||||
|
className?: string;
|
||||||
customProps?: Record<string, unknown>;
|
customProps?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,7 @@ Available document ids are:
|
||||||
type: 'link',
|
type: 'link',
|
||||||
label: sidebarLabel || item.label || title,
|
label: sidebarLabel || item.label || title,
|
||||||
href: permalink,
|
href: permalink,
|
||||||
|
className: item.className,
|
||||||
customProps: item.customProps,
|
customProps: item.customProps,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -30,6 +30,7 @@ export type CategoryMetadatasFile = {
|
||||||
position?: number;
|
position?: number;
|
||||||
collapsed?: boolean;
|
collapsed?: boolean;
|
||||||
collapsible?: boolean;
|
collapsible?: boolean;
|
||||||
|
className?: string;
|
||||||
|
|
||||||
// TODO should we allow "items" here? how would this work? would an "autogenerated" type be allowed?
|
// 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/
|
// 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(),
|
position: Joi.number(),
|
||||||
collapsed: Joi.boolean(),
|
collapsed: Joi.boolean(),
|
||||||
collapsible: 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
|
// 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 && {
|
...(doc.frontMatter.sidebar_label && {
|
||||||
label: doc.frontMatter.sidebar_label,
|
label: doc.frontMatter.sidebar_label,
|
||||||
}),
|
}),
|
||||||
|
...(doc.frontMatter.sidebar_class_name && {
|
||||||
|
className: doc.frontMatter.sidebar_class_name,
|
||||||
|
}),
|
||||||
...(typeof doc.sidebarPosition !== 'undefined' && {
|
...(typeof doc.sidebarPosition !== 'undefined' && {
|
||||||
position: doc.sidebarPosition,
|
position: doc.sidebarPosition,
|
||||||
}),
|
}),
|
||||||
|
@ -205,6 +210,7 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
||||||
const collapsible =
|
const collapsible =
|
||||||
categoryMetadatas?.collapsible ?? options.sidebarCollapsible;
|
categoryMetadatas?.collapsible ?? options.sidebarCollapsible;
|
||||||
const collapsed = categoryMetadatas?.collapsed ?? options.sidebarCollapsed;
|
const collapsed = categoryMetadatas?.collapsed ?? options.sidebarCollapsed;
|
||||||
|
const className = categoryMetadatas?.className;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
|
@ -213,6 +219,7 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
||||||
collapsed,
|
collapsed,
|
||||||
collapsible,
|
collapsible,
|
||||||
...(typeof position !== 'undefined' && {position}),
|
...(typeof position !== 'undefined' && {position}),
|
||||||
|
...(typeof className !== 'undefined' && {className}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -311,5 +318,5 @@ function sortSidebarItems(
|
||||||
['asc'],
|
['asc'],
|
||||||
);
|
);
|
||||||
|
|
||||||
return sortedSidebarItems.map(({position: _removed, ...item}) => item);
|
return sortedSidebarItems.map(({position, ...item}) => item);
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ type SidebarItemCategoryJSON = SidebarItemBase & {
|
||||||
items: SidebarItemJSON[];
|
items: SidebarItemJSON[];
|
||||||
collapsed?: boolean;
|
collapsed?: boolean;
|
||||||
collapsible?: boolean;
|
collapsible?: boolean;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SidebarItemAutogeneratedJSON = SidebarItemBase & {
|
type SidebarItemAutogeneratedJSON = SidebarItemBase & {
|
||||||
|
@ -100,8 +101,7 @@ function assertItem<K extends string>(
|
||||||
keys: K[],
|
keys: K[],
|
||||||
): asserts item is Record<K, unknown> {
|
): asserts item is Record<K, unknown> {
|
||||||
const unknownKeys = Object.keys(item).filter(
|
const unknownKeys = Object.keys(item).filter(
|
||||||
// @ts-expect-error: key is always string
|
(key) => !keys.includes(key as K) && key !== 'type',
|
||||||
(key) => !keys.includes(key as string) && key !== 'type',
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (unknownKeys.length) {
|
if (unknownKeys.length) {
|
||||||
|
@ -121,6 +121,7 @@ function assertIsCategory(
|
||||||
'label',
|
'label',
|
||||||
'collapsed',
|
'collapsed',
|
||||||
'collapsible',
|
'collapsible',
|
||||||
|
'className',
|
||||||
'customProps',
|
'customProps',
|
||||||
]);
|
]);
|
||||||
if (typeof item.label !== 'string') {
|
if (typeof item.label !== 'string') {
|
||||||
|
@ -150,6 +151,14 @@ function assertIsCategory(
|
||||||
`Error loading ${JSON.stringify(item)}: "collapsible" must be a boolean.`,
|
`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(
|
function assertIsAutogenerated(
|
||||||
|
@ -173,24 +182,33 @@ function assertIsAutogenerated(
|
||||||
function assertIsDoc(
|
function assertIsDoc(
|
||||||
item: Record<string, unknown>,
|
item: Record<string, unknown>,
|
||||||
): asserts item is SidebarItemDoc {
|
): asserts item is SidebarItemDoc {
|
||||||
assertItem(item, ['id', 'label', 'customProps']);
|
assertItem(item, ['id', 'label', 'className', 'customProps']);
|
||||||
if (typeof item.id !== 'string') {
|
if (typeof item.id !== 'string') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Error loading ${JSON.stringify(item)}: "id" must be a string.`,
|
`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(
|
throw new Error(
|
||||||
`Error loading ${JSON.stringify(item)}: "label" must be a string.`,
|
`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(
|
function assertIsLink(
|
||||||
item: Record<string, unknown>,
|
item: Record<string, unknown>,
|
||||||
): asserts item is SidebarItemLink {
|
): asserts item is SidebarItemLink {
|
||||||
assertItem(item, ['href', 'label', 'customProps']);
|
assertItem(item, ['href', 'label', 'className', 'customProps']);
|
||||||
if (typeof item.href !== 'string') {
|
if (typeof item.href !== 'string') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Error loading ${JSON.stringify(item)}: "href" must be a string.`,
|
`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.`,
|
`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.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -105,6 +105,7 @@ export type PluginOptions = MetadataOptions &
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SidebarItemBase = {
|
export type SidebarItemBase = {
|
||||||
|
className?: string;
|
||||||
customProps?: Record<string, unknown>;
|
customProps?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -217,6 +218,7 @@ export type DocFrontMatter = {
|
||||||
slug?: string;
|
slug?: string;
|
||||||
sidebar_label?: string;
|
sidebar_label?: string;
|
||||||
sidebar_position?: number;
|
sidebar_position?: number;
|
||||||
|
sidebar_class_name?: string;
|
||||||
pagination_label?: string;
|
pagination_label?: string;
|
||||||
custom_edit_url?: string | null;
|
custom_edit_url?: string | null;
|
||||||
parse_number_prefixes?: boolean;
|
parse_number_prefixes?: boolean;
|
||||||
|
|
|
@ -80,7 +80,7 @@ function DocSidebarDesktop({path, sidebar, onCollapse, isHidden}: Props) {
|
||||||
!isAnnouncementBarClosed && showAnnouncementBar,
|
!isAnnouncementBarClosed && showAnnouncementBar,
|
||||||
})}>
|
})}>
|
||||||
<ul className={clsx(ThemeClassNames.docs.docSidebarMenu, 'menu__list')}>
|
<ul className={clsx(ThemeClassNames.docs.docSidebarMenu, 'menu__list')}>
|
||||||
<DocSidebarItems items={sidebar} activePath={path} />
|
<DocSidebarItems items={sidebar} activePath={path} level={1} />
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
{hideableSidebar && <HideableSidebarButton onClick={onCollapse} />}
|
{hideableSidebar && <HideableSidebarButton onClick={onCollapse} />}
|
||||||
|
@ -99,6 +99,7 @@ const DocSidebarMobileSecondaryMenu: MobileSecondaryMenuComponent<Props> = ({
|
||||||
items={sidebar}
|
items={sidebar}
|
||||||
activePath={path}
|
activePath={path}
|
||||||
onItemClick={() => toggleSidebar()}
|
onItemClick={() => toggleSidebar()}
|
||||||
|
level={1}
|
||||||
/>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
|
|
|
@ -100,9 +100,10 @@ function DocSidebarItemCategory({
|
||||||
item,
|
item,
|
||||||
onItemClick,
|
onItemClick,
|
||||||
activePath,
|
activePath,
|
||||||
|
level,
|
||||||
...props
|
...props
|
||||||
}: Props & {item: PropSidebarItemCategory}) {
|
}: Props & {item: PropSidebarItemCategory}) {
|
||||||
const {items, label, collapsible} = item;
|
const {items, label, collapsible, className} = item;
|
||||||
|
|
||||||
const isActive = isActiveSidebarItem(item, activePath);
|
const isActive = isActiveSidebarItem(item, activePath);
|
||||||
|
|
||||||
|
@ -123,10 +124,12 @@ function DocSidebarItemCategory({
|
||||||
<li
|
<li
|
||||||
className={clsx(
|
className={clsx(
|
||||||
ThemeClassNames.docs.docSidebarItemCategory,
|
ThemeClassNames.docs.docSidebarItemCategory,
|
||||||
|
ThemeClassNames.docs.docSidebarItemCategoryLevel(level),
|
||||||
'menu__list-item',
|
'menu__list-item',
|
||||||
{
|
{
|
||||||
'menu__list-item--collapsed': collapsed,
|
'menu__list-item--collapsed': collapsed,
|
||||||
},
|
},
|
||||||
|
className,
|
||||||
)}>
|
)}>
|
||||||
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||||
<a
|
<a
|
||||||
|
@ -154,6 +157,7 @@ function DocSidebarItemCategory({
|
||||||
tabIndex={collapsed ? -1 : 0}
|
tabIndex={collapsed ? -1 : 0}
|
||||||
onItemClick={onItemClick}
|
onItemClick={onItemClick}
|
||||||
activePath={activePath}
|
activePath={activePath}
|
||||||
|
level={level + 1}
|
||||||
/>
|
/>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</li>
|
</li>
|
||||||
|
@ -164,15 +168,18 @@ function DocSidebarItemLink({
|
||||||
item,
|
item,
|
||||||
onItemClick,
|
onItemClick,
|
||||||
activePath,
|
activePath,
|
||||||
|
level,
|
||||||
...props
|
...props
|
||||||
}: Props & {item: PropSidebarItemLink}) {
|
}: Props & {item: PropSidebarItemLink}) {
|
||||||
const {href, label} = item;
|
const {href, label, className} = item;
|
||||||
const isActive = isActiveSidebarItem(item, activePath);
|
const isActive = isActiveSidebarItem(item, activePath);
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
className={clsx(
|
className={clsx(
|
||||||
ThemeClassNames.docs.docSidebarItemLink,
|
ThemeClassNames.docs.docSidebarItemLink,
|
||||||
|
ThemeClassNames.docs.docSidebarItemLinkLevel(level),
|
||||||
'menu__list-item',
|
'menu__list-item',
|
||||||
|
className,
|
||||||
)}
|
)}
|
||||||
key={label}>
|
key={label}>
|
||||||
<Link
|
<Link
|
||||||
|
|
|
@ -140,6 +140,7 @@ declare module '@theme/DocSidebarItem' {
|
||||||
type DocSidebarPropsBase = {
|
type DocSidebarPropsBase = {
|
||||||
readonly activePath: string;
|
readonly activePath: string;
|
||||||
readonly onItemClick?: () => void;
|
readonly onItemClick?: () => void;
|
||||||
|
readonly level: number;
|
||||||
readonly tabIndex?: number;
|
readonly tabIndex?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -50,9 +50,13 @@ export const ThemeClassNames = {
|
||||||
docSidebarMenu: 'theme-doc-sidebar-menu',
|
docSidebarMenu: 'theme-doc-sidebar-menu',
|
||||||
docSidebarItemCategory: 'theme-doc-sidebar-item-category',
|
docSidebarItemCategory: 'theme-doc-sidebar-item-category',
|
||||||
docSidebarItemLink: 'theme-doc-sidebar-item-link',
|
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
|
// TODO add other stable classNames here
|
||||||
},
|
},
|
||||||
blog: {
|
blog: {
|
||||||
// TODO add other stable classNames here
|
// TODO add other stable classNames here
|
||||||
},
|
},
|
||||||
};
|
} as const;
|
||||||
|
|
|
@ -10,6 +10,7 @@ module.exports = {
|
||||||
{
|
{
|
||||||
type: 'doc',
|
type: 'doc',
|
||||||
id: 'index',
|
id: 'index',
|
||||||
|
className: 'red',
|
||||||
label: 'Index',
|
label: 'Index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -23,9 +24,16 @@ module.exports = {
|
||||||
label: 'Huge sidebar category',
|
label: 'Huge sidebar category',
|
||||||
items: generateHugeSidebarItems(4),
|
items: generateHugeSidebarItems(4),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
label: 'External link',
|
||||||
|
href: 'https://github.com/facebook/docusaurus',
|
||||||
|
className: 'red',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: 'category',
|
||||||
label: 'TOC tests',
|
label: 'TOC tests',
|
||||||
|
className: 'red',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
type: 'autogenerated',
|
type: 'autogenerated',
|
||||||
|
|
|
@ -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. |
|
| `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_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_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_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. |
|
| `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. |
|
| `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. |
|
||||||
|
|
|
@ -173,6 +173,7 @@ type SidebarItemDoc =
|
||||||
type: 'doc';
|
type: 'doc';
|
||||||
id: string;
|
id: string;
|
||||||
label: string; // Sidebar label text
|
label: string; // Sidebar label text
|
||||||
|
className?: string; // Class name for sidebar label
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shorthand syntax
|
// Shorthand syntax
|
||||||
|
@ -244,6 +245,7 @@ type SidebarItemLink = {
|
||||||
type: 'link';
|
type: 'link';
|
||||||
label: string;
|
label: string;
|
||||||
href: string;
|
href: string;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -282,6 +284,7 @@ type SidebarItemCategory = {
|
||||||
type: 'category';
|
type: 'category';
|
||||||
label: string; // Sidebar label text.
|
label: string; // Sidebar label text.
|
||||||
items: SidebarItem[]; // Array of sidebar items.
|
items: SidebarItem[]; // Array of sidebar items.
|
||||||
|
className?: string;
|
||||||
|
|
||||||
// Category options:
|
// Category options:
|
||||||
collapsible: boolean; // Set the category to be collapsible
|
collapsible: boolean; // Set the category to be collapsible
|
||||||
|
@ -519,7 +522,8 @@ This is the easy tutorial!
|
||||||
```json title="docs/tutorials/_category_.json"
|
```json title="docs/tutorials/_category_.json"
|
||||||
{
|
{
|
||||||
"label": "Tutorial",
|
"label": "Tutorial",
|
||||||
"position": 3
|
"position": 3,
|
||||||
|
"className": "red"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -146,3 +146,7 @@ div[class^='announcementBar_'] {
|
||||||
);
|
);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.red > a {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue