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

refactor(v2): simplify and optimize sidebar #4617

Merged
merged 2 commits into from
Apr 15, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
262 changes: 151 additions & 111 deletions packages/docusaurus-theme-classic/src/theme/DocSidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/

import React, {useState, useCallback, useEffect, useRef, useMemo} from 'react';
import React, {useState, useCallback, useEffect, useRef, memo} from 'react';
import clsx from 'clsx';
import {useThemeConfig, isSamePath} from '@docusaurus/theme-common';
import useUserPreferencesContext from '@theme/hooks/useUserPreferencesContext';
Expand Down Expand Up @@ -44,6 +44,32 @@ const isActiveSidebarItem = (item, activePath) => {
return false;
};

// Optimize sidebar at each "level"
// TODO this item should probably not receive the "activePath" props
// TODO this triggers whole sidebar re-renders on navigation
const DocSidebarItems = memo(function DocSidebarItems({
items,
...props
}: any): JSX.Element {
return items.map((item, index) => (
<DocSidebarItem
key={index} // sidebar is static, the index does not change
item={item}
{...props}
/>
));
});

function DocSidebarItem(props): JSX.Element {
switch (props.item.type) {
case 'category':
return <DocSidebarItemCategory {...props} />;
case 'link':
default:
return <DocSidebarItemLink {...props} />;
}
}

function DocSidebarItemCategory({
item,
onItemClick,
Expand Down Expand Up @@ -104,8 +130,7 @@ function DocSidebarItemCategory({
<li
className={clsx('menu__list-item', {
'menu__list-item--collapsed': collapsed,
})}
key={label}>
})}>
<a
className={clsx('menu__link', {
'menu__link--sublist': collapsible,
Expand All @@ -128,16 +153,13 @@ function DocSidebarItemCategory({
handleMenuListHeight(false);
}
}}>
{items.map((childItem) => (
<DocSidebarItem
tabIndex={collapsed ? '-1' : '0'}
key={childItem.label}
item={childItem}
onItemClick={onItemClick}
collapsible={collapsible}
activePath={activePath}
/>
))}
<DocSidebarItems
items={items}
tabIndex={collapsed ? '-1' : '0'}
onItemClick={onItemClick}
collapsible={collapsible}
activePath={activePath}
/>
</ul>
</li>
);
Expand Down Expand Up @@ -172,37 +194,24 @@ function DocSidebarItemLink({
);
}

function DocSidebarItem(props): JSX.Element {
switch (props.item.type) {
case 'category':
return <DocSidebarItemCategory {...props} />;
case 'link':
default:
return <DocSidebarItemLink {...props} />;
}
}

function DocSidebar({
path,
sidebar,
sidebarCollapsible = true,
onCollapse,
isHidden,
}: Props): JSX.Element | null {
const [showResponsiveSidebar, setShowResponsiveSidebar] = useState(false);
const [showAnnouncementBar, setShowAnnouncementBar] = useState(true);
const {
navbar: {hideOnScroll},
hideableSidebar,
} = useThemeConfig();
function useShowAnnouncementBar() {
const {isAnnouncementBarClosed} = useUserPreferencesContext();
const [showAnnouncementBar, setShowAnnouncementBar] = useState(
!isAnnouncementBarClosed,
);
useScrollPosition(({scrollY}) => {
setShowAnnouncementBar(scrollY === 0);
if (!isAnnouncementBarClosed) {
setShowAnnouncementBar(scrollY === 0);
}
});
return showAnnouncementBar;
}

function useResponsiveSidebar() {
const [showResponsiveSidebar, setShowResponsiveSidebar] = useState(false);
useLockBodyScroll(showResponsiveSidebar);
const windowSize = useWindowSize();

const windowSize = useWindowSize();
useEffect(() => {
if (windowSize === windowSizes.desktop) {
setShowResponsiveSidebar(false);
Expand All @@ -216,19 +225,99 @@ function DocSidebar({
},
[setShowResponsiveSidebar],
);
const sidebarItems = useMemo(
() =>
sidebar.map((item) => (
<DocSidebarItem
key={item.label}
item={item}
onItemClick={closeResponsiveSidebar}
collapsible={sidebarCollapsible}
activePath={path}

const toggleResponsiveSidebar = useCallback(() => {
setShowResponsiveSidebar(!showResponsiveSidebar);
}, [setShowResponsiveSidebar]);

return {
showResponsiveSidebar,
closeResponsiveSidebar,
toggleResponsiveSidebar,
};
}

function HideableSidebarButton({onClick}) {
return (
<button
type="button"
title={translate({
id: 'theme.docs.sidebar.collapseButtonTitle',
message: 'Collapse sidebar',
description: 'The title attribute for collapse button of doc sidebar',
})}
aria-label={translate({
id: 'theme.docs.sidebar.collapseButtonAriaLabel',
message: 'Collapse sidebar',
description: 'The title attribute for collapse button of doc sidebar',
})}
className={clsx(
'button button--secondary button--outline',
styles.collapseSidebarButton,
)}
onClick={onClick}>
<IconArrow className={styles.collapseSidebarButtonIcon} />
</button>
);
}

function ResponsiveSidebarButton({responsiveSidebarOpened, onClick}) {
return (
<button
aria-label={
responsiveSidebarOpened
? translate({
id: 'theme.docs.sidebar.responsiveCloseButtonLabel',
message: 'Close menu',
description:
'The ARIA label for close button of mobile doc sidebar',
})
: translate({
id: 'theme.docs.sidebar.responsiveOpenButtonLabel',
message: 'Open menu',
description:
'The ARIA label for open button of mobile doc sidebar',
})
}
aria-haspopup="true"
className="button button--secondary button--sm menu__button"
type="button"
onClick={onClick}>
{responsiveSidebarOpened ? (
<span
className={clsx(styles.sidebarMenuIcon, styles.sidebarMenuCloseIcon)}>
&times;
</span>
) : (
<IconMenu
className={styles.sidebarMenuIcon}
height={MOBILE_TOGGLE_SIZE}
width={MOBILE_TOGGLE_SIZE}
/>
)),
[sidebar, sidebarCollapsible, path, closeResponsiveSidebar],
)}
</button>
);
}

function DocSidebar({
path,
sidebar,
sidebarCollapsible = true,
onCollapse,
isHidden,
}: Props): JSX.Element | null {
const showAnnouncementBar = useShowAnnouncementBar();
const {
navbar: {hideOnScroll},
hideableSidebar,
} = useThemeConfig();
const {isAnnouncementBarClosed} = useUserPreferencesContext();

const {
showResponsiveSidebar,
closeResponsiveSidebar,
toggleResponsiveSidebar,
} = useResponsiveSidebar();

return (
<div
Expand All @@ -249,69 +338,20 @@ function DocSidebar({
!isAnnouncementBarClosed && showAnnouncementBar,
},
)}>
<button
aria-label={
showResponsiveSidebar
? translate({
id: 'theme.docs.sidebar.responsiveCloseButtonLabel',
message: 'Close menu',
description:
'The ARIA label for close button of mobile doc sidebar',
})
: translate({
id: 'theme.docs.sidebar.responsiveOpenButtonLabel',
message: 'Open menu',
description:
'The ARIA label for open button of mobile doc sidebar',
})
}
aria-haspopup="true"
className="button button--secondary button--sm menu__button"
type="button"
onClick={() => {
setShowResponsiveSidebar(!showResponsiveSidebar);
}}>
{showResponsiveSidebar ? (
<span
className={clsx(
styles.sidebarMenuIcon,
styles.sidebarMenuCloseIcon,
)}>
&times;
</span>
) : (
<IconMenu
className={styles.sidebarMenuIcon}
height={MOBILE_TOGGLE_SIZE}
width={MOBILE_TOGGLE_SIZE}
/>
)}
</button>
<ul className="menu__list">{sidebarItems}</ul>
<ResponsiveSidebarButton
responsiveSidebarOpened={showResponsiveSidebar}
onClick={toggleResponsiveSidebar}
/>
<ul className="menu__list">
<DocSidebarItems
items={sidebar}
onItemClick={closeResponsiveSidebar}
collapsible={sidebarCollapsible}
activePath={path}
/>
</ul>
</div>
{hideableSidebar && (
<button
type="button"
title={translate({
id: 'theme.docs.sidebar.collapseButtonTitle',
message: 'Collapse sidebar',
description:
'The title attribute for collapse button of doc sidebar',
})}
aria-label={translate({
id: 'theme.docs.sidebar.collapseButtonAriaLabel',
message: 'Collapse sidebar',
description:
'The title attribute for collapse button of doc sidebar',
})}
className={clsx(
'button button--secondary button--outline',
styles.collapseSidebarButton,
)}
onClick={onCollapse}>
<IconArrow className={styles.collapseSidebarButtonIcon} />
</button>
)}
{hideableSidebar && <HideableSidebarButton onClick={onCollapse} />}
</div>
);
}
Expand Down