diff --git a/packages/usehooks-ts/src/index.ts b/packages/usehooks-ts/src/index.ts index 9577bd16..da4c51b5 100644 --- a/packages/usehooks-ts/src/index.ts +++ b/packages/usehooks-ts/src/index.ts @@ -2,6 +2,7 @@ export * from './useBoolean' export * from './useClickAnyWhere' export * from './useCopyToClipboard' export * from './useCountdown' +export * from './useContextMenu' export * from './useCounter' export * from './useDarkMode' export * from './useDebounceCallback' diff --git a/packages/usehooks-ts/src/useContextMenu/index.ts b/packages/usehooks-ts/src/useContextMenu/index.ts new file mode 100644 index 00000000..9c94058c --- /dev/null +++ b/packages/usehooks-ts/src/useContextMenu/index.ts @@ -0,0 +1 @@ +export * from './useContextMenu' diff --git a/packages/usehooks-ts/src/useContextMenu/useContextMenu.demo.tsx b/packages/usehooks-ts/src/useContextMenu/useContextMenu.demo.tsx new file mode 100644 index 00000000..8f15d2a5 --- /dev/null +++ b/packages/usehooks-ts/src/useContextMenu/useContextMenu.demo.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { useContextMenu } from './useContextMenu'; + +const Component: React.FC = () => { + const { isOpen, setIsOpen, position, handleContextMenu } = useContextMenu(); + + return ( +
+

Right-click anywhere to open the context menu

+ {isOpen && ( +
+

Context Menu

+
+ )} +
+ ); +}; + +export default Component; diff --git a/packages/usehooks-ts/src/useContextMenu/useContextMenu.md b/packages/usehooks-ts/src/useContextMenu/useContextMenu.md new file mode 100644 index 00000000..13c847a4 --- /dev/null +++ b/packages/usehooks-ts/src/useContextMenu/useContextMenu.md @@ -0,0 +1,14 @@ +# useContextMenu Hook + +`useContextMenu` is a custom React hook that helps manage the visibility and position of a context menu. It tracks whether the menu is open or closed and updates its position based on the user's right-click location. + +## Features +- Manage the visibility state of a context menu. +- Position the menu precisely based on the right-click coordinates. +- Automatically close the menu when the user clicks anywhere outside of it. + +## Installation +To use this hook, simply copy the `useContextMenu` code into your project. + +```typescript +import { useContextMenu } from 'usehooks-ts'; diff --git a/packages/usehooks-ts/src/useContextMenu/useContextMenu.test.ts b/packages/usehooks-ts/src/useContextMenu/useContextMenu.test.ts new file mode 100644 index 00000000..d1ce6035 --- /dev/null +++ b/packages/usehooks-ts/src/useContextMenu/useContextMenu.test.ts @@ -0,0 +1,50 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import { useContextMenu } from './useContextMenu'; + +describe('useContextMenu', () => { + test('should initialize with default values', () => { + const { result } = renderHook(() => useContextMenu()); + + expect(result.current.isOpen).toBe(false); + expect(result.current.position).toEqual({ x: 0, y: 0 }); + }); + + test('should initialize with provided initial values', () => { + const initialOpen = true; + const initialPosition = { x: 50, y: 50 }; + const { result } = renderHook(() => useContextMenu(initialOpen, initialPosition)); + + expect(result.current.isOpen).toBe(true); + expect(result.current.position).toEqual(initialPosition); + }); + + test('should toggle isOpen state', () => { + const { result } = renderHook(() => useContextMenu()); + + act(() => result.current.setIsOpen(true)); + expect(result.current.isOpen).toBe(true); + + act(() => result.current.setIsOpen(false)); + expect(result.current.isOpen).toBe(false); + }); + + test('should update position state', () => { + const { result } = renderHook(() => useContextMenu()); + + const newPosition = { x: 20, y: 20 }; + act(() => result.current.setPosition(newPosition)); + expect(result.current.position).toEqual(newPosition); + }); + + test('should close context menu when document is clicked', () => { + const { result } = renderHook(() => useContextMenu(true)); + + expect(result.current.isOpen).toBe(true); + + act(() => { + document.dispatchEvent(new MouseEvent('click', { bubbles: true })); + }); + + expect(result.current.isOpen).toBe(false); + }); +}); diff --git a/packages/usehooks-ts/src/useContextMenu/useContextMenu.ts b/packages/usehooks-ts/src/useContextMenu/useContextMenu.ts new file mode 100644 index 00000000..0e1d8136 --- /dev/null +++ b/packages/usehooks-ts/src/useContextMenu/useContextMenu.ts @@ -0,0 +1,79 @@ +import { useEffect, useState } from 'react'; +import type { Dispatch, SetStateAction, MouseEvent } from 'react'; + +/** + * Structure of the return value for `useContextMenu`. + */ +type UseContextMenuReturn = { + /** State indicating if the context menu is open. */ + isOpen: boolean; + /** Coordinates for the context menu position. */ + position: { + x: number; + y: number; + }; + /** Toggle function for the `isOpen` state. */ + setIsOpen: Dispatch>; + /** Function to update the context menu position. */ + setPosition: Dispatch>; + /** Function to handle right-click to open the context menu at cursor position. */ + handleContextMenu: (event: MouseEvent) => void; +}; + +/** + * Custom hook to manage a context menu's visibility and position. + * + * This hook provides: + * - `isOpen`: Boolean state indicating visibility of the context menu. + * - `position`: An object containing `x` and `y` coordinates for the context menu. + * - `setIsOpen` and `setPosition`: Functions to control the menu state and position. + * + * By default, the context menu is hidden, and its initial position is `{ x: 0, y: 0 }`. + * The hook automatically closes the menu when a click is detected anywhere on the document. + * + * @param {boolean} [initialOpen=false] - Initial visibility of the context menu. + * @param {{ x: number; y: number }} [initialPosition={ x: 0, y: 0 }] - Initial position of the context menu. + * @returns {UseContextMenuReturn} Object containing `isOpen`, `position`, and handler functions. + * + * @example + * ```tsx + * const { isOpen, setIsOpen, position, setPosition, handleContextMenu } = useContextMenu(); + * ``` + * + * @public + */ +export function useContextMenu( + initialOpen: boolean = false, + initialPosition: { x: number; y: number } = { x: 0, y: 0 } +): UseContextMenuReturn { + const [isOpen, setIsOpen] = useState(initialOpen); + const [position, setPosition] = useState<{ x: number; y: number }>(initialPosition); + + useEffect(() => { + /** Hide the context menu on any outside click */ + const handleClickOutside = () => setIsOpen(false); + + document.addEventListener("click", handleClickOutside); + return () => { + document.removeEventListener("click", handleClickOutside); + }; + }, []); + + /** + * Opens the context menu at the cursor's position on right-click. + * @param {MouseEvent} event - Mouse event triggered by right-click. + */ + const handleContextMenu = (event: MouseEvent) => { + event.preventDefault(); + setPosition({ x: event.clientX + 2, y: event.clientY - 6 }); + setIsOpen(true); + }; + + return { + isOpen, + setIsOpen, + position, + setPosition, + handleContextMenu, + }; +}