mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-01 19:27: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' {
|
||||
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 {
|
||||
readonly activePath: string;
|
||||
readonly onItemClick?: (item: PropSidebarItem) => void;
|
||||
readonly level: number;
|
||||
readonly tabIndex?: number;
|
||||
readonly item: PropSidebarItem;
|
||||
readonly index: number;
|
||||
}
|
||||
|
||||
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 {PropSidebarItem} from '@docusaurus/plugin-content-docs';
|
||||
|
||||
export type Props = Omit<DocSidebarItemProps, 'item'> & {
|
||||
export type Props = Omit<DocSidebarItemProps, 'item' | 'index'> & {
|
||||
readonly items: readonly PropSidebarItem[];
|
||||
};
|
||||
|
||||
|
|
|
@ -14,6 +14,8 @@ import {
|
|||
useCollapsible,
|
||||
findFirstCategoryLink,
|
||||
ThemeClassNames,
|
||||
useThemeConfig,
|
||||
useDocSidebarItemsExpandedState,
|
||||
} from '@docusaurus/theme-common';
|
||||
import Link from '@docusaurus/Link';
|
||||
import isInternalUrl from '@docusaurus/isInternalUrl';
|
||||
|
@ -92,6 +94,7 @@ function DocSidebarItemCategory({
|
|||
onItemClick,
|
||||
activePath,
|
||||
level,
|
||||
index,
|
||||
...props
|
||||
}: Props & {item: PropSidebarItemCategory}) {
|
||||
const {items, label, collapsible, className, href} = item;
|
||||
|
@ -99,7 +102,7 @@ function DocSidebarItemCategory({
|
|||
|
||||
const isActive = isActiveSidebarItem(item, activePath);
|
||||
|
||||
const {collapsed, setCollapsed, toggleCollapsed} = useCollapsible({
|
||||
const {collapsed, setCollapsed} = useCollapsible({
|
||||
// active categories are always initialized as expanded
|
||||
// the default (item.collapsed) is only used for non-active categories
|
||||
initialState: () => {
|
||||
|
@ -111,6 +114,28 @@ function DocSidebarItemCategory({
|
|||
});
|
||||
|
||||
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 (
|
||||
<li
|
||||
|
@ -136,10 +161,10 @@ function DocSidebarItemCategory({
|
|||
? (e) => {
|
||||
onItemClick?.(item);
|
||||
if (href) {
|
||||
setCollapsed(false);
|
||||
updateCollapsed(false);
|
||||
} else {
|
||||
e.preventDefault();
|
||||
toggleCollapsed();
|
||||
updateCollapsed();
|
||||
}
|
||||
}
|
||||
: () => {
|
||||
|
@ -165,7 +190,7 @@ function DocSidebarItemCategory({
|
|||
className="clean-btn menu__caret"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
toggleCollapsed();
|
||||
updateCollapsed();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -189,6 +214,7 @@ function DocSidebarItemLink({
|
|||
onItemClick,
|
||||
activePath,
|
||||
level,
|
||||
index,
|
||||
...props
|
||||
}: Props & {item: PropSidebarItemLink}) {
|
||||
const {href, label, className} = item;
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
*/
|
||||
|
||||
import React, {memo} from 'react';
|
||||
|
||||
import DocSidebarItem from '@theme/DocSidebarItem';
|
||||
import {DocSidebarItemsExpandedStateProvider} from '@docusaurus/theme-common';
|
||||
|
||||
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
|
||||
function DocSidebarItems({items, ...props}: Props): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<DocSidebarItemsExpandedStateProvider>
|
||||
{items.map((item, index) => (
|
||||
<DocSidebarItem
|
||||
key={index} // sidebar is static, the index does not change
|
||||
item={item}
|
||||
index={index}
|
||||
{...props}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
</DocSidebarItemsExpandedStateProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ const DEFAULT_CONFIG = {
|
|||
items: [],
|
||||
},
|
||||
hideableSidebar: false,
|
||||
autoCollapseSidebarCategories: false,
|
||||
tableOfContents: {
|
||||
minHeadingLevel: 2,
|
||||
maxHeadingLevel: 3,
|
||||
|
@ -352,6 +353,9 @@ const ThemeConfigSchema = Joi.object({
|
|||
.default(DEFAULT_CONFIG.prism)
|
||||
.unknown(),
|
||||
hideableSidebar: Joi.bool().default(DEFAULT_CONFIG.hideableSidebar),
|
||||
autoCollapseSidebarCategories: Joi.bool().default(
|
||||
DEFAULT_CONFIG.autoCollapseSidebarCategories,
|
||||
),
|
||||
sidebarCollapsible: Joi.forbidden().messages({
|
||||
'any.unknown':
|
||||
'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 {
|
||||
DocSidebarItemsExpandedStateProvider,
|
||||
useDocSidebarItemsExpandedState,
|
||||
} from './utils/docSidebarItemsExpandedState';
|
||||
|
||||
export type {
|
||||
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;
|
||||
footer?: Footer;
|
||||
hideableSidebar: boolean;
|
||||
autoCollapseSidebarCategories: boolean;
|
||||
image?: string;
|
||||
metadata: Array<Record<string, string>>;
|
||||
sidebarCollapsible: boolean;
|
||||
|
|
|
@ -279,6 +279,7 @@ Example:
|
|||
module.exports = {
|
||||
themeConfig: {
|
||||
hideableSidebar: false,
|
||||
autoCollapseSidebarCategories: false,
|
||||
colorMode: {
|
||||
defaultMode: 'light',
|
||||
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).
|
||||
|
||||
|
@ -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}
|
||||
|
||||
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',
|
||||
},
|
||||
hideableSidebar: true,
|
||||
autoCollapseSidebarCategories: true,
|
||||
colorMode: {
|
||||
defaultMode: 'light',
|
||||
disableSwitch: false,
|
||||
|
|
Loading…
Add table
Reference in a new issue