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
|
import { useCallback, useEffect, useRef } from 'react';
import './ContextMenu.css';
export interface ContextMenuAction {
label: string;
icon: string;
disabled?: boolean;
onClick: () => void;
}
export interface ContextMenuProps {
x: number;
y: number;
actions: ContextMenuAction[];
dividerAfter?: number[];
onClose: () => void;
}
export default function ContextMenu({
x,
y,
actions,
dividerAfter = [],
onClose,
}: ContextMenuProps) {
const menuRef = useRef<HTMLDivElement>(null);
// Adjust position so menu stays within viewport
const adjustedPosition = useCallback(() => {
const el = menuRef.current;
if (!el) return { left: x, top: y };
const rect = el.getBoundingClientRect();
const left = x + rect.width > window.innerWidth ? x - rect.width : x;
const top = y + rect.height > window.innerHeight ? y - rect.height : y;
return { left: Math.max(0, left), top: Math.max(0, top) };
}, [x, y]);
// Close on click outside
useEffect(() => {
const handler = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
onClose();
}
};
// Use capture so we catch clicks before any other handler
document.addEventListener('mousedown', handler, true);
return () => document.removeEventListener('mousedown', handler, true);
}, [onClose]);
// Close on Escape
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('keydown', handler);
return () => document.removeEventListener('keydown', handler);
}, [onClose]);
// After mount, adjust position
useEffect(() => {
const el = menuRef.current;
if (!el) return;
const pos = adjustedPosition();
el.style.left = `${pos.left}px`;
el.style.top = `${pos.top}px`;
}, [adjustedPosition]);
const dividerSet = new Set(dividerAfter);
return (
<div
ref={menuRef}
className="ctx-menu"
style={{ left: x, top: y }}
role="menu"
>
{actions.map((action, i) => (
<div key={i}>
<button
className={`ctx-menu-item ${action.disabled ? 'ctx-menu-item-disabled' : ''}`}
role="menuitem"
disabled={action.disabled}
onClick={() => {
if (!action.disabled) {
action.onClick();
onClose();
}
}}
>
<span className="ctx-menu-icon">{action.icon}</span>
<span className="ctx-menu-label">{action.label}</span>
</button>
{dividerSet.has(i) && <div className="ctx-menu-divider" />}
</div>
))}
</div>
);
}
|