import React, { memo, RefObject, useCallback, useEffect, useMemo, useState } from 'react'; import { createPortal } from 'react-dom'; import Transition from '../Transition'; import styles from './ContextMenu.less'; const PADDING = 8; type Coordinates = [number, number]; type Size = [number, number]; type Lock = 'top' | 'right' | 'bottom' | 'left'; type Props = { children: React.ReactNode, on: RefObject[], autoClose: boolean, lock?: Lock, }; const ContextMenu = ({ children, on, autoClose, lock }: Props) => { const [active, setActive] = useState(false); const [position, setPosition] = useState([0, 0]); const [containerSize, setContainerSize] = useState([0, 0]); const [triggerRect, setTriggerRect] = useState(null); const ref = useCallback((element: HTMLDivElement) => { element && setContainerSize([element.offsetWidth, element.offsetHeight]); }, []); const style = useMemo(() => { const [viewportWidth, viewportHeight] = [window.innerWidth, window.innerHeight]; const [containerWidth, containerHeight] = containerSize; let x: number; let y: number; if (lock && triggerRect) { switch (lock) { case 'top': x = triggerRect.left; y = triggerRect.top - containerHeight; break; case 'bottom': x = triggerRect.left; y = triggerRect.bottom; break; case 'left': x = triggerRect.left - containerWidth; y = triggerRect.top; break; case 'right': x = triggerRect.right; y = triggerRect.top; break; } } else { [x, y] = position; } const left = Math.max( PADDING, Math.min( x + containerWidth > viewportWidth - PADDING ? x - containerWidth : x, viewportWidth - containerWidth - PADDING ) ); const top = Math.max( PADDING, Math.min( y + containerHeight > viewportHeight - PADDING ? y - containerHeight : y, viewportHeight - containerHeight - PADDING ) ); return { top, left }; }, [position, containerSize, lock, triggerRect]); const close = () => { setActive(false); }; const stopPropagation = (event: React.MouseEvent | React.TouchEvent) => { event.stopPropagation(); }; const onContextMenu = useCallback((event: MouseEvent) => { event.preventDefault(); if (lock) { const target = event.currentTarget as HTMLElement; setTriggerRect(target.getBoundingClientRect()); } else { setPosition([event.clientX, event.clientY]); } setActive(true); }, [lock]); const handleKeyDown = useCallback((event: KeyboardEvent) => event.key === 'Escape' && close(), []); const onClick = useCallback(() => { autoClose && close(); }, [autoClose]); useEffect(() => { on.forEach((ref) => ref.current && ref.current.addEventListener('contextmenu', onContextMenu)); document.addEventListener('keydown', handleKeyDown); return () => { on.forEach((ref) => ref.current && ref.current.removeEventListener('contextmenu', onContextMenu)); document.removeEventListener('keydown', handleKeyDown); }; }, [on, onContextMenu, handleKeyDown]); return createPortal((
{children}
), document.body); }; export default memo(ContextMenu);