mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-15 17:22:35 +02:00
refactor(v2): simplify and optimize sidebar (#4617)
* safe sidebar refactor * simplify and optimize a bit the sidebar code
This commit is contained in:
parent
cd47d8a815
commit
2d89d5c84f
1 changed files with 153 additions and 113 deletions
|
@ -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';
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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 useShowAnnouncementBar() {
|
||||
const {isAnnouncementBarClosed} = useUserPreferencesContext();
|
||||
const [showAnnouncementBar, setShowAnnouncementBar] = useState(
|
||||
!isAnnouncementBarClosed,
|
||||
);
|
||||
useScrollPosition(({scrollY}) => {
|
||||
if (!isAnnouncementBarClosed) {
|
||||
setShowAnnouncementBar(scrollY === 0);
|
||||
}
|
||||
});
|
||||
return showAnnouncementBar;
|
||||
}
|
||||
|
||||
function DocSidebar({
|
||||
path,
|
||||
sidebar,
|
||||
sidebarCollapsible = true,
|
||||
onCollapse,
|
||||
isHidden,
|
||||
}: Props): JSX.Element | null {
|
||||
function useResponsiveSidebar() {
|
||||
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);
|
||||
const windowSize = useWindowSize();
|
||||
|
||||
const windowSize = useWindowSize();
|
||||
useEffect(() => {
|
||||
if (windowSize === windowSizes.desktop) {
|
||||
setShowResponsiveSidebar(false);
|
||||
|
@ -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}
|
||||
/>
|
||||
)),
|
||||
[sidebar, sidebarCollapsible, path, closeResponsiveSidebar],
|
||||
|
||||
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)}>
|
||||
×
|
||||
</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 (
|
||||
<div
|
||||
|
@ -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,
|
||||
)}>
|
||||
×
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue