71 lines
2.1 KiB
TypeScript
71 lines
2.1 KiB
TypeScript
import React, { useState, useEffect, useRef, ReactNode } from 'react';
|
|
|
|
interface DropdownItem {
|
|
label: string;
|
|
icon?: ReactNode;
|
|
onClick: () => void;
|
|
danger?: boolean;
|
|
}
|
|
|
|
interface DropdownMenuProps {
|
|
trigger: ReactNode;
|
|
items: DropdownItem[];
|
|
align?: 'left' | 'right';
|
|
}
|
|
|
|
export default function DropdownMenu({ trigger, items, align = 'left' }: DropdownMenuProps) {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (!isOpen) return;
|
|
|
|
function handleClickOutside(event: MouseEvent) {
|
|
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
setIsOpen(false);
|
|
}
|
|
}
|
|
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}, [isOpen]);
|
|
|
|
const alignmentClass = align === 'right' ? 'right-0' : 'left-0';
|
|
|
|
return (
|
|
<div ref={containerRef} className="relative inline-block text-left">
|
|
<div
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
className="cursor-pointer"
|
|
>
|
|
{trigger}
|
|
</div>
|
|
|
|
{isOpen && (
|
|
<div
|
|
className={`absolute z-50 mt-2 w-48 origin-top-left bg-white border border-gray-200 rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none ${alignmentClass}`}
|
|
>
|
|
<div className="py-1">
|
|
{items.map((item, index) => (
|
|
<button
|
|
key={index}
|
|
onClick={() => {
|
|
item.onClick();
|
|
setIsOpen(false);
|
|
}}
|
|
className={`flex items-center w-full px-4 py-2 text-sm transition-colors duration-150 ${
|
|
item.danger
|
|
? 'text-red-600 hover:bg-red-50'
|
|
: 'text-gray-700 hover:bg-gray-100'
|
|
}`}
|
|
>
|
|
{item.icon && <span className="mr-2">{item.icon}</span>}
|
|
{item.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
} |