diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/sidebars/sidebars-category.js b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/sidebars/sidebars-category.js index 975f1203feb7..402803c0c44d 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/sidebars/sidebars-category.js +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/sidebars/sidebars-category.js @@ -9,30 +9,25 @@ module.exports = { docs: [ { type: 'category', - collapsed: true, label: 'level 1', items: [ 'a', { type: 'category', - collapsed: true, label: 'level 2', items: [ { type: 'category', - collapsed: true, label: 'level 3', items: [ 'c', { type: 'category', - collapsed: true, label: 'level 4', items: [ 'd', { type: 'category', - collapsed: true, label: 'deeper more more', items: ['e'], }, diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap index 10b5e10336e8..407d7d3ebc12 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap @@ -7,6 +7,7 @@ Object { "collapsed": true, "items": Array [ Object { + "collapsed": true, "items": Array [ Object { "href": "/docs/foo/bar", diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/sidebars.test.ts.snap b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/sidebars.test.ts.snap index 0aa670c44b39..edf6e3d5837b 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/sidebars.test.ts.snap +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/sidebars.test.ts.snap @@ -157,6 +157,7 @@ exports[`loadSidebars sidebars with first level not a category 1`] = ` Object { "docs": Array [ Object { + "collapsed": true, "items": Array [ Object { "id": "greeting", diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/version.test.ts.snap b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/version.test.ts.snap index 26c10f3c0ab6..6f2a9ce513ea 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/version.test.ts.snap +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/version.test.ts.snap @@ -7,6 +7,7 @@ Object { "collapsed": true, "items": Array [ Object { + "collapsed": true, "items": Array [ Object { "id": "version-1.0.0/foo/bar", diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars.ts b/packages/docusaurus-plugin-content-docs/src/sidebars.ts index abe988984b2b..0fb341895344 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars.ts @@ -25,6 +25,9 @@ function isCategoryShorthand( return typeof item !== 'string' && !item.type; } +// categories are collapsed by default, unless user set collapsed = false +const defaultCategoryCollapsedValue = true; + /** * Convert {category1: [item1,item2]} shorthand syntax to long-form syntax */ @@ -33,7 +36,7 @@ function normalizeCategoryShorthand( ): SidebarItemCategoryRaw[] { return Object.entries(sidebar).map(([label, items]) => ({ type: 'category', - collapsed: true, + collapsed: defaultCategoryCollapsedValue, label, items, })); @@ -118,7 +121,13 @@ function normalizeItem(item: SidebarItemRaw): SidebarItem[] { switch (item.type) { case 'category': assertIsCategory(item); - return [{...item, items: flatMap(item.items, normalizeItem)}]; + return [ + { + collapsed: defaultCategoryCollapsedValue, + ...item, + items: flatMap(item.items, normalizeItem), + }, + ]; case 'link': assertIsLink(item); return [item]; diff --git a/packages/docusaurus-plugin-content-docs/src/types.ts b/packages/docusaurus-plugin-content-docs/src/types.ts index 642d9703ab6b..d9c4608bbdeb 100644 --- a/packages/docusaurus-plugin-content-docs/src/types.ts +++ b/packages/docusaurus-plugin-content-docs/src/types.ts @@ -42,7 +42,7 @@ export interface SidebarItemCategory { type: 'category'; label: string; items: SidebarItem[]; - collapsed?: boolean; + collapsed: boolean; } export interface SidebarItemCategoryRaw { diff --git a/packages/docusaurus-theme-classic/src/theme/DocSidebar/index.js b/packages/docusaurus-theme-classic/src/theme/DocSidebar/index.js index b8c8cff518d4..ebe1d4c9776c 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocSidebar/index.js +++ b/packages/docusaurus-theme-classic/src/theme/DocSidebar/index.js @@ -5,8 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import React, {useState, useCallback} from 'react'; -import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; +import React, {useState, useCallback, useEffect, useRef} from 'react'; import classnames from 'classnames'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import useAnnouncementBarContext from '@theme/hooks/useAnnouncementBarContext'; @@ -20,146 +19,140 @@ import styles from './styles.module.css'; const MOBILE_TOGGLE_SIZE = 24; -function DocSidebarItem({ +function usePrevious(value) { + const ref = useRef(value); + useEffect(() => { + ref.current = value; + }, [value]); + return ref.current; +} + +const isActiveSidebarItem = (item, activePath) => { + if (item.type === 'link') { + return item.href === activePath; + } + if (item.type === 'category') { + return item.items.some((subItem) => + isActiveSidebarItem(subItem, activePath), + ); + } + return false; +}; + +function DocSidebarItemCategory({ item, onItemClick, collapsible, activePath, ...props }) { - const {items, href, label, type} = item; - const [collapsed, setCollapsed] = useState(item.collapsed); - const [prevCollapsedProp, setPreviousCollapsedProp] = useState(null); + const {items, label} = item; - // If the collapsing state from props changed, probably a navigation event - // occurred. Overwrite the component's collapsed state with the props' - // collapsed value. - if (item.collapsed !== prevCollapsedProp) { - setPreviousCollapsedProp(item.collapsed); - setCollapsed(item.collapsed); - } + const isActive = isActiveSidebarItem(item, activePath); + const wasActive = usePrevious(isActive); - const handleItemClick = useCallback((e) => { - e.preventDefault(); - e.target.blur(); - setCollapsed((state) => !state); + // active categories are always initialized as expanded + // the default (item.collapsed) is only used for non-active categories + const [collapsed, setCollapsed] = useState(() => { + if (!collapsible) { + return false; + } + return isActive ? false : item.collapsed; }); - // Make sure we have access to the window - const activePageRelativeUrl = ExecutionEnvironment.canUseDOM - ? window.location.pathname + window.location.search - : null; - - // We need to know if the category item - // is the parent of the active page - // If it is, this returns true and make sure to highlight this category - const isCategoryOfActivePage = (_items, _activePageRelativeUrl) => { - // Make sure we have items - if (typeof _items !== 'undefined') { - return _items.some((categoryItem) => { - // Grab the category item's href - const childHref = categoryItem.href; - // Compare it to the current active page - return _activePageRelativeUrl === childHref; - }); + // If we navigate to a category, it should automatically expand itself + useEffect(() => { + const justBecameActive = isActive && !wasActive; + if (justBecameActive && collapsed) { + setCollapsed(false); } + }, [isActive, wasActive, collapsed]); - return false; - }; - - switch (type) { - case 'category': - return ( - items.length > 0 && ( -
  • - - {label} - - -
  • - ) - ); + const handleItemClick = useCallback( + (e) => { + e.preventDefault(); + e.target.blur(); + setCollapsed((state) => !state); + }, + [setCollapsed], + ); - case 'link': - default: - return ( -
  • - - {label} - -
  • - ); + if (items.length === 0) { + return null; } -} - -// Calculate the category collapsing state when a page navigation occurs. -// We want to automatically expand the categories which contains the current page. -function mutateSidebarCollapsingState(item, path) { - const {items, href, type} = item; - switch (type) { - case 'category': { - const anyChildItemsActive = - items - .map((childItem) => mutateSidebarCollapsingState(childItem, path)) - .filter((val) => val).length > 0; - - // Check if the user wants the category to be expanded by default - const shouldExpand = item.collapsed === false; - // eslint-disable-next-line no-param-reassign - item.collapsed = !anyChildItemsActive; - - if (shouldExpand) { - // eslint-disable-next-line no-param-reassign - item.collapsed = false; - } + return ( +
  • + + {label} + + +
  • + ); +} - return anyChildItemsActive; - } +function DocSidebarItemLink({ + item, + onItemClick, + activePath, + collapsible, + ...props +}) { + const {href, label} = item; + const isActive = isActiveSidebarItem(item, activePath); + return ( +
  • + + {label} + +
  • + ); +} +function DocSidebarItem(props) { + switch (props.item.type) { + case 'category': + return ; case 'link': default: - return href === path; + return ; } } @@ -196,12 +189,6 @@ function DocSidebar(props) { ); } - if (sidebarCollapsible) { - sidebarData.forEach((sidebarItem) => - mutateSidebarCollapsingState(sidebarItem, path), - ); - } - return (