Skip to content

Commit

Permalink
refactor(v2): simplify and optimize sidebar (#4617)
Browse files Browse the repository at this point in the history
* safe sidebar refactor

* simplify and optimize a bit the sidebar code
  • Loading branch information
slorber authored Apr 15, 2021
1 parent cd47d8a commit 2d89d5c
Showing 1 changed file with 151 additions and 111 deletions.
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

0 comments on commit 2d89d5c

Please sign in to comment.