diff --git a/components/social/comment/comment-menu.tsx b/components/social/comment/comment-menu.tsx index 0436a4c5..37ef252d 100644 --- a/components/social/comment/comment-menu.tsx +++ b/components/social/comment/comment-menu.tsx @@ -2,12 +2,14 @@ /** * CommentMenu — the kebab (⋮) overflow menu on a comment: Pin / Report / - * Delete (each shown only when permitted). Rendered in a portal with a - * transparent full-screen backdrop that swallows the dismiss click, so it - * can't reach (and accidentally close) the surrounding vaul comments drawer. + * Delete (each shown only when permitted). The menu is portaled (fixed + * position) so it isn't clipped by the comments drawer's overflow, and it + * dismisses via a deferred document `pointerdown`-capture listener + Escape + + * close-on-scroll; the trigger itself toggles. Closing only ever flips this + * menu's own state, so it never closes the surrounding vaul drawer. */ -import { useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { Flag, MoreVertical, Pin, PinOff, Trash2 } from "lucide-react"; import { cn } from "@/lib/utils"; @@ -32,11 +34,45 @@ export function CommentMenu({ const [open, setOpen] = useState(false); const [pos, setPos] = useState<{ top: number; right: number } | null>(null); const btnRef = useRef(null); + const menuRef = useRef(null); + + // Dismiss on any outside pointer-down, Escape, or scroll. Deferred by a tick + // so the same click that opened the menu doesn't immediately close it. + // Capture phase so it fires regardless of where the target lives in the tree. + useEffect(() => { + if (!open) return; + function onDown(e: PointerEvent) { + const t = e.target as Node; + if (menuRef.current?.contains(t) || btnRef.current?.contains(t)) return; + setOpen(false); + } + function onKey(e: KeyboardEvent) { + if (e.key === "Escape") setOpen(false); + } + function onScroll() { + setOpen(false); + } + const id = window.setTimeout(() => { + document.addEventListener("pointerdown", onDown, true); + document.addEventListener("keydown", onKey); + window.addEventListener("scroll", onScroll, true); + }, 0); + return () => { + window.clearTimeout(id); + document.removeEventListener("pointerdown", onDown, true); + document.removeEventListener("keydown", onKey); + window.removeEventListener("scroll", onScroll, true); + }; + }, [open]); // Nothing actionable → no kebab at all. if (!canPin && !canDelete && !canReport) return null; - function openMenu() { + function toggle() { + if (open) { + setOpen(false); + return; + } const r = btnRef.current?.getBoundingClientRect(); if (r) setPos({ top: r.bottom + 4, right: window.innerWidth - r.right }); setOpen(true); @@ -47,7 +83,7 @@ export function CommentMenu({