109 lines
3.0 KiB
TypeScript
109 lines
3.0 KiB
TypeScript
import React, { useState, useRef, useEffect } from 'react';
|
|
|
|
interface DropdownMenuProps {
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
export function DropdownMenu({ children }: DropdownMenuProps) {
|
|
return <>{children}</>;
|
|
}
|
|
|
|
interface DropdownMenuTriggerProps {
|
|
children: React.ReactNode;
|
|
asChild?: boolean;
|
|
}
|
|
|
|
export function DropdownMenuTrigger({ children }: DropdownMenuTriggerProps) {
|
|
return <>{children}</>;
|
|
}
|
|
|
|
interface DropdownMenuContentProps {
|
|
children: React.ReactNode;
|
|
align?: 'start' | 'center' | 'end';
|
|
}
|
|
|
|
export function DropdownMenuContent({ children, align = 'end' }: DropdownMenuContentProps) {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const triggerRef = useRef<HTMLDivElement>(null);
|
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
const trigger = triggerRef.current;
|
|
if (!trigger) return;
|
|
|
|
const handleClick = () => setIsOpen(!isOpen);
|
|
trigger.addEventListener('click', handleClick);
|
|
|
|
return () => {
|
|
trigger.removeEventListener('click', handleClick);
|
|
};
|
|
}, [isOpen]);
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (
|
|
contentRef.current &&
|
|
!contentRef.current.contains(event.target as Node) &&
|
|
triggerRef.current &&
|
|
!triggerRef.current.contains(event.target as Node)
|
|
) {
|
|
setIsOpen(false);
|
|
}
|
|
};
|
|
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => {
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<div className="relative">
|
|
<div ref={triggerRef}>
|
|
{React.Children.map(children, (child) => {
|
|
if (React.isValidElement(child)) {
|
|
return React.cloneElement(child, {
|
|
...child.props,
|
|
children: (
|
|
<>
|
|
{child.props.children}
|
|
{isOpen && (
|
|
<div
|
|
ref={contentRef}
|
|
className={`absolute z-50 mt-2 min-w-[8rem] overflow-hidden rounded-md border bg-white p-1 shadow-md ${
|
|
align === 'start' ? 'left-0' : align === 'center' ? 'left-1/2 transform -translate-x-1/2' : 'right-0'
|
|
}`}
|
|
>
|
|
{children}
|
|
</div>
|
|
)}
|
|
</>
|
|
)
|
|
});
|
|
}
|
|
return child;
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface DropdownMenuItemProps {
|
|
children: React.ReactNode;
|
|
onClick?: () => void;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
export function DropdownMenuItem({ children, onClick, disabled = false }: DropdownMenuItemProps) {
|
|
return (
|
|
<button
|
|
className={`relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-gray-100 focus:bg-gray-100 disabled:pointer-events-none disabled:opacity-50 ${
|
|
disabled ? 'cursor-not-allowed opacity-50' : ''
|
|
}`}
|
|
onClick={onClick}
|
|
disabled={disabled}
|
|
>
|
|
{children}
|
|
</button>
|
|
);
|
|
}
|