diff --git a/packages/docusaurus-theme-classic/src/theme-classic.d.ts b/packages/docusaurus-theme-classic/src/theme-classic.d.ts index 323ef6effb..a815adb2fc 100644 --- a/packages/docusaurus-theme-classic/src/theme-classic.d.ts +++ b/packages/docusaurus-theme-classic/src/theme-classic.d.ts @@ -1181,19 +1181,40 @@ declare module '@theme/NavbarItem/DefaultNavbarItem' { import type {ReactNode} from 'react'; import type {Props as NavbarNavLinkProps} from '@theme/NavbarItem/NavbarNavLink'; - export type DesktopOrMobileNavBarItemProps = NavbarNavLinkProps & { + export type DefaultNavbarItemProps = NavbarNavLinkProps & { readonly isDropdownItem?: boolean; readonly className?: string; readonly position?: 'left' | 'right'; }; - export interface Props extends DesktopOrMobileNavBarItemProps { + // TODO Docusaurus v4, remove old type name + export type DesktopOrMobileNavBarItemProps = DefaultNavbarItemProps; + + export interface Props extends DefaultNavbarItemProps { readonly mobile?: boolean; } export default function DefaultNavbarItem(props: Props): ReactNode; } +declare module '@theme/NavbarItem/DefaultNavbarItem/Mobile' { + import type {ReactNode} from 'react'; + import type {DefaultNavbarItemProps} from '@theme/NavbarItem/DefaultNavbarItem'; + + export interface Props extends DefaultNavbarItemProps {} + + export default function DefaultNavbarItemMobile(props: Props): ReactNode; +} + +declare module '@theme/NavbarItem/DefaultNavbarItem/Desktop' { + import type {ReactNode} from 'react'; + import type {DefaultNavbarItemProps} from '@theme/NavbarItem/DefaultNavbarItem'; + + export interface Props extends DefaultNavbarItemProps {} + + export default function DefaultNavbarItemDesktop(props: Props): ReactNode; +} + declare module '@theme/NavbarItem/NavbarNavLink' { import type {ReactNode} from 'react'; import type {Props as LinkProps} from '@docusaurus/Link'; @@ -1216,19 +1237,40 @@ declare module '@theme/NavbarItem/DropdownNavbarItem' { import type {Props as NavbarNavLinkProps} from '@theme/NavbarItem/NavbarNavLink'; import type {LinkLikeNavbarItemProps} from '@theme/NavbarItem'; - export type DesktopOrMobileNavBarItemProps = NavbarNavLinkProps & { + export type DropdownNavbarItemProps = NavbarNavLinkProps & { readonly position?: 'left' | 'right'; readonly items: readonly LinkLikeNavbarItemProps[]; readonly className?: string; }; - export interface Props extends DesktopOrMobileNavBarItemProps { + // TODO Docusaurus v4, remove old type name + export type DesktopOrMobileNavBarItemProps = DropdownNavbarItemProps; + + export interface Props extends DropdownNavbarItemProps { readonly mobile?: boolean; } export default function DropdownNavbarItem(props: Props): ReactNode; } +declare module '@theme/NavbarItem/DropdownNavbarItem/Mobile' { + import type {ReactNode} from 'react'; + import type {DropdownNavbarItemProps} from '@theme/NavbarItem/DropdownNavbarItem'; + + export interface Props extends DropdownNavbarItemProps {} + + export default function DropdownNavbarItemMobile(props: Props): ReactNode; +} + +declare module '@theme/NavbarItem/DropdownNavbarItem/Desktop' { + import type {ReactNode} from 'react'; + import type {DropdownNavbarItemProps} from '@theme/NavbarItem/DropdownNavbarItem'; + + export interface Props extends DropdownNavbarItemProps {} + + export default function DropdownNavbarItemDesktop(props: Props): ReactNode; +} + declare module '@theme/NavbarItem/SearchNavbarItem' { import type {ReactNode} from 'react'; diff --git a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DefaultNavbarItem.tsx b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DefaultNavbarItem.tsx deleted file mode 100644 index 5db484cf14..0000000000 --- a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DefaultNavbarItem.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/** - * 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, {type ReactNode} from 'react'; -import clsx from 'clsx'; -import NavbarNavLink from '@theme/NavbarItem/NavbarNavLink'; -import type { - DesktopOrMobileNavBarItemProps, - Props, -} from '@theme/NavbarItem/DefaultNavbarItem'; - -function DefaultNavbarItemDesktop({ - className, - isDropdownItem = false, - ...props -}: DesktopOrMobileNavBarItemProps) { - const element = ( - - ); - - if (isDropdownItem) { - return
  • {element}
  • ; - } - - return element; -} - -function DefaultNavbarItemMobile({ - className, - isDropdownItem, - ...props -}: DesktopOrMobileNavBarItemProps) { - return ( -
  • - -
  • - ); -} - -export default function DefaultNavbarItem({ - mobile = false, - position, // Need to destructure position from props so that it doesn't get passed on. - ...props -}: Props): ReactNode { - const Comp = mobile ? DefaultNavbarItemMobile : DefaultNavbarItemDesktop; - return ( - - ); -} diff --git a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DefaultNavbarItem/Desktop/index.tsx b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DefaultNavbarItem/Desktop/index.tsx new file mode 100644 index 0000000000..de9ab826e0 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DefaultNavbarItem/Desktop/index.tsx @@ -0,0 +1,34 @@ +/** + * 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, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import NavbarNavLink from '@theme/NavbarItem/NavbarNavLink'; +import type {Props} from '@theme/NavbarItem/DefaultNavbarItem/Desktop'; + +export default function DefaultNavbarItemDesktop({ + className, + isDropdownItem = false, + ...props +}: Props): ReactNode { + const element = ( + + ); + + if (isDropdownItem) { + return
  • {element}
  • ; + } + + return element; +} diff --git a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DefaultNavbarItem/Mobile/index.tsx b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DefaultNavbarItem/Mobile/index.tsx new file mode 100644 index 0000000000..5d37c0487d --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DefaultNavbarItem/Mobile/index.tsx @@ -0,0 +1,23 @@ +/** + * 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, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import NavbarNavLink from '@theme/NavbarItem/NavbarNavLink'; +import type {Props} from '@theme/NavbarItem/DefaultNavbarItem/Mobile'; + +export default function DefaultNavbarItemMobile({ + className, + isDropdownItem, + ...props +}: Props): ReactNode { + return ( +
  • + +
  • + ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DefaultNavbarItem/index.tsx b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DefaultNavbarItem/index.tsx new file mode 100644 index 0000000000..ff8992bf8d --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DefaultNavbarItem/index.tsx @@ -0,0 +1,28 @@ +/** + * 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, {type ReactNode} from 'react'; +import DefaultNavbarItemMobile from '@theme/NavbarItem/DefaultNavbarItem/Mobile'; +import DefaultNavbarItemDesktop from '@theme/NavbarItem/DefaultNavbarItem/Desktop'; +import type {Props} from '@theme/NavbarItem/DefaultNavbarItem'; + +export default function DefaultNavbarItem({ + mobile = false, + position, // Need to destructure position from props so that it doesn't get passed on. + ...props +}: Props): ReactNode { + const Comp = mobile ? DefaultNavbarItemMobile : DefaultNavbarItemDesktop; + return ( + + ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DropdownNavbarItem/Desktop/index.tsx b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DropdownNavbarItem/Desktop/index.tsx new file mode 100644 index 0000000000..0ecc3b9fc6 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DropdownNavbarItem/Desktop/index.tsx @@ -0,0 +1,86 @@ +/** + * 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, useRef, useEffect, type ReactNode} from 'react'; +import clsx from 'clsx'; +import NavbarNavLink from '@theme/NavbarItem/NavbarNavLink'; +import NavbarItem from '@theme/NavbarItem'; +import type {Props} from '@theme/NavbarItem/DropdownNavbarItem/Desktop'; + +export default function DropdownNavbarItemDesktop({ + items, + position, + className, + onClick, + ...props +}: Props): ReactNode { + const dropdownRef = useRef(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 ( +
    + 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} + +
      + {items.map((childItemProps, i) => ( + + ))} +
    +
    + ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DropdownNavbarItem/Mobile/index.tsx b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DropdownNavbarItem/Mobile/index.tsx new file mode 100644 index 0000000000..ef59dcdf82 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DropdownNavbarItem/Mobile/index.tsx @@ -0,0 +1,166 @@ +/** + * 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, {useEffect, type ReactNode, type ComponentProps} from 'react'; +import clsx from 'clsx'; +import { + isRegexpStringMatch, + useCollapsible, + Collapsible, +} from '@docusaurus/theme-common'; +import {isSamePath, useLocalPathname} from '@docusaurus/theme-common/internal'; +import {translate} from '@docusaurus/Translate'; +import NavbarNavLink from '@theme/NavbarItem/NavbarNavLink'; +import NavbarItem, {type LinkLikeNavbarItemProps} from '@theme/NavbarItem'; +import type {Props} from '@theme/NavbarItem/DropdownNavbarItem/Mobile'; +import styles from './styles.module.css'; + +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 CollapseButton({ + collapsed, + onClick, +}: { + collapsed: boolean; + onClick: ComponentProps<'button'>['onClick']; +}) { + return ( +