재사용 가능한 리액트 컨텍스트 메뉴 Hook

JM·2023년 7월 31일
0
post-custom-banner

배포된 패키지를 찾아봤지만 대부분 스타일이 강제되거나 입맛대로 수정하기가 힘들게 되어있다.
그래서 재사용이 가능하게 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;

이지

profile
No one's perfect, but still striving for perfection
post-custom-banner

0개의 댓글