From 8ce3cee400c96bcf109f8b55fc24a348c153b83e Mon Sep 17 00:00:00 2001 From: Joseph Date: Thu, 20 Jan 2022 07:38:16 -0800 Subject: [PATCH] feat(theme-classic): auto-collapse sibling categories in doc sidebar (#3811) Co-authored-by: Josh-Cena --- .../src/theme-classic.d.ts | 10 +---- .../src/theme/DocSidebarItem/index.tsx | 34 +++++++++++++-- .../src/theme/DocSidebarItems/index.tsx | 7 ++-- .../src/validateThemeConfig.ts | 4 ++ packages/docusaurus-theme-common/src/index.ts | 4 ++ .../utils/docSidebarItemsExpandedState.tsx | 41 +++++++++++++++++++ .../src/utils/useThemeConfig.ts | 1 + website/docs/api/docusaurus.config.js.md | 1 + website/docs/guides/docs/sidebar.md | 17 +++++++- website/docusaurus.config.js | 1 + 10 files changed, 104 insertions(+), 16 deletions(-) create mode 100644 packages/docusaurus-theme-common/src/utils/docSidebarItemsExpandedState.tsx diff --git a/packages/docusaurus-theme-classic/src/theme-classic.d.ts b/packages/docusaurus-theme-classic/src/theme-classic.d.ts index 89f9dde359..23bb1fdf9d 100644 --- a/packages/docusaurus-theme-classic/src/theme-classic.d.ts +++ b/packages/docusaurus-theme-classic/src/theme-classic.d.ts @@ -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 & { + export type Props = Omit & { readonly items: readonly PropSidebarItem[]; }; diff --git a/packages/docusaurus-theme-classic/src/theme/DocSidebarItem/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocSidebarItem/index.tsx index f94d8077e8..0f9ba7e119 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocSidebarItem/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocSidebarItem/index.tsx @@ -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 (
  • { 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; diff --git a/packages/docusaurus-theme-classic/src/theme/DocSidebarItems/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocSidebarItems/index.tsx index baec593128..9d61191264 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocSidebarItems/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocSidebarItems/index.tsx @@ -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 ( - <> + {items.map((item, index) => ( ))} - + ); } diff --git a/packages/docusaurus-theme-classic/src/validateThemeConfig.ts b/packages/docusaurus-theme-classic/src/validateThemeConfig.ts index 2bc4fbbdb7..449b7f4e8f 100644 --- a/packages/docusaurus-theme-classic/src/validateThemeConfig.ts +++ b/packages/docusaurus-theme-classic/src/validateThemeConfig.ts @@ -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', diff --git a/packages/docusaurus-theme-common/src/index.ts b/packages/docusaurus-theme-common/src/index.ts index 5c622ec999..07e6348abc 100644 --- a/packages/docusaurus-theme-common/src/index.ts +++ b/packages/docusaurus-theme-common/src/index.ts @@ -6,6 +6,10 @@ */ export {useThemeConfig} from './utils/useThemeConfig'; +export { + DocSidebarItemsExpandedStateProvider, + useDocSidebarItemsExpandedState, +} from './utils/docSidebarItemsExpandedState'; export type { ThemeConfig, diff --git a/packages/docusaurus-theme-common/src/utils/docSidebarItemsExpandedState.tsx b/packages/docusaurus-theme-common/src/utils/docSidebarItemsExpandedState.tsx new file mode 100644 index 0000000000..c2435f6931 --- /dev/null +++ b/packages/docusaurus-theme-common/src/utils/docSidebarItemsExpandedState.tsx @@ -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(null); + const contextValue = useMemo( + () => ({expandedItem, setExpandedItem}), + [expandedItem], + ); + + return {children}; +} + +export function useDocSidebarItemsExpandedState(): DocSidebarItemsExpandedState { + const contextValue = useContext(Context); + if (contextValue === EmptyContext) { + throw new Error( + 'This hook requires usage of ', + ); + } + return contextValue; +} diff --git a/packages/docusaurus-theme-common/src/utils/useThemeConfig.ts b/packages/docusaurus-theme-common/src/utils/useThemeConfig.ts index f045caa029..26975d2563 100644 --- a/packages/docusaurus-theme-common/src/utils/useThemeConfig.ts +++ b/packages/docusaurus-theme-common/src/utils/useThemeConfig.ts @@ -122,6 +122,7 @@ export type ThemeConfig = { prism: PrismConfig; footer?: Footer; hideableSidebar: boolean; + autoCollapseSidebarCategories: boolean; image?: string; metadata: Array>; sidebarCollapsible: boolean; diff --git a/website/docs/api/docusaurus.config.js.md b/website/docs/api/docusaurus.config.js.md index d3d4939aea..ab50dc5395 100644 --- a/website/docs/api/docusaurus.config.js.md +++ b/website/docs/api/docusaurus.config.js.md @@ -279,6 +279,7 @@ Example: module.exports = { themeConfig: { hideableSidebar: false, + autoCollapseSidebarCategories: false, colorMode: { defaultMode: 'light', disableSwitch: false, diff --git a/website/docs/guides/docs/sidebar.md b/website/docs/guides/docs/sidebar.md index 5951be10f3..f10928ff1b 100644 --- a/website/docs/guides/docs/sidebar.md +++ b/website/docs/guides/docs/sidebar.md @@ -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**. diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index a808700a7b..a7bdc0edb9 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -316,6 +316,7 @@ const config = { playgroundPosition: 'bottom', }, hideableSidebar: true, + autoCollapseSidebarCategories: true, colorMode: { defaultMode: 'light', disableSwitch: false,