Files
fiberops-web/src/components/ui/action-menu.tsx
2026-04-13 09:36:37 +08:00

91 lines
2.8 KiB
TypeScript

'use client';
import { useState, useRef, useEffect, useCallback } from 'react';
import { ActionIcon } from './action-icon';
import type { IconName } from './action-icon';
export interface ActionMenuItem {
icon: IconName;
label: string;
onClick: () => void;
variant?: 'default' | 'danger';
disabled?: boolean;
}
interface ActionMenuProps {
items: ActionMenuItem[];
}
export function ActionMenu({ items }: ActionMenuProps) {
const [open, setOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const close = useCallback(() => setOpen(false), []);
useEffect(() => {
if (!open) return;
function handleClickOutside(e: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
close();
}
}
function handleEscape(e: KeyboardEvent) {
if (e.key === 'Escape') close();
}
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleEscape);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('keydown', handleEscape);
};
}, [open, close]);
return (
<div ref={menuRef} className="relative inline-block">
<ActionIcon
icon="more"
variant="ghost"
label="Actions"
onClick={(e) => {
e.stopPropagation();
setOpen((prev) => !prev);
}}
/>
{open && (
<div
role="menu"
className="absolute right-0 top-full mt-1 min-w-[160px] bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-lg py-1 z-50 animate-in fade-in"
>
{items.map((item, i) => (
<button
key={i}
role="menuitem"
disabled={item.disabled}
onClick={(e) => {
e.stopPropagation();
item.onClick();
close();
}}
className={`w-full flex items-center gap-2.5 px-3 py-2 text-sm transition-colors cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed ${
item.variant === 'danger'
? 'text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20'
: 'text-surface-700 dark:text-surface-300 hover:bg-surface-50 dark:hover:bg-surface-700'
}`}
>
<ActionIcon
icon={item.icon}
variant={item.variant === 'danger' ? 'danger' : 'ghost'}
label={item.label}
size="sm"
onClick={() => {}}
className="pointer-events-none"
/>
<span>{item.label}</span>
</button>
))}
</div>
)}
</div>
);
}