All files / src/entities/layer/ui/components LayerMenu.tsx

76.47% Statements 13/17
44.44% Branches 4/9
83.33% Functions 5/6
78.57% Lines 11/14

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>
	);
});