src/components/header-popover.tsx
Linked repository file from this build.
"use client";
import { useEffect, useRef, useState, type ReactNode } from "react";
export function HeaderPopover({
label,
trigger,
children,
}: {
label: string;
trigger: ReactNode;
children: ReactNode;
}) {
const [open, setOpen] = useState(false);
const root = useRef<HTMLDivElement>(null);
const button = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (!open) return;
const outside = (event: PointerEvent) => {
if (!root.current?.contains(event.target as Node)) setOpen(false);
};
const escape = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setOpen(false);
button.current?.focus();
}
};
document.addEventListener("pointerdown", outside);
document.addEventListener("keydown", escape);
return () => {
document.removeEventListener("pointerdown", outside);
document.removeEventListener("keydown", escape);
};
}, [open]);
return (
<div ref={root} className="header-popover relative" data-open={open || undefined}>
<button
ref={button}
type="button"
aria-label={label}
aria-expanded={open}
data-tip={open ? undefined : label}
onClick={() => setOpen(!open)}
className="app-nav-link relative"
>
{trigger}
</button>
{open && (
<div
aria-label={label}
className="header-popover-panel absolute right-0 top-full z-50 mt-2 max-h-[75dvh] w-[22rem] max-w-[calc(100vw-2rem)] overflow-auto rounded-xl border bg-popover p-4 text-popover-foreground shadow-xl"
>
{children}
</div>
)}
</div>
);
}