reactuse icon indicating copy to clipboard operation
reactuse copied to clipboard

Integrate new hook useRightClick

Open virus231 opened this issue 11 months ago • 10 comments

virus231 avatar Mar 06 '25 10:03 virus231

@virus231 hello, thx for open new issuse, can you share refs or example of hook and describe user cases, thx

debabin avatar Mar 06 '25 17:03 debabin

@debabin Hello

Here's a basic implementation of `useRightClick` hook:

```typescript
import { useRef, useEffect } from 'react';

export function useRightClick(handler: (event: MouseEvent) => void) {
  const ref = useRef<HTMLElement>(null);

  useEffect(() => {
    const element = ref.current;
    if (!element) return;

    const handleRightClick = (event: MouseEvent) => {
      event.preventDefault();
      handler(event);
    };

    element.addEventListener('contextmenu', handleRightClick);
    return () => element.removeEventListener('contextmenu', handleRightClick);
  }, [handler]);

  return ref;
}

Advanced implementation with options:

interface UseRightClickOptions {
  preventDefault?: boolean;
  stopPropagation?: boolean;
  enabled?: boolean;
  touchSupport?: boolean; // Long press on mobile
  touchDuration?: number;
}

export function useRightClick(
  handler: (event: MouseEvent | TouchEvent) => void,
  options: UseRightClickOptions = {}
) {
  const {
    preventDefault = true,
    stopPropagation = false,
    enabled = true,
    touchSupport = true,
    touchDuration = 500
  } = options;

  const ref = useRef<HTMLElement>(null);
  const touchTimer = useRef<NodeJS.Timeout>();

  useEffect(() => {
    const element = ref.current;
    if (!element || !enabled) return;

    const handleContextMenu = (event: MouseEvent) => {
      if (preventDefault) event.preventDefault();
      if (stopPropagation) event.stopPropagation();
      handler(event);
    };

    const handleTouchStart = (event: TouchEvent) => {
      if (!touchSupport) return;
      touchTimer.current = setTimeout(() => {
        if (preventDefault) event.preventDefault();
        handler(event);
      }, touchDuration);
    };

    const handleTouchEnd = () => {
      if (touchTimer.current) clearTimeout(touchTimer.current);
    };

    element.addEventListener('contextmenu', handleContextMenu);
    if (touchSupport) {
      element.addEventListener('touchstart', handleTouchStart);
      element.addEventListener('touchend', handleTouchEnd);
    }

    return () => {
      element.removeEventListener('contextmenu', handleContextMenu);
      element.removeEventListener('touchstart', handleTouchStart);
      element.removeEventListener('touchend', handleTouchEnd);
      if (touchTimer.current) clearTimeout(touchTimer.current);
    };
  }, [handler, preventDefault, stopPropagation, enabled, touchSupport, touchDuration]);

  return ref;
}

Usage examples:

// Basic context menu
const ref = useRightClick(() => showContextMenu());

// With coordinates for positioning
const ref = useRightClick((event) => {
  showMenu({ x: event.clientX, y: event.clientY });
});

// Mobile-friendly with options
const ref = useRightClick(handleAction, {
  touchSupport: true,
  touchDuration: 600,
  preventDefault: false
});

Additional useful features to consider:

  • Return mouse coordinates: { ref, lastClickPosition }
  • Multiple gesture support: double right-click, right-click + key
  • Integration with existing context menu libraries
  • Accessibility support (keyboard navigation)
  • Performance optimization for large lists

Would be a valuable addition! 🚀

virus231 avatar Jul 16 '25 23:07 virus231

Any updates?

virus231 avatar Jul 26 '25 23:07 virus231

@virus231 in progress

debabin avatar Jul 27 '25 12:07 debabin

@virus231 Now I think a lot about the right click hook

the useContextMenu hook looks more logical, you can immediately implement touch delay and scroll lock in it,

right click is more about just a click, if you just do a right click then everything else will be on the conscience of the users, for example, debounce the popup set and scroll lock for example

debabin avatar Jul 27 '25 15:07 debabin

@debabin Thanks for your input!

I see your point. useContextMenu covers more advanced cases like touch delay and scroll lock out of the box, while useRightClick is just a basic right-click handler. Maybe it makes sense to focus on improving useContextMenu and document how to use it for simple right-click needs, instead of maintaining two similar hooks. What do you think?

virus231 avatar Jul 27 '25 16:07 virus231

I'm in a difficult situation right now, on the one hand I don't really want to make a hook for the sake of rigth click, because the context menu in mobile is done 100% with a delay, but at the same time making a super hook useContextMenu is also a strong story, I'm thinking about something in between, make useContextMenu instead of useRigthClick and make the exact logic inside, disable scrolling and everything else can be implemented with the help of other hooks @virus231

debabin avatar Jul 27 '25 16:07 debabin

I appreciate your perspective. A unified useContextMenu hook that handles both desktop and mobile interactions sounds like a solid direction. This would avoid redundancy and give users more control over advanced features if needed. @debabin

virus231 avatar Jul 27 '25 17:07 virus231

import {
  useClickOutside,
  useLockScroll,
  useRightClick,
} from "@siberiacancode/reactuse";
import { useRef, useState } from "react";

const useContextMenu = () => {
  const [position, setPosition] = useState<{ x: number; y: number }>();

  const timerIdRef = useRef<NodeJS.Timeout>(null);

  useLockScroll({
    enabled: !!position,
  });

  const triggerRef = useRightClick<HTMLDivElement>(
    (positions, event) => {
      if (event instanceof MouseEvent) setPosition(positions);
    },
    {
      onStart: (event) => {
        console.log("onStart", event);
        timerIdRef.current = setTimeout(() => {
          if (event instanceof MouseEvent) return;
          const touchEvent = event as TouchEvent;
          const touch = touchEvent.touches[0];
          if (!touch) return;
          setPosition({ x: touch.clientX, y: touch.clientY });
        }, 500);
      },
      onEnd: () => {
        if (timerIdRef.current) clearTimeout(timerIdRef.current);
      },
    }
  );

  const contextMenuRef = useClickOutside<HTMLDivElement>(() =>
    setPosition(undefined)
  );

  return { triggerRef, contextMenuRef, position };
};

const Demo = () => {
  const contextMenu = useContextMenu();

  return (
    <div
      ref={contextMenu.triggerRef}
      className="relative h-20 cursor-pointer flex items-center justify-center text-lg border border-gray-200 rounded-lg p-2"
    >
      Right click me
      {!!contextMenu.position?.x && !!contextMenu.position?.y && (
        <div
          className="fixed z-10 flex items-center justify-center rounded-lg border border-gray-200 border-dashed bg-[var(--vp-code-block-bg)] p-5"
          ref={contextMenu.contextMenuRef}
          style={{
            top: contextMenu.position.y,
            left: contextMenu.position.x,
          }}
        >
          Context menu
        </div>
      )}
    </div>
  );
};

export default Demo;

I prepared small example about context menu @virus231

debabin avatar Jul 27 '25 17:07 debabin

in this example i implemented a custom hook from hooks for context

debabin avatar Jul 27 '25 17:07 debabin