| /** |
| * 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)}> |
| × |
| </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; |