77 lines
2.1 KiB
TypeScript
77 lines
2.1 KiB
TypeScript
"use client";
|
|
import React, { useEffect, useRef } from "react";
|
|
|
|
export interface MenuItem {
|
|
label: string;
|
|
icon?: string | React.ReactNode;
|
|
danger?: boolean;
|
|
divider?: boolean;
|
|
onClick?: () => void;
|
|
}
|
|
|
|
interface ContextMenuProps {
|
|
x: number;
|
|
y: number;
|
|
items: MenuItem[];
|
|
onClose: () => void;
|
|
}
|
|
|
|
export function ContextMenu({ x, y, items, onClose }: ContextMenuProps) {
|
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = (e: MouseEvent) => {
|
|
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
|
onClose();
|
|
}
|
|
};
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape") onClose();
|
|
};
|
|
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
document.addEventListener("keydown", handleKeyDown);
|
|
return () => {
|
|
document.removeEventListener("mousedown", handleClickOutside);
|
|
document.removeEventListener("keydown", handleKeyDown);
|
|
};
|
|
}, [onClose]);
|
|
|
|
// Adjust coordinates so menu stays inside viewport
|
|
const adjustedX = typeof window !== "undefined" ? Math.min(x, window.innerWidth - 180) : x;
|
|
const adjustedY = typeof window !== "undefined" ? Math.min(y, window.innerHeight - 250) : y;
|
|
|
|
return (
|
|
<div
|
|
ref={menuRef}
|
|
className="custom-context-menu"
|
|
style={{
|
|
position: "fixed",
|
|
top: adjustedY,
|
|
left: adjustedX,
|
|
zIndex: 9999,
|
|
}}
|
|
onClick={(e) => e.stopPropagation()}
|
|
onContextMenu={(e) => e.preventDefault()}
|
|
>
|
|
{items.map((item, i) => {
|
|
if (item.divider) {
|
|
return <div key={i} className="context-menu-divider" />;
|
|
}
|
|
return (
|
|
<div
|
|
key={i}
|
|
className={`context-menu-item${item.danger ? " danger" : ""}`}
|
|
onClick={() => {
|
|
if (item.onClick) item.onClick();
|
|
onClose();
|
|
}}
|
|
>
|
|
{item.icon && <span className="context-menu-icon">{item.icon}</span>}
|
|
<span style={{ flex: 1 }}>{item.label}</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
} |