배포된 패키지를 찾아봤지만 대부분 스타일이 강제되거나 입맛대로 수정하기가 힘들게 되어있다.
그래서 재사용이 가능하게 Hook 으로 만들었다.
// use-context-menu.tsx
import { useState, useCallback, useRef, useEffect } from "react";
export interface ContextMenuItem {
label: string;
action: () => void;
}
interface ContextMenuState {
x: number;
y: number;
menuItems: ContextMenuItem[];
isOpen: boolean;
}
const useContextMenu = () => {
const [contextMenuState, setContextMenuState] = useState<ContextMenuState>({
x: 0,
y: 0,
menuItems: [],
isOpen: false,
});
const contextMenuRef = useRef<HTMLDivElement>(null);
const handleContextMenu = useCallback(
(event: React.MouseEvent, menuItems: ContextMenuItem[]) => {
event.preventDefault();
const clickX = event.clientX;
const clickY = event.clientY;
setContextMenuState({
x: clickX,
y: clickY,
menuItems: menuItems,
isOpen: true,
});
},
[]
);
const handleCloseContextMenu = useCallback(() => {
setContextMenuState((prevState) => ({
...prevState,
isOpen: false,
}));
}, []);
const handleMenuItemClick = useCallback(
(action: () => void) => {
action();
handleCloseContextMenu();
},
[handleCloseContextMenu]
);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
contextMenuRef.current &&
!contextMenuRef.current.contains(event.target as Node)
) {
handleCloseContextMenu();
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [handleCloseContextMenu]);
return {
contextMenuState,
handleContextMenu,
handleMenuItemClick,
contextMenuRef,
};
};
export default useContextMenu;
모든 작업은 끝났다. 이제 원하는 컴포넌트에서 사용만 하면 된다.
훅을 사용하는 컴포넌트에서 컨텍스트 메뉴 스타일링, 메뉴 구성까지 모두 커스텀할 수 있다.
const SomeComponent:React.FC = () => {
const {
contextMenuRef,
contextMenuState,
handleContextMenu,
handleMenuItemClick,
} = useContextMenu();
const handleRightClick = (event: React.MouseEvent) => {
const menuItems: ContextMenuItem[] = [
{
label: "Open",
action: () => handleMenuItemClick(() => { console.log("Open action is triggering"); }),
},
{
label: "Rename",
action: () => handleMenuItemClick(() => { console.log("Rename action is triggering"); }),
},
{
label: "Delete",
action: () => handleMenuItemClick(() => { console.log("Delete action is triggering") }),
},
];
handleContextMenu(event, menuItems);
};
return (
<div>
{contextMenuState.isOpen && (
<div
ref={contextMenuRef}
className="fixed rounded bg-gray-100 border border-gray-300 shadow shadow-gray-500 py-1 z-50"
style={{ top: contextMenuState.y, left: contextMenuState.x }}
>
{contextMenuState.menuItems.map((item) => (
<div
key={item.label}
className="px-4 py-2 cursor-pointer hover:bg-gray-200"
onClick={() => item.action()}
>
{item.label}
</div>
))}
</div>
)}
</div>
);
}
export default SomeComponent;
이지