src/theme/NavbarItem/DropdownNavbarItem/index.tsx (130 lines of code) (raw):
import React, { useState, useRef, useEffect } from 'react';
import clsx from 'clsx';
import { isRegexpStringMatch, useCollapsible, Collapsible } from '@docusaurus/theme-common';
import { isSamePath, useLocalPathname } from '@docusaurus/theme-common/internal';
import NavbarNavLink from '@theme/NavbarItem/NavbarNavLink';
import NavbarItem, { type LinkLikeNavbarItemProps } from '@theme/NavbarItem';
import type { DesktopOrMobileNavBarItemProps, Props } from '@theme/NavbarItem/DropdownNavbarItem';
import "./style.scss";
function isItemActive(item: LinkLikeNavbarItemProps, localPathname: string): boolean {
if (isSamePath(item.to, localPathname)) {
return true;
}
if (isRegexpStringMatch(item.activeBaseRegex, localPathname)) {
return true;
}
if (item.activeBasePath && localPathname.startsWith(item.activeBasePath)) {
return true;
}
return false;
}
function containsActiveItems(items: readonly LinkLikeNavbarItemProps[], localPathname: string): boolean {
return items.some(item => isItemActive(item, localPathname));
}
function DropdownNavbarItemDesktop({ items, position, className, onClick, ...props }: DesktopOrMobileNavBarItemProps) {
const dropdownRef = useRef<HTMLDivElement>(null);
const [showDropdown, setShowDropdown] = useState(false);
useEffect(() => {
const handleClickOutside = (event: MouseEvent | TouchEvent | FocusEvent) => {
if (!dropdownRef.current || dropdownRef.current.contains(event.target as Node)) {
return;
}
setShowDropdown(false);
};
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('touchstart', handleClickOutside);
document.addEventListener('focusin', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('touchstart', handleClickOutside);
document.removeEventListener('focusin', handleClickOutside);
};
}, [dropdownRef]);
return (
<div
ref={dropdownRef}
className={clsx('navbar__item', 'dropdown', 'dropdown--hoverable', {
'dropdown--right': position === 'right',
'dropdown--show': showDropdown,
})}
>
<NavbarNavLink
aria-haspopup="true"
aria-expanded={showDropdown}
role="button"
// # hash permits to make the <a> tag focusable in case no link target
// See https://github.com/facebook/docusaurus/pull/6003
// There's probably a better solution though...
href={props.to ? undefined : '#'}
className={clsx('navbar__link', className)}
{...props}
onClick={props.to ? undefined : e => e.preventDefault()}
onKeyDown={e => {
if (e.key === 'Enter') {
e.preventDefault();
setShowDropdown(!showDropdown);
}
}}
>
{props.children ?? props.label}
</NavbarNavLink>
<ul className="dropdown__menu">
{items.map((childItemProps, i) => (
<NavbarItem isDropdownItem activeClassName="dropdown__link--active" {...childItemProps} key={i} />
))}
</ul>
</div>
);
}
function DropdownNavbarItemMobile({
items,
className,
position, // Need to destructure position from props so that it doesn't get passed on.
onClick,
...props
}: DesktopOrMobileNavBarItemProps) {
const localPathname = useLocalPathname();
const containsActive = containsActiveItems(items, localPathname);
const { collapsed, toggleCollapsed, setCollapsed } = useCollapsible({
initialState: () => !containsActive,
});
// Expand/collapse if any item active after a navigation
useEffect(() => {
if (containsActive) {
setCollapsed(!containsActive);
}
}, [localPathname, containsActive, setCollapsed]);
return (
<li
className={clsx('menu__list-item', {
'menu__list-item--collapsed': collapsed,
})}
>
<NavbarNavLink
role="button"
className={clsx(
'menu__link menu__link--sublist menu__link--sublist-caret',
className,
)}
{...props}
onClick={e => {
e.preventDefault();
toggleCollapsed();
}}
>
{props.children ?? props.label}
</NavbarNavLink>
<Collapsible lazy as="ul" className="menu__list" collapsed={collapsed}>
{items.map((childItemProps, i) => (
<NavbarItem
mobile
isDropdownItem
onClick={onClick}
activeClassName="menu__link--active"
{...childItemProps}
key={i}
/>
))}
</Collapsible>
</li>
);
}
export default function DropdownNavbarItem({ mobile = false, ...props }: Props): JSX.Element {
const Comp = mobile ? DropdownNavbarItemMobile : DropdownNavbarItemDesktop;
return <Comp {...props} />;
}