Skip to article
Documentation

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>
  );
}