Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 15 commits into from
Jan 20, 2022
10 changes: 2 additions & 8 deletions packages/docusaurus-theme-classic/src/theme-classic.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,19 +149,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;
Expand All @@ -171,7 +165,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[];
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
useCollapsible,
findFirstCategoryLink,
ThemeClassNames,
useThemeConfig,
useDocSidebarItemsExpandedState,
} from '@docusaurus/theme-common';
import Link from '@docusaurus/Link';
import isInternalUrl from '@docusaurus/isInternalUrl';
Expand Down Expand Up @@ -92,14 +94,15 @@ function DocSidebarItemCategory({
onItemClick,
activePath,
level,
index,
...props
}: Props & {item: PropSidebarItemCategory}) {
const {items, label, collapsible, className, href} = item;
const hrefWithSSRFallback = useCategoryHrefWithSSRFallback(item);

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: () => {
Expand All @@ -111,6 +114,28 @@ function DocSidebarItemCategory({
});

useAutoExpandActiveCategory({isActive, collapsed, setCollapsed});
const {expandedItem, setExpandedItem} = useDocSidebarItemsExpandedState();
function updateCollapsed(toCollapsed: boolean = !collapsed) {
setExpandedItem(toCollapsed ? null : index);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only case we have to think twice about UX is when there are multiple sidebar categories expanded by default. In this implementation, the expandedItem context is initialized to null, and when the user collapses one of those default-expanded categories, the others remain expanded until any category is toggled to be expanded.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m excited this feature is finally getting some steam

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! This is the UX I've been looking forward to, so I did invest some time in this :D

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven’t really looked at the code, just a quick glance but I would set the default setting to false, I know not everyone will want this feature on by default, and that was an issue when I first tried to get this in over a year ago 😂

Copy link
Collaborator

@Josh-Cena Josh-Cena Jan 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's indeed defaulted to false :) Just that it would be cool to be turned on for our website.

setCollapsed(toCollapsed);
}
const {autoCollapseSidebarCategories} = useThemeConfig();
useEffect(() => {
if (
collapsible &&
expandedItem &&
expandedItem !== index &&
autoCollapseSidebarCategories
) {
setCollapsed(true);
}
}, [
collapsible,
expandedItem,
index,
setCollapsed,
autoCollapseSidebarCategories,
]);

return (
<li
Expand All @@ -136,10 +161,10 @@ function DocSidebarItemCategory({
? (e) => {
onItemClick?.(item);
if (href) {
setCollapsed(false);
updateCollapsed(false);
} else {
e.preventDefault();
toggleCollapsed();
updateCollapsed();
}
}
: () => {
Expand All @@ -165,7 +190,7 @@ function DocSidebarItemCategory({
className="clean-btn menu__caret"
onClick={(e) => {
e.preventDefault();
toggleCollapsed();
updateCollapsed();
}}
/>
)}
Expand All @@ -189,6 +214,7 @@ function DocSidebarItemLink({
onItemClick,
activePath,
level,
index,
...props
}: Props & {item: PropSidebarItemLink}) {
const {href, label, className} = item;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,25 @@
*/

import React, {memo} from 'react';

import DocSidebarItem from '@theme/DocSidebarItem';
import {DocSidebarItemsExpandedStateProvider} from '@docusaurus/theme-common';

import type {Props} from '@theme/DocSidebarItems';

// TODO this item should probably not receive the "activePath" props
// 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>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const DEFAULT_CONFIG = {
items: [],
},
hideableSidebar: false,
autoCollapseSidebarCategories: false,
tableOfContents: {
minHeadingLevel: 2,
maxHeadingLevel: 3,
Expand Down Expand Up @@ -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',
Expand Down
4 changes: 4 additions & 0 deletions packages/docusaurus-theme-common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
*/

export {useThemeConfig} from './utils/useThemeConfig';
export {
DocSidebarItemsExpandedStateProvider,
useDocSidebarItemsExpandedState,
} from './utils/docSidebarItemsExpandedState';

export type {
ThemeConfig,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export type ThemeConfig = {
prism: PrismConfig;
footer?: Footer;
hideableSidebar: boolean;
autoCollapseSidebarCategories: boolean;
image?: string;
metadata: Array<Record<string, string>>;
sidebarCollapsible: boolean;
Expand Down
1 change: 1 addition & 0 deletions website/docs/api/docusaurus.config.js.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ Example:
module.exports = {
themeConfig: {
hideableSidebar: false,
autoCollapseSidebarCategories: false,
colorMode: {
defaultMode: 'light',
disableSwitch: false,
Expand Down
17 changes: 16 additions & 1 deletion website/docs/guides/docs/sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand All @@ -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**.
Expand Down
1 change: 1 addition & 0 deletions website/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ const config = {
playgroundPosition: 'bottom',
},
hideableSidebar: true,
autoCollapseSidebarCategories: true,
colorMode: {
defaultMode: 'light',
disableSwitch: false,
Expand Down