Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | 107x 107x 107x 101x 3x 1x 12x 12x 6x 6x 6x 107x 13x 27x 6x | import { useState, useRef, useEffect, type ReactNode } from "react";
export interface DropdownMenuItem {
label: string;
icon?: ReactNode;
onClick: () => void;
variant?: "default" | "destructive";
disabled?: boolean;
}
interface DropdownMenuProps {
trigger: ReactNode;
items: DropdownMenuItem[];
}
export function DropdownMenu({ trigger, items }: DropdownMenuProps) {
const [isOpen, setIsOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!isOpen) return;
function handleMouseDown(event: MouseEvent) {
if (
containerRef.current &&
!containerRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
}
document.addEventListener("mousedown", handleMouseDown);
return () => document.removeEventListener("mousedown", handleMouseDown);
}, [isOpen]);
function handleItemClick(item: DropdownMenuItem) {
Iif (item.disabled) return;
setIsOpen(false);
item.onClick();
}
return (
<div ref={containerRef} className="relative">
{/* Trigger */}
<button
type="button"
aria-haspopup="menu"
aria-expanded={isOpen}
onClick={() => setIsOpen((prev) => !prev)}
className="cursor-pointer"
>
{trigger}
</button>
{/* Mobile backdrop */}
{isOpen && (
<div
className="fixed inset-0 z-40 sm:hidden"
aria-hidden="true"
onClick={() => setIsOpen(false)}
/>
)}
{/* Dropdown panel */}
{isOpen && (
<div
role="menu"
className="absolute left-0 top-full mt-1 z-50 min-w-[160px] rounded-lg border border-border-default bg-background-elevated shadow-lg transition-opacity duration-150"
>
{items.map((item) => (
<button
key={item.label}
type="button"
role="menuitem"
disabled={item.disabled}
onClick={() => handleItemClick(item)}
className={[
"flex w-full items-center gap-2 px-3 py-2 text-sm first:rounded-t-lg last:rounded-b-lg",
"disabled:pointer-events-none disabled:opacity-50",
item.variant === "destructive"
? "text-error hover:bg-error-light"
: "text-foreground-default hover:bg-background-muted",
].join(" ")}
>
{item.icon && (
<span className="shrink-0 [&_svg]:h-4 [&_svg]:w-4">
{item.icon}
</span>
)}
{item.label}
</button>
))}
</div>
)}
</div>
);
}
|