"use client"; /** * CommentMenu — the kebab (⋮) overflow menu on a comment: Pin / Report / * 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 { useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { Flag, MoreVertical, Pin, PinOff, Trash2 } from "lucide-react"; import { cn } from "@/lib/utils"; export function CommentMenu({ canPin, isPinned, canDelete, canReport, onPin, onDelete, onReport, }: { canPin: boolean; isPinned: boolean; canDelete: boolean; canReport: boolean; onPin: () => void; onDelete: () => void; onReport: () => void; }) { 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 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); } return ( <> {open && pos && typeof document !== "undefined" ? createPortal(
{canPin ? ( : } onClick={() => { setOpen(false); onPin(); }} > {isPinned ? "Unpin" : "Pin"} ) : null} {canReport ? ( } onClick={() => { setOpen(false); onReport(); }} > Report ) : null} {canDelete ? ( } danger onClick={() => { setOpen(false); onDelete(); }} > Delete ) : null}
, document.body, ) : null} ); } function MenuItem({ icon, children, onClick, danger = false, }: { icon: React.ReactNode; children: React.ReactNode; onClick: () => void; danger?: boolean; }) { return ( ); }