feat(theme-classic): auto-collapse sibling categories in doc sidebar (#3811)

Co-authored-by: Josh-Cena <sidachen2003@gmail.com>
This commit is contained in:
Joseph 2022-01-20 07:38:16 -08:00 committed by GitHub
parent c9a6c7b6fb
commit 8ce3cee400
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 104 additions and 16 deletions

View file

@ -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[];
}; };

View file

@ -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;

View file

@ -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>
); );
} }

View file

@ -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',

View file

@ -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,

View file

@ -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;
}

View file

@ -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;

View file

@ -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,

View file

@ -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**.

View file

@ -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,