Skip to content

feature: Add useContextMenu Hook for Context Menu Management #660

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/usehooks-ts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions packages/usehooks-ts/src/useContextMenu/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useContextMenu'
38 changes: 38 additions & 0 deletions packages/usehooks-ts/src/useContextMenu/useContextMenu.demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import { useContextMenu } from './useContextMenu';

const Component: React.FC = () => {
const { isOpen, setIsOpen, position, handleContextMenu } = useContextMenu();

return (
<div
style={{
height: '100vh',
width: '100vw',
backgroundColor: '#f0f0f0',
position: 'relative',
}}
onContextMenu={handleContextMenu}
>
<h1>Right-click anywhere to open the context menu</h1>
{isOpen && (
<div
style={{
position: 'absolute',
top: position.y,
left: position.x,
padding: '8px',
backgroundColor: 'white',
border: '1px solid #ccc',
borderRadius: '4px',
boxShadow: '0px 4px 8px rgba(0, 0, 0, 0.2)',
}}
>
<p style={{ margin: 0 }}>Context Menu</p>
</div>
)}
</div>
);
};

export default Component;
14 changes: 14 additions & 0 deletions packages/usehooks-ts/src/useContextMenu/useContextMenu.md
Original file line number Diff line number Diff line change
@@ -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';
50 changes: 50 additions & 0 deletions packages/usehooks-ts/src/useContextMenu/useContextMenu.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
79 changes: 79 additions & 0 deletions packages/usehooks-ts/src/useContextMenu/useContextMenu.ts
Original file line number Diff line number Diff line change
@@ -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<SetStateAction<boolean>>;
/** Function to update the context menu position. */
setPosition: Dispatch<SetStateAction<{ x: number; y: number }>>;
/** 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<boolean>(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,
};
}