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(),
|
||||
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),
|
||||
|
|
|
@ -30,6 +30,7 @@ declare module '@docusaurus/plugin-content-docs-types' {
|
|||
};
|
||||
|
||||
type PropsSidebarItemBase = {
|
||||
className?: string;
|
||||
customProps?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
|
|
|
@ -47,6 +47,7 @@ Available document ids are:
|
|||
type: 'link',
|
||||
label: sidebarLabel || item.label || title,
|
||||
href: permalink,
|
||||
className: item.className,
|
||||
customProps: item.customProps,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -140,6 +140,7 @@ declare module '@theme/DocSidebarItem' {
|
|||
type DocSidebarPropsBase = {
|
||||
readonly activePath: string;
|
||||
readonly onItemClick?: () => void;
|
||||
readonly level: number;
|
||||
readonly tabIndex?: number;
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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. |
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -146,3 +146,7 @@ div[class^='announcementBar_'] {
|
|||
);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.red > a {
|
||||
color: red;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue