import type { RefObject } from 'react'; import { useState, useEffect } from '../lib/teact/teact'; import { addExtraClass, removeExtraClass } from '../lib/teact/teact-dom'; import { requestMutation } from '../lib/fasterdom/fasterdom'; import type { IAnchorPosition } from '../types'; import { IS_TOUCH_ENV, IS_PWA, IS_IOS, } from '../util/windowEnvironment'; import useLastCallback from './useLastCallback'; const LONG_TAP_DURATION_MS = 200; const IOS_PWA_CONTEXT_MENU_DELAY_MS = 100; function stopEvent(e: Event) { e.stopImmediatePropagation(); e.preventDefault(); e.stopPropagation(); } const useContextMenuHandlers = ( elementRef: RefObject, isMenuDisabled?: boolean, shouldDisableOnLink?: boolean, shouldDisableOnLongTap?: boolean, ) => { const [isContextMenuOpen, setIsContextMenuOpen] = useState(false); const [contextMenuPosition, setContextMenuPosition] = useState(undefined); const [contextMenuTarget, setContextMenuTarget] = useState(undefined); const handleBeforeContextMenu = useLastCallback((e: React.MouseEvent) => { if (!isMenuDisabled && e.button === 2) { requestMutation(() => { removeExtraClass(e.target as HTMLElement, 'text-selection'); }); } }); const handleContextMenu = useLastCallback((e: React.MouseEvent) => { requestMutation(() => { addExtraClass(e.target as HTMLElement, 'text-selection'); }); if (isMenuDisabled || (shouldDisableOnLink && (e.target as HTMLElement).matches('a[href]'))) { return; } e.preventDefault(); if (contextMenuPosition) { return; } setIsContextMenuOpen(true); setContextMenuPosition({ x: e.clientX, y: e.clientY }); setContextMenuTarget(e.target as HTMLElement); }); const handleContextMenuClose = useLastCallback(() => { setIsContextMenuOpen(false); }); const handleContextMenuHide = useLastCallback(() => { setContextMenuPosition(undefined); }); // Support context menu on touch devices useEffect(() => { if (isMenuDisabled || !IS_TOUCH_ENV || shouldDisableOnLongTap) { return undefined; } const element = elementRef.current; if (!element) { return undefined; } let timer: number | undefined; const clearLongPressTimer = () => { if (timer) { clearTimeout(timer); timer = undefined; } }; const emulateContextMenuEvent = (originalEvent: TouchEvent) => { clearLongPressTimer(); const { clientX, clientY, target } = originalEvent.touches[0]; if (contextMenuPosition || (shouldDisableOnLink && (target as HTMLElement).matches('a[href]'))) { return; } // Temporarily intercept and clear the next click element.addEventListener('touchend', (e) => { // On iOS in PWA mode, the context menu may cause click-through to the element in the menu upon opening if (IS_IOS && IS_PWA) { setTimeout(() => { element.removeEventListener('mousedown', stopEvent, { capture: true, }); element.removeEventListener('click', stopEvent, { capture: true, }); }, IOS_PWA_CONTEXT_MENU_DELAY_MS); } stopEvent(e); }, { once: true, capture: true, }); // On iOS15, in PWA mode, the context menu immediately closes after opening if (IS_PWA && IS_IOS) { element.addEventListener('mousedown', stopEvent, { once: true, capture: true, }); element.addEventListener('click', stopEvent, { once: true, capture: true, }); } setIsContextMenuOpen(true); setContextMenuPosition({ x: clientX, y: clientY }); }; const startLongPressTimer = (e: TouchEvent) => { if (isMenuDisabled) { return; } clearLongPressTimer(); timer = window.setTimeout(() => emulateContextMenuEvent(e), LONG_TAP_DURATION_MS); }; // @perf Consider event delegation element.addEventListener('touchstart', startLongPressTimer, { passive: true }); element.addEventListener('touchcancel', clearLongPressTimer, true); element.addEventListener('touchend', clearLongPressTimer, true); element.addEventListener('touchmove', clearLongPressTimer, { passive: true }); return () => { clearLongPressTimer(); element.removeEventListener('touchstart', startLongPressTimer); element.removeEventListener('touchcancel', clearLongPressTimer, true); element.removeEventListener('touchend', clearLongPressTimer, true); element.removeEventListener('touchmove', clearLongPressTimer); }; }, [contextMenuPosition, isMenuDisabled, shouldDisableOnLongTap, elementRef, shouldDisableOnLink]); return { isContextMenuOpen, contextMenuPosition, contextMenuTarget, handleBeforeContextMenu, handleContextMenu, handleContextMenuClose, handleContextMenuHide, }; }; export default useContextMenuHandlers;