Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 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 | 1x 4x 4x 4x 3x 3x 3x 4x 1x 1x 1x | import React, { useEffect, useRef } from 'react';
import {
MoreVertical as MenuIcon,
Pencil as RenameIcon,
Trash2 as DeleteIcon,
} from 'lucide-react';
import { AnimatePresence, motion } from 'framer-motion';
interface LayerMenuProps {
layerId: string;
isMenuOpen: boolean;
setOpenMenuId: (id: string | null) => void;
onRenameClick: () => void;
onDeleteClick: () => void;
}
export const LayerMenu = React.memo(function LayerMenu({
layerId,
isMenuOpen,
setOpenMenuId,
onRenameClick,
onDeleteClick,
}: LayerMenuProps) {
const menuRef = useRef<HTMLDivElement>(null);
// Close menu if click out of menu
useEffect(() => {
if (!isMenuOpen) return;
const handleOutsideClick = (mouseEvent: MouseEvent) => {
if (!menuRef.current) return;
if (!menuRef.current.contains(mouseEvent.target as Node)) {
setOpenMenuId(null);
}
};
document.addEventListener('mousedown', handleOutsideClick);
return () => document.removeEventListener('mousedown', handleOutsideClick);
}, [isMenuOpen, setOpenMenuId]);
// Toggle menu
const handleMenuClick = (mouseEvent: React.MouseEvent) => {
mouseEvent.stopPropagation();
setOpenMenuId(isMenuOpen ? null : layerId);
};
return (
<div className="relative" ref={menuRef}>
<button
onClick={handleMenuClick}
className="w-8 h-8 flex items-center justify-center rounded hover:bg-gray-100"
aria-label="Layer options menu"
data-testid="layer-menu-btn"
>
<MenuIcon className="w-4 h-4" />
</button>
{/* Animated context menu using Framer Motion */}
<AnimatePresence>
{isMenuOpen && (
<motion.div
// Closes the menu if the mouse leaves the menu area.
onMouseLeave={() => setOpenMenuId(null)}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.15 }}
className="absolute right-0 mt-2 w-40 rounded-md border border-gray-200 bg-white shadow-md z-10 origin-top-right"
role="menu"
>
<button
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-gray-50"
onClick={onRenameClick}
>
<RenameIcon className="w-4 h-4 text-gray-600" />
Rename
</button>
<button
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-rose-600 hover:bg-rose-50"
onClick={onDeleteClick}
>
<DeleteIcon className="w-4 h-4" />
Delete
</button>
</motion.div>
)}
</AnimatePresence>
</div>
);
});
|