diff --git a/src/css/custom.css b/src/css/custom.css index ca9c1f5..d73c340 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -32,4 +32,7 @@ :root { --doc-sidebar-width: 16.66vw !important; --doc-sidebar-max-width: 320px; + + --ifm-navbar-item-padding-vertical: 8px; + --ifm-navbar-item-padding-horizontal: 12px; } diff --git a/src/theme/NavbarItem/DropdownNavbarItem.scss b/src/theme/NavbarItem/DropdownNavbarItem.scss new file mode 100644 index 0000000..693c37a --- /dev/null +++ b/src/theme/NavbarItem/DropdownNavbarItem.scss @@ -0,0 +1,24 @@ +.navbar__item.dropdown { + background-color: #eeeef1; + border-radius: 0.5rem; + + & > .navbar__link { + &::after { + display: none; + } + + & + svg { + margin-left: 0.75rem; + } + } + + ul { + margin-top: 5px; + } +} + +[data-theme='dark'] { + .navbar__item.dropdown { + background-color: var(--ifm-background-color); + } +} diff --git a/src/theme/NavbarItem/DropdownNavbarItem.tsx b/src/theme/NavbarItem/DropdownNavbarItem.tsx new file mode 100644 index 0000000..1eb484a --- /dev/null +++ b/src/theme/NavbarItem/DropdownNavbarItem.tsx @@ -0,0 +1,190 @@ +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, { LinkLikeNavbarItemProps } from '@theme/NavbarItem' +import type { + DesktopOrMobileNavBarItemProps, + Props, +} from '@theme/NavbarItem/DropdownNavbarItem' +import './DropdownNavbarItem.scss' +import DropdownIcon from '@site/static/icons/dropdown.svg' + +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(null) + const [showDropdown, setShowDropdown] = useState(false) + + useEffect(() => { + const handleClickOutside = (event: MouseEvent | TouchEvent) => { + if ( + !dropdownRef.current || + dropdownRef.current.contains(event.target as Node) + ) { + return + } + setShowDropdown(false) + } + + document.addEventListener('mousedown', handleClickOutside) + document.addEventListener('touchstart', handleClickOutside) + + return () => { + document.removeEventListener('mousedown', handleClickOutside) + document.removeEventListener('touchstart', handleClickOutside) + } + }, [dropdownRef]) + + return ( +
+ e.preventDefault()} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() + setShowDropdown(!showDropdown) + } + }} + > + {props.children ?? props.label} + + +
    + {items.map((childItemProps, i) => ( + { + if (i === items.length - 1 && e.key === 'Tab') { + e.preventDefault() + setShowDropdown(false) + const nextNavbarItem = dropdownRef.current!.nextElementSibling + if (nextNavbarItem) { + const targetItem = + nextNavbarItem instanceof HTMLAnchorElement + ? nextNavbarItem + : // Next item is another dropdown; focus on the inner + // anchor element instead so there's outline + nextNavbarItem.querySelector('a')! + targetItem.focus() + } + } + }} + activeClassName="dropdown__link--active" + {...childItemProps} + key={i} + /> + ))} +
+
+ ) +} + +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 ( +
  • + { + e.preventDefault() + toggleCollapsed() + }} + > + {props.children ?? props.label} + + + {items.map((childItemProps, i) => ( + + ))} + +
  • + ) +} + +export default function DropdownNavbarItem({ + mobile = false, + ...props +}: Props): JSX.Element { + const Comp = mobile ? DropdownNavbarItemMobile : DropdownNavbarItemDesktop + return +} diff --git a/src/theme/NavbarItem/LocaleDropdownNavbarItem/index.tsx b/src/theme/NavbarItem/LocaleDropdownNavbarItem/index.tsx new file mode 100644 index 0000000..7d1229f --- /dev/null +++ b/src/theme/NavbarItem/LocaleDropdownNavbarItem/index.tsx @@ -0,0 +1,66 @@ +import React from 'react' +import useDocusaurusContext from '@docusaurus/useDocusaurusContext' +import { useAlternatePageUtils } from '@docusaurus/theme-common/internal' +import { translate } from '@docusaurus/Translate' +import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem' +import IconLanguage from '@theme/Icon/Language' +import type { LinkLikeNavbarItemProps } from '@theme/NavbarItem' +import type { Props } from '@theme/NavbarItem/LocaleDropdownNavbarItem' + +import styles from './styles.module.css' + +export default function LocaleDropdownNavbarItem({ + mobile, + dropdownItemsBefore, + dropdownItemsAfter, + ...props +}: Props): JSX.Element { + const { + i18n: { currentLocale, locales, localeConfigs }, + } = useDocusaurusContext() + const alternatePageUtils = useAlternatePageUtils() + + const localeItems = locales.map((locale): LinkLikeNavbarItemProps => { + const to = `pathname://${alternatePageUtils.createUrl({ + locale, + fullyQualified: false, + })}` + return { + label: localeConfigs[locale]!.label, + lang: localeConfigs[locale]!.htmlLang, + to, + target: '_self', + autoAddBaseUrl: false, + className: + // eslint-disable-next-line no-nested-ternary + locale === currentLocale + ? // Similar idea as DefaultNavbarItem: select the right Infima active + // class name. This cannot be substituted with isActive, because the + // target URLs contain `pathname://` and therefore are not NavLinks! + mobile + ? 'menu__link--active' + : 'dropdown__link--active' + : '', + } + }) + + const items = [...dropdownItemsBefore, ...localeItems, ...dropdownItemsAfter] + + // Mobile is handled a bit differently + const dropdownLabel = mobile + ? translate({ + message: 'Languages', + id: 'theme.navbar.mobileLanguageDropdown.label', + description: 'The label for the mobile language switcher dropdown', + }) + : localeConfigs[currentLocale]!.label + + return ( + {dropdownLabel}} + items={items} + /> + ) +} diff --git a/src/theme/NavbarItem/LocaleDropdownNavbarItem/styles.module.css b/src/theme/NavbarItem/LocaleDropdownNavbarItem/styles.module.css new file mode 100644 index 0000000..8804a08 --- /dev/null +++ b/src/theme/NavbarItem/LocaleDropdownNavbarItem/styles.module.css @@ -0,0 +1,4 @@ +.iconLanguage { + vertical-align: text-bottom; + margin-right: 5px; +} diff --git a/static/icons/dropdown.svg b/static/icons/dropdown.svg new file mode 100644 index 0000000..f583120 --- /dev/null +++ b/static/icons/dropdown.svg @@ -0,0 +1,3 @@ + + +