mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-02 03:37:48 +02:00
feat(theme-classic): auto-collapse sibling categories in doc sidebar (#3811)
Co-authored-by: Josh-Cena <sidachen2003@gmail.com>
This commit is contained in:
parent
c9a6c7b6fb
commit
8ce3cee400
10 changed files with 104 additions and 16 deletions
|
@ -151,19 +151,13 @@ declare module '@theme/DocSidebar' {
|
||||||
declare module '@theme/DocSidebarItem' {
|
declare module '@theme/DocSidebarItem' {
|
||||||
import type {PropSidebarItem} from '@docusaurus/plugin-content-docs';
|
import type {PropSidebarItem} from '@docusaurus/plugin-content-docs';
|
||||||
|
|
||||||
export type DocSidebarPropsBase = {
|
|
||||||
readonly activePath: string;
|
|
||||||
readonly onItemClick?: (item: PropSidebarItem) => void;
|
|
||||||
readonly level: number;
|
|
||||||
readonly tabIndex?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
readonly activePath: string;
|
readonly activePath: string;
|
||||||
readonly onItemClick?: (item: PropSidebarItem) => void;
|
readonly onItemClick?: (item: PropSidebarItem) => void;
|
||||||
readonly level: number;
|
readonly level: number;
|
||||||
readonly tabIndex?: number;
|
readonly tabIndex?: number;
|
||||||
readonly item: PropSidebarItem;
|
readonly item: PropSidebarItem;
|
||||||
|
readonly index: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DocSidebarItem(props: Props): JSX.Element;
|
export default function DocSidebarItem(props: Props): JSX.Element;
|
||||||
|
@ -173,7 +167,7 @@ declare module '@theme/DocSidebarItems' {
|
||||||
import type {Props as DocSidebarItemProps} from '@theme/DocSidebarItem';
|
import type {Props as DocSidebarItemProps} from '@theme/DocSidebarItem';
|
||||||
import type {PropSidebarItem} from '@docusaurus/plugin-content-docs';
|
import type {PropSidebarItem} from '@docusaurus/plugin-content-docs';
|
||||||
|
|
||||||
export type Props = Omit<DocSidebarItemProps, 'item'> & {
|
export type Props = Omit<DocSidebarItemProps, 'item' | 'index'> & {
|
||||||
readonly items: readonly PropSidebarItem[];
|
readonly items: readonly PropSidebarItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,8 @@ import {
|
||||||
useCollapsible,
|
useCollapsible,
|
||||||
findFirstCategoryLink,
|
findFirstCategoryLink,
|
||||||
ThemeClassNames,
|
ThemeClassNames,
|
||||||
|
useThemeConfig,
|
||||||
|
useDocSidebarItemsExpandedState,
|
||||||
} from '@docusaurus/theme-common';
|
} from '@docusaurus/theme-common';
|
||||||
import Link from '@docusaurus/Link';
|
import Link from '@docusaurus/Link';
|
||||||
import isInternalUrl from '@docusaurus/isInternalUrl';
|
import isInternalUrl from '@docusaurus/isInternalUrl';
|
||||||
|
@ -92,6 +94,7 @@ function DocSidebarItemCategory({
|
||||||
onItemClick,
|
onItemClick,
|
||||||
activePath,
|
activePath,
|
||||||
level,
|
level,
|
||||||
|
index,
|
||||||
...props
|
...props
|
||||||
}: Props & {item: PropSidebarItemCategory}) {
|
}: Props & {item: PropSidebarItemCategory}) {
|
||||||
const {items, label, collapsible, className, href} = item;
|
const {items, label, collapsible, className, href} = item;
|
||||||
|
@ -99,7 +102,7 @@ function DocSidebarItemCategory({
|
||||||
|
|
||||||
const isActive = isActiveSidebarItem(item, activePath);
|
const isActive = isActiveSidebarItem(item, activePath);
|
||||||
|
|
||||||
const {collapsed, setCollapsed, toggleCollapsed} = useCollapsible({
|
const {collapsed, setCollapsed} = useCollapsible({
|
||||||
// active categories are always initialized as expanded
|
// active categories are always initialized as expanded
|
||||||
// the default (item.collapsed) is only used for non-active categories
|
// the default (item.collapsed) is only used for non-active categories
|
||||||
initialState: () => {
|
initialState: () => {
|
||||||
|
@ -111,6 +114,28 @@ function DocSidebarItemCategory({
|
||||||
});
|
});
|
||||||
|
|
||||||
useAutoExpandActiveCategory({isActive, collapsed, setCollapsed});
|
useAutoExpandActiveCategory({isActive, collapsed, setCollapsed});
|
||||||
|
const {expandedItem, setExpandedItem} = useDocSidebarItemsExpandedState();
|
||||||
|
function updateCollapsed(toCollapsed: boolean = !collapsed) {
|
||||||
|
setExpandedItem(toCollapsed ? null : index);
|
||||||
|
setCollapsed(toCollapsed);
|
||||||
|
}
|
||||||
|
const {autoCollapseSidebarCategories} = useThemeConfig();
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
collapsible &&
|
||||||
|
expandedItem &&
|
||||||
|
expandedItem !== index &&
|
||||||
|
autoCollapseSidebarCategories
|
||||||
|
) {
|
||||||
|
setCollapsed(true);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
collapsible,
|
||||||
|
expandedItem,
|
||||||
|
index,
|
||||||
|
setCollapsed,
|
||||||
|
autoCollapseSidebarCategories,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
|
@ -136,10 +161,10 @@ function DocSidebarItemCategory({
|
||||||
? (e) => {
|
? (e) => {
|
||||||
onItemClick?.(item);
|
onItemClick?.(item);
|
||||||
if (href) {
|
if (href) {
|
||||||
setCollapsed(false);
|
updateCollapsed(false);
|
||||||
} else {
|
} else {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
toggleCollapsed();
|
updateCollapsed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: () => {
|
: () => {
|
||||||
|
@ -165,7 +190,7 @@ function DocSidebarItemCategory({
|
||||||
className="clean-btn menu__caret"
|
className="clean-btn menu__caret"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
toggleCollapsed();
|
updateCollapsed();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -189,6 +214,7 @@ function DocSidebarItemLink({
|
||||||
onItemClick,
|
onItemClick,
|
||||||
activePath,
|
activePath,
|
||||||
level,
|
level,
|
||||||
|
index,
|
||||||
...props
|
...props
|
||||||
}: Props & {item: PropSidebarItemLink}) {
|
}: Props & {item: PropSidebarItemLink}) {
|
||||||
const {href, label, className} = item;
|
const {href, label, className} = item;
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {memo} from 'react';
|
import React, {memo} from 'react';
|
||||||
|
|
||||||
import DocSidebarItem from '@theme/DocSidebarItem';
|
import DocSidebarItem from '@theme/DocSidebarItem';
|
||||||
|
import {DocSidebarItemsExpandedStateProvider} from '@docusaurus/theme-common';
|
||||||
|
|
||||||
import type {Props} from '@theme/DocSidebarItems';
|
import type {Props} from '@theme/DocSidebarItems';
|
||||||
|
|
||||||
|
@ -15,15 +15,16 @@ import type {Props} from '@theme/DocSidebarItems';
|
||||||
// TODO this triggers whole sidebar re-renders on navigation
|
// TODO this triggers whole sidebar re-renders on navigation
|
||||||
function DocSidebarItems({items, ...props}: Props): JSX.Element {
|
function DocSidebarItems({items, ...props}: Props): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<>
|
<DocSidebarItemsExpandedStateProvider>
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<DocSidebarItem
|
<DocSidebarItem
|
||||||
key={index} // sidebar is static, the index does not change
|
key={index} // sidebar is static, the index does not change
|
||||||
item={item}
|
item={item}
|
||||||
|
index={index}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</DocSidebarItemsExpandedStateProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,7 @@ const DEFAULT_CONFIG = {
|
||||||
items: [],
|
items: [],
|
||||||
},
|
},
|
||||||
hideableSidebar: false,
|
hideableSidebar: false,
|
||||||
|
autoCollapseSidebarCategories: false,
|
||||||
tableOfContents: {
|
tableOfContents: {
|
||||||
minHeadingLevel: 2,
|
minHeadingLevel: 2,
|
||||||
maxHeadingLevel: 3,
|
maxHeadingLevel: 3,
|
||||||
|
@ -352,6 +353,9 @@ const ThemeConfigSchema = Joi.object({
|
||||||
.default(DEFAULT_CONFIG.prism)
|
.default(DEFAULT_CONFIG.prism)
|
||||||
.unknown(),
|
.unknown(),
|
||||||
hideableSidebar: Joi.bool().default(DEFAULT_CONFIG.hideableSidebar),
|
hideableSidebar: Joi.bool().default(DEFAULT_CONFIG.hideableSidebar),
|
||||||
|
autoCollapseSidebarCategories: Joi.bool().default(
|
||||||
|
DEFAULT_CONFIG.autoCollapseSidebarCategories,
|
||||||
|
),
|
||||||
sidebarCollapsible: Joi.forbidden().messages({
|
sidebarCollapsible: Joi.forbidden().messages({
|
||||||
'any.unknown':
|
'any.unknown':
|
||||||
'The themeConfig.sidebarCollapsible has been moved to docs plugin options. See: https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-docs',
|
'The themeConfig.sidebarCollapsible has been moved to docs plugin options. See: https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-docs',
|
||||||
|
|
|
@ -6,6 +6,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export {useThemeConfig} from './utils/useThemeConfig';
|
export {useThemeConfig} from './utils/useThemeConfig';
|
||||||
|
export {
|
||||||
|
DocSidebarItemsExpandedStateProvider,
|
||||||
|
useDocSidebarItemsExpandedState,
|
||||||
|
} from './utils/docSidebarItemsExpandedState';
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
ThemeConfig,
|
ThemeConfig,
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {type ReactNode, useMemo, useState, useContext} from 'react';
|
||||||
|
|
||||||
|
const EmptyContext: unique symbol = Symbol('EmptyContext');
|
||||||
|
const Context = React.createContext<
|
||||||
|
DocSidebarItemsExpandedState | typeof EmptyContext
|
||||||
|
>(EmptyContext);
|
||||||
|
type DocSidebarItemsExpandedState = {
|
||||||
|
expandedItem: number | null;
|
||||||
|
setExpandedItem: (a: number | null) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DocSidebarItemsExpandedStateProvider({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
}): JSX.Element {
|
||||||
|
const [expandedItem, setExpandedItem] = useState<number | null>(null);
|
||||||
|
const contextValue = useMemo(
|
||||||
|
() => ({expandedItem, setExpandedItem}),
|
||||||
|
[expandedItem],
|
||||||
|
);
|
||||||
|
|
||||||
|
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDocSidebarItemsExpandedState(): DocSidebarItemsExpandedState {
|
||||||
|
const contextValue = useContext(Context);
|
||||||
|
if (contextValue === EmptyContext) {
|
||||||
|
throw new Error(
|
||||||
|
'This hook requires usage of <DocSidebarItemsExpandedStateProvider>',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return contextValue;
|
||||||
|
}
|
|
@ -122,6 +122,7 @@ export type ThemeConfig = {
|
||||||
prism: PrismConfig;
|
prism: PrismConfig;
|
||||||
footer?: Footer;
|
footer?: Footer;
|
||||||
hideableSidebar: boolean;
|
hideableSidebar: boolean;
|
||||||
|
autoCollapseSidebarCategories: boolean;
|
||||||
image?: string;
|
image?: string;
|
||||||
metadata: Array<Record<string, string>>;
|
metadata: Array<Record<string, string>>;
|
||||||
sidebarCollapsible: boolean;
|
sidebarCollapsible: boolean;
|
||||||
|
|
|
@ -279,6 +279,7 @@ Example:
|
||||||
module.exports = {
|
module.exports = {
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
hideableSidebar: false,
|
hideableSidebar: false,
|
||||||
|
autoCollapseSidebarCategories: false,
|
||||||
colorMode: {
|
colorMode: {
|
||||||
defaultMode: 'light',
|
defaultMode: 'light',
|
||||||
disableSwitch: false,
|
disableSwitch: false,
|
||||||
|
|
|
@ -913,7 +913,9 @@ module.exports = {
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Hideable sidebar {#hideable-sidebar}
|
## Theme configuration
|
||||||
|
|
||||||
|
### Hideable sidebar {#hideable-sidebar}
|
||||||
|
|
||||||
By enabling the `themeConfig.hideableSidebar` option, you can make the entire sidebar hideable, allowing users to better focus on the content. This is especially useful when content is consumed on medium-sized screens (e.g. tablets).
|
By enabling the `themeConfig.hideableSidebar` option, you can make the entire sidebar hideable, allowing users to better focus on the content. This is especially useful when content is consumed on medium-sized screens (e.g. tablets).
|
||||||
|
|
||||||
|
@ -927,6 +929,19 @@ module.exports = {
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Auto-collapse sidebar categories
|
||||||
|
|
||||||
|
The `themeConfig.autoCollapseSidebarCategories` option would collapse all sibling categories when expanding one category. This saves the user from having too many categories open and helps them focus on the selected section.
|
||||||
|
|
||||||
|
```js title="docusaurus.config.js"
|
||||||
|
module.exports = {
|
||||||
|
themeConfig: {
|
||||||
|
// highlight-next-line
|
||||||
|
autoCollapseSidebarCategories: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
## Using multiple sidebars {#using-multiple-sidebars}
|
## Using multiple sidebars {#using-multiple-sidebars}
|
||||||
|
|
||||||
You can create a sidebar for each **set of Markdown files** that you want to **group together**.
|
You can create a sidebar for each **set of Markdown files** that you want to **group together**.
|
||||||
|
|
|
@ -316,6 +316,7 @@ const config = {
|
||||||
playgroundPosition: 'bottom',
|
playgroundPosition: 'bottom',
|
||||||
},
|
},
|
||||||
hideableSidebar: true,
|
hideableSidebar: true,
|
||||||
|
autoCollapseSidebarCategories: true,
|
||||||
colorMode: {
|
colorMode: {
|
||||||
defaultMode: 'light',
|
defaultMode: 'light',
|
||||||
disableSwitch: false,
|
disableSwitch: false,
|
||||||
|
|
Loading…
Add table
Reference in a new issue