refactor(v2): simplify and optimize sidebar (#4617)

* safe sidebar refactor

* simplify and optimize a bit the sidebar code
This commit is contained in:
Sébastien Lorber 2021-04-15 18:43:38 +02:00 committed by GitHub
parent cd47d8a815
commit 2d89d5c84f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree. * 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 clsx from 'clsx';
import {useThemeConfig, isSamePath} from '@docusaurus/theme-common'; import {useThemeConfig, isSamePath} from '@docusaurus/theme-common';
import useUserPreferencesContext from '@theme/hooks/useUserPreferencesContext'; import useUserPreferencesContext from '@theme/hooks/useUserPreferencesContext';
@ -44,6 +44,32 @@ const isActiveSidebarItem = (item, activePath) => {
return false; 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({ function DocSidebarItemCategory({
item, item,
onItemClick, onItemClick,
@ -104,8 +130,7 @@ function DocSidebarItemCategory({
<li <li
className={clsx('menu__list-item', { className={clsx('menu__list-item', {
'menu__list-item--collapsed': collapsed, 'menu__list-item--collapsed': collapsed,
})} })}>
key={label}>
<a <a
className={clsx('menu__link', { className={clsx('menu__link', {
'menu__link--sublist': collapsible, 'menu__link--sublist': collapsible,
@ -128,16 +153,13 @@ function DocSidebarItemCategory({
handleMenuListHeight(false); handleMenuListHeight(false);
} }
}}> }}>
{items.map((childItem) => ( <DocSidebarItems
<DocSidebarItem items={items}
tabIndex={collapsed ? '-1' : '0'} tabIndex={collapsed ? '-1' : '0'}
key={childItem.label} onItemClick={onItemClick}
item={childItem} collapsible={collapsible}
onItemClick={onItemClick} activePath={activePath}
collapsible={collapsible} />
activePath={activePath}
/>
))}
</ul> </ul>
</li> </li>
); );
@ -172,37 +194,24 @@ function DocSidebarItemLink({
); );
} }
function DocSidebarItem(props): JSX.Element { function useShowAnnouncementBar() {
switch (props.item.type) { const {isAnnouncementBarClosed} = useUserPreferencesContext();
case 'category': const [showAnnouncementBar, setShowAnnouncementBar] = useState(
return <DocSidebarItemCategory {...props} />; !isAnnouncementBarClosed,
case 'link': );
default: useScrollPosition(({scrollY}) => {
return <DocSidebarItemLink {...props} />; if (!isAnnouncementBarClosed) {
} setShowAnnouncementBar(scrollY === 0);
}
});
return showAnnouncementBar;
} }
function DocSidebar({ function useResponsiveSidebar() {
path,
sidebar,
sidebarCollapsible = true,
onCollapse,
isHidden,
}: Props): JSX.Element | null {
const [showResponsiveSidebar, setShowResponsiveSidebar] = useState(false); const [showResponsiveSidebar, setShowResponsiveSidebar] = useState(false);
const [showAnnouncementBar, setShowAnnouncementBar] = useState(true);
const {
navbar: {hideOnScroll},
hideableSidebar,
} = useThemeConfig();
const {isAnnouncementBarClosed} = useUserPreferencesContext();
useScrollPosition(({scrollY}) => {
setShowAnnouncementBar(scrollY === 0);
});
useLockBodyScroll(showResponsiveSidebar); useLockBodyScroll(showResponsiveSidebar);
const windowSize = useWindowSize();
const windowSize = useWindowSize();
useEffect(() => { useEffect(() => {
if (windowSize === windowSizes.desktop) { if (windowSize === windowSizes.desktop) {
setShowResponsiveSidebar(false); setShowResponsiveSidebar(false);
@ -216,19 +225,99 @@ function DocSidebar({
}, },
[setShowResponsiveSidebar], [setShowResponsiveSidebar],
); );
const sidebarItems = useMemo(
() => const toggleResponsiveSidebar = useCallback(() => {
sidebar.map((item) => ( setShowResponsiveSidebar(!showResponsiveSidebar);
<DocSidebarItem }, [setShowResponsiveSidebar]);
key={item.label}
item={item} return {
onItemClick={closeResponsiveSidebar} showResponsiveSidebar,
collapsible={sidebarCollapsible} closeResponsiveSidebar,
activePath={path} toggleResponsiveSidebar,
/> };
)), }
[sidebar, sidebarCollapsible, path, closeResponsiveSidebar],
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}
/>
)}
</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 ( return (
<div <div
@ -249,69 +338,20 @@ function DocSidebar({
!isAnnouncementBarClosed && showAnnouncementBar, !isAnnouncementBarClosed && showAnnouncementBar,
}, },
)}> )}>
<button <ResponsiveSidebarButton
aria-label={ responsiveSidebarOpened={showResponsiveSidebar}
showResponsiveSidebar onClick={toggleResponsiveSidebar}
? translate({ />
id: 'theme.docs.sidebar.responsiveCloseButtonLabel', <ul className="menu__list">
message: 'Close menu', <DocSidebarItems
description: items={sidebar}
'The ARIA label for close button of mobile doc sidebar', onItemClick={closeResponsiveSidebar}
}) collapsible={sidebarCollapsible}
: translate({ activePath={path}
id: 'theme.docs.sidebar.responsiveOpenButtonLabel', />
message: 'Open menu', </ul>
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>
</div> </div>
{hideableSidebar && ( {hideableSidebar && <HideableSidebarButton onClick={onCollapse} />}
<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>
)}
</div> </div>
); );
} }