483

useContextMenu

Hook that handles custom context menus on desktop and long press on touch devices

elementslowtest coverage
TK
reacuseTokyo, Japan
Tokyo
284,910 likes

reacuse Neon nights in Shibuya 🌃 Currently 18°C and clear.

9.7M residents · View all 1,204 comments
import { useClickOutside, useContextMenu } from '@siberiacancode/reactuse';
import {
  BookmarkIcon,
  HeartIcon,
  MapPinIcon,
  MessageCircleIcon,
  SendIcon,
  Share2Icon,
  Trash2Icon
} from 'lucide-react';

const Demo = () => {
  const contextMenu = useContextMenu<HTMLDivElement>();
  const menuRef = useClickOutside<HTMLDivElement>(() => contextMenu.close());

  return (
    <section className='flex flex-col items-center p-4'>
      <div
        ref={contextMenu.ref}
        className='bg-card w-full max-w-sm cursor-context-menu overflow-hidden rounded-2xl select-none'
      >
        <div className='flex items-center gap-3 px-4 py-3'>
          <div data-size='lg' data-slot='avatar'>
            <span data-slot='avatar-fallback'>TK</span>
          </div>
          <div className='flex flex-1 flex-col leading-tight'>
            <span className='text-foreground text-sm font-semibold'>reacuse</span>
            <span className='text-muted-foreground flex items-center gap-1 text-xs'>
              <MapPinIcon className='size-3' />
              Tokyo, Japan
            </span>
          </div>
        </div>

        <div className='relative aspect-square'>
          <img alt='Tokyo' className='size-full object-cover' src='/new/images/tokyo.png' />
        </div>

        <div className='flex items-center gap-4 px-4 pt-3'>
          <HeartIcon className='size-6' />
          <MessageCircleIcon className='size-6' />
          <SendIcon className='size-6' />
          <BookmarkIcon className='ml-auto size-6' />
        </div>

        <div className='flex flex-col gap-1 px-4 py-2'>
          <span className='text-foreground text-sm font-semibold'>284,910 likes</span>
          <p className='text-foreground text-sm'>
            <span className='font-semibold'>reacuse</span> Neon nights in Shibuya 🌃 Currently 18°C
            and clear.
          </p>
          <span className='text-muted-foreground text-xs'>
            9.7M residents · View all 1,204 comments
          </span>
        </div>
      </div>

      {contextMenu.opened && contextMenu.position && (
        <div
          ref={menuRef}
          className='fixed z-50 w-48'
          data-slot='dropdown-menu-content'
          style={{ top: contextMenu.position.y, left: contextMenu.position.x }}
        >
          <div data-slot='dropdown-menu-item'>
            <HeartIcon />
            Like post
          </div>
          <div data-slot='dropdown-menu-item'>
            <BookmarkIcon />
            Save
          </div>
          <div data-slot='dropdown-menu-item'>
            <Share2Icon />
            Share
            <span data-slot='dropdown-menu-shortcut'>⌘S</span>
          </div>
          <div data-slot='dropdown-menu-separator' />
          <div data-slot='dropdown-menu-item' data-variant='destructive'>
            <Trash2Icon />
            Remove
          </div>
        </div>
      )}
    </section>
  );
};

export default Demo;

Installation

pnpm add @siberiacancode/reactuse

Usage

const menu = useContextMenu(ref, (position) => console.log(position));
// or
const menu = useContextMenu(ref, options);
// or
const { ref, opened, position } = useContextMenu((position) => console.log(position));
// or
const { ref, opened, position } = useContextMenu(options);

Type Declarations

import type { HookTarget } from '@/utils/helpers';

import type { StateRef } from '../useRefState/useRefState';

export type ContextMenuEvent = MouseEvent | TouchEvent;

export interface ContextMenuPosition {
  /** The x coordinate of the event */
  x: number;
  /** The y coordinate of the event */
  y: number;
}

export type UseContextMenuCallback = (
  position: ContextMenuPosition,
  event: ContextMenuEvent
) => void;

export interface UseContextMenuOptions {
  /** The long press delay on touch devices in milliseconds */
  delay?: number;
  /** The enabled state of the hook */
  enabled?: boolean;
  /** The callback function to be invoked when the menu opens */
  onOpen?: UseContextMenuCallback;
  /** The callback function to be invoked when the menu closes */
  onClose?: () => void;
  /** The callback function to be invoked when the interaction ends */
  onEnd?: (event: ContextMenuEvent) => void;
  /** The callback function to be invoked when the interaction starts */
  onStart?: (event: ContextMenuEvent) => void;
}

export interface UseContextMenuReturn {
  /** The context menu opened state */
  opened: boolean;
  /** The context menu position */
  position?: ContextMenuPosition;
  /** Close the context menu */
  close: () => void;
  /** Open the context menu */
  open: (position: ContextMenuPosition, event?: ContextMenuEvent) => void;
}

export interface UseContextMenu {
  (target: HookTarget, callback?: UseContextMenuCallback): UseContextMenuReturn;

  (target: HookTarget, options?: UseContextMenuOptions): UseContextMenuReturn;

  <Target extends Element>(
    callback?: UseContextMenuCallback,
    target?: never
  ): UseContextMenuReturn & { ref: StateRef<Target> };

  <Target extends Element>(
    options?: UseContextMenuOptions,
    target?: never
  ): UseContextMenuReturn & { ref: StateRef<Target> };
}

API

Parameters

NameTypeDefaultNote
targetHookTarget-The target element for context menu handling
callbackUseContextMenuCallback-The callback function to be invoked when the menu opens

Returns

UseContextMenuReturn

Parameters

NameTypeDefaultNote
targetHookTarget-The target element for context menu handling
options.delaynumber500The long press delay on touch devices in milliseconds
options.enabledbooleantrueThe enabled state of the hook
options.onOpenUseContextMenuCallback-The callback function to be invoked when the menu opens
options.onClose() => void-The callback function to be invoked when the menu closes
options.onStart(event: ContextMenuEvent) => void-The callback function to be invoked when the interaction starts
options.onEnd(event: ContextMenuEvent) => void-The callback function to be invoked when the interaction ends

Returns

UseContextMenuReturn

Parameters

NameTypeDefaultNote
callbackUseContextMenuCallback-The callback function to be invoked when the menu opens

Returns

UseContextMenuReturn & { ref: StateRef<Target> }

Parameters

NameTypeDefaultNote
options.delaynumber500The long press delay on touch devices in milliseconds
options.enabledbooleantrueThe enabled state of the hook
options.onOpenUseContextMenuCallback-The callback function to be invoked when the menu opens
options.onClose() => void-The callback function to be invoked when the menu closes
options.onStart(event: ContextMenuEvent) => void-The callback function to be invoked when the interaction starts
options.onEnd(event: ContextMenuEvent) => void-The callback function to be invoked when the interaction ends

Returns

UseContextMenuReturn & { ref: StateRef<Target> }

Contributors

ddebabin

Last updated on