113 lines
3.1 KiB
TypeScript
113 lines
3.1 KiB
TypeScript
"use client";
|
|
|
|
import type { FC, MouseEvent } from "react";
|
|
import { useRef } from "react";
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import { useHeightVar } from "@/hooks/useHeightVar";
|
|
import { useIsDesktop } from "@/hooks/useIsDesktop";
|
|
|
|
import styles from "./NavbarSubMenu.module.css";
|
|
|
|
type SubMenuItem = {
|
|
label: string;
|
|
to: string;
|
|
};
|
|
|
|
type Props = {
|
|
id: string;
|
|
open: boolean;
|
|
noTransition: boolean;
|
|
instantSwitch: boolean;
|
|
setOpenSubmenuId: (id: string | null) => void;
|
|
closeInstantNoAnim?: () => void;
|
|
label: string;
|
|
items: SubMenuItem[];
|
|
};
|
|
|
|
export const NavbarSubMenu: FC<Props> = ({
|
|
id,
|
|
open,
|
|
noTransition,
|
|
instantSwitch,
|
|
setOpenSubmenuId,
|
|
closeInstantNoAnim,
|
|
label,
|
|
items,
|
|
}) => {
|
|
const pathname = usePathname();
|
|
const subMenuRef = useRef<HTMLUListElement | null>(null);
|
|
const subMenuInnerRef = useRef<HTMLLIElement | null>(null);
|
|
const isDesktop = useIsDesktop();
|
|
|
|
const openMenu = () => setOpenSubmenuId(id);
|
|
const closeMenu = () => setOpenSubmenuId(null);
|
|
const toggleMenu = () => setOpenSubmenuId(open ? null : id);
|
|
|
|
// Measure the inner (unclipped) wrapper, not the ul.nav-submenu that
|
|
// gets its max-height animated — observing the animated element itself
|
|
// causes the ResizeObserver to refire on every step of the transition,
|
|
// which makes the animation stutter in Firefox.
|
|
useHeightVar(subMenuInnerRef, "--submenu-h", "scroll", open, subMenuRef);
|
|
|
|
const handleSubmenuClick = (e: MouseEvent<HTMLUListElement>) => {
|
|
const target = e.target as HTMLElement;
|
|
if (target.closest("a")) {
|
|
closeInstantNoAnim?.();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<li
|
|
className={styles["nav-item-with-sub"]}
|
|
onMouseEnter={isDesktop ? openMenu : undefined}
|
|
onMouseLeave={isDesktop ? closeMenu : undefined}
|
|
>
|
|
<button
|
|
type="button"
|
|
className={styles["nav-sub-toggle"]}
|
|
onClick={toggleMenu}
|
|
aria-expanded={open}
|
|
aria-controls={`submenu-${id}`}
|
|
>
|
|
{label}
|
|
</button>
|
|
|
|
<ul
|
|
ref={subMenuRef}
|
|
id={`submenu-${id}`}
|
|
inert={!open}
|
|
className={[
|
|
styles["nav-submenu"],
|
|
open ? styles.open : "",
|
|
noTransition || instantSwitch ? styles.noTransition : "",
|
|
].join(" ")}
|
|
onClick={handleSubmenuClick}
|
|
>
|
|
<li ref={subMenuInnerRef} className={styles["nav-submenu-inner"]}>
|
|
<ul className={styles["nav-submenu-list"]}>
|
|
{items.map((item) => {
|
|
const isActive =
|
|
item.to === "/"
|
|
? pathname === "/"
|
|
: pathname === item.to || pathname.startsWith(`${item.to}/`);
|
|
|
|
return (
|
|
<li key={item.to}>
|
|
<Link
|
|
href={item.to}
|
|
aria-current={isActive ? "page" : undefined}
|
|
onClick={closeInstantNoAnim}
|
|
>
|
|
{item.label}
|
|
</Link>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
);
|
|
};
|