import { useEffect, useState } from 'react'; import { useMousePosition } from '../../../hooks'; import { MOUSE_SAFE_AREA_CLASSNAME } from './MouseSafeArea.constants'; import { MouseSafeAreaProps } from './MouseSafeArea.types'; // From https://gist.github.com/eldh/51e3825b7aa55694f2a5ffa5f7de8a6a // open sourced by Linear.app /** * Component to cover the area between the mouse cursor and the submenu, to * allow moving cursor to lower parts of sub-menu without the submenu disappearing. */ export function MouseSafeArea({ openRight, parentRef, setIsInSafeArea }: MouseSafeAreaProps) { // Mouse safe area positions and dimensions const [left, setLeft] = useState(undefined); const [right, setRight] = useState(undefined); const [width, setWidth] = useState(undefined); const [clipPath, setClipPath] = useState(undefined); // Mouse positions const [mouseX, mouseY] = useMousePosition(); // Submenu positions and dimensions const { x: submenuX = 0, y: submenuY = 0, height: submenuHeight = 0, width: submenuWidth = 0 } = parentRef?.current?.getBoundingClientRect() || {}; useEffect(() => { setTimeout(() => { // The safe area should be positioned to the left of the submenu if it's opened to the right of the parent cell const getLeft = () => (openRight ? -Math.max(submenuX - mouseX, 10) : undefined); // The safe area should be positioned to the right of the submenu if it's opened to the left of the parent cell const getRight = () => openRight ? undefined : -Math.max(mouseX + submenuWidth - (submenuX + submenuWidth), 10 + submenuWidth); // The safe area should span the whole area between the cursor and the submenu const getWidth = () => openRight ? Math.max(submenuX - mouseX, 10) : Math.max(mouseX - (submenuX + submenuWidth), 10); // Renders the safe area triangle const getClipPath = () => openRight ? `polygon(100% 0%, 0% ${(100 * (mouseY - submenuY)) / submenuHeight}%, 100% 100%)` : `polygon(0% 0%, 0% 100%, 100% ${(100 * (mouseY - submenuY)) / submenuHeight}%)`; setLeft(getLeft()); setRight(getRight()); setWidth(getWidth()); setClipPath(getClipPath()); }, 100); }, [mouseX, mouseY, openRight, submenuHeight, submenuWidth, submenuX, submenuY]); return (
setIsInSafeArea(true)} onMouseLeave={() => setIsInSafeArea(false)} className={MOUSE_SAFE_AREA_CLASSNAME} style={{ position: 'absolute', top: 0, // background: 'orange', // for debugging height: submenuHeight, width, left, right, clipPath }} /> ); }