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.
|
* 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)}>
|
||||||
|
×
|
||||||
|
</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,
|
|
||||||
)}>
|
|
||||||
×
|
|
||||||
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue