blob: 9722d521665a725a3fcc10f69ba14970d22aef91 [file] [log] [blame]
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, { useState, useCallback, useEffect, useRef } from 'react';
import clsx from 'clsx';
import { useThemeConfig, isSamePath } from '@docusaurus/theme-common';
import useUserPreferencesContext from '@theme/hooks/useUserPreferencesContext';
import useLockBodyScroll from '@theme/hooks/useLockBodyScroll';
import useWindowSize, { windowSizes } from '@theme/hooks/useWindowSize';
import useScrollPosition from '@theme/hooks/useScrollPosition';
import Link from '@docusaurus/Link';
import isInternalUrl from '@docusaurus/isInternalUrl';
import Logo from '@theme/Logo';
import IconArrow from '@theme/IconArrow';
import IconMenu from '@theme/IconMenu';
import styles from './styles.module.css';
import DocsVersionDropdownNavbarItem from "@theme/NavbarItem/DocsVersionDropdownNavbarItem"
const MOBILE_TOGGLE_SIZE = 24;
function usePrevious(value) {
const ref = useRef(value);
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
const isActiveSidebarItem = (item, activePath) => {
if (item.type === 'link') {
return isSamePath(item.href, activePath);
}
if (item.type === 'category') {
return item.items.some(subItem => isActiveSidebarItem(subItem, activePath));
}
return false;
};
function DocSidebarItemCategory({
item,
onItemClick,
collapsible,
activePath,
...props
}) {
const {
items,
label
} = item;
const isActive = isActiveSidebarItem(item, activePath);
const wasActive = usePrevious(isActive); // active categories are always initialized as expanded
// the default (item.collapsed) is only used for non-active categories
const [collapsed, setCollapsed] = useState(() => {
if (!collapsible) {
return false;
}
return isActive ? false : item.collapsed;
});
const menuListRef = useRef(null);
const [menuListHeight, setMenuListHeight] = useState(undefined);
const handleMenuListHeight = (calc = true) => {
setMenuListHeight(calc ? `${menuListRef.current?.scrollHeight}px` : undefined);
}; // If we navigate to a category, it should automatically expand itself
useEffect(() => {
const justBecameActive = isActive && !wasActive;
if (justBecameActive && collapsed) {
setCollapsed(false);
}
}, [isActive, wasActive, collapsed]);
const handleItemClick = useCallback(e => {
e.preventDefault();
if (!menuListHeight) {
handleMenuListHeight();
}
setTimeout(() => setCollapsed(state => !state), 100);
}, [menuListHeight]);
if (items.length === 0) {
return null;
}
return <li className={clsx('menu__list-item', {
'menu__list-item--collapsed': collapsed
})} key={label}>
<a className={clsx('menu__link', {
'menu__link--sublist': collapsible,
'menu__link--active': collapsible && isActive,
[styles.menuLinkText]: !collapsible
})} onClick={collapsible ? handleItemClick : undefined} href={collapsible ? '#!' : undefined} {...props}>
{label}
</a>
<ul className="menu__list" ref={menuListRef} style={{
height: menuListHeight
}} onTransitionEnd={() => {
if (!collapsed) {
handleMenuListHeight(false);
}
}}>
{items.map(childItem => <DocSidebarItem tabIndex={collapsed ? '-1' : '0'} key={childItem.label} item={childItem} onItemClick={onItemClick} collapsible={collapsible} activePath={activePath} />)}
</ul>
</li>;
}
function DocSidebarItemLink({
item,
onItemClick,
activePath,
collapsible: _collapsible,
...props
}) {
const {
href,
label
} = item;
const isActive = isActiveSidebarItem(item, activePath);
return <li className="menu__list-item" key={label}>
<Link className={clsx('menu__link', {
'menu__link--active': isActive
})} to={href} {...isInternalUrl(href) ? {
isNavLink: true,
exact: true,
onClick: onItemClick
} : {
target: '_blank',
rel: 'noreferrer noopener'
}} {...props}>
{label}
</Link>
</li>;
}
function DocSidebarItem(props) {
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,
docPluginId
}) {
const showAnnouncementBar = useShowAnnouncementBar();
const [showResponsiveSidebar, setShowResponsiveSidebar] = useState(false);
const {
navbar: {
hideOnScroll
},
hideableSidebar
} = useThemeConfig();
useLockBodyScroll(showResponsiveSidebar);
const windowSize = useWindowSize();
useEffect(() => {
if (windowSize === windowSizes.desktop) {
setShowResponsiveSidebar(false);
}
}, [windowSize]);
return <div className={clsx(styles.sidebar, {
[styles.sidebarWithHideableNavbar]: hideOnScroll,
[styles.sidebarHidden]: isHidden
})}>
{hideOnScroll && <Logo tabIndex={-1} className={styles.sidebarLogo} />}
<div className={clsx('menu', 'menu--responsive', 'thin-scrollbar', styles.menu, {
'menu--show': showResponsiveSidebar,
[styles.menuWithAnnouncementBar]: showAnnouncementBar
})}>
<button aria-label={showResponsiveSidebar ? 'Close Menu' : 'Open Menu'} 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">
{sidebar.map(item => <DocSidebarItem key={item.label} item={item} onItemClick={e => {
e.target.blur();
setShowResponsiveSidebar(false);
}} collapsible={sidebarCollapsible} activePath={path} />)}
</ul>
</div>
{hideableSidebar && <button type="button" title="Collapse sidebar" aria-label="Collapse sidebar" className={clsx('button button--secondary button--outline', styles.collapseSidebarButton)} onClick={onCollapse}>
<IconArrow className={styles.collapseSidebarButtonIcon} aria-label="Collapse sidebar" />
</button>}
</div>;
}
export default DocSidebar;