import { css, cx } from '@emotion/css' import { ForwardedRef, forwardRef, useEffect, useLayoutEffect, useRef, useState, } from 'react' import { useClickAway } from 'react-use' import Icon from '../../Icon' import useLockMainScroll from '@/web/hooks/useLockMainScroll' import { motion } from 'framer-motion' import useMeasure from 'react-use-measure' interface ContextMenuItem { type: 'item' | 'submenu' | 'divider' label?: string onClick?: (e: MouseEvent) => void items?: ContextMenuItem[] } const Divider = () => (
) const Item = ({ item, onClose, }: { item: ContextMenuItem onClose: (e: MouseEvent) => void }) => { const [isHover, setIsHover] = useState(false) const itemRef = useRef(null) const submenuRef = useRef(null) const getSubmenuPosition = () => { if (!itemRef.current || !submenuRef.current) { return { x: 0, y: 0 } } const item = itemRef.current.getBoundingClientRect() const submenu = submenuRef.current.getBoundingClientRect() const isRightSide = item.x + item.width + submenu.width <= window.innerWidth const x = isRightSide ? item.x + item.width : item.x - submenu.width const isTopSide = item.y - 8 + submenu.height <= window.innerHeight const y = isTopSide ? item.y - 8 : item.y + item.height + 8 - submenu.height const transformOriginTable = { top: { right: 'origin-top-left', left: 'origin-top-right', }, bottom: { right: 'origin-bottom-left', left: 'origin-bottom-right', }, } as const return { x, y, transformOrigin: transformOriginTable[isTopSide ? 'top' : 'bottom'][ isRightSide ? 'right' : 'left' ], } } if (item.type === 'divider') return return (
{ if (!item.onClick) { return } const event = e as unknown as MouseEvent item.onClick?.(event) onClose(event) }} onMouseOver={() => setIsHover(true)} onMouseLeave={() => setIsHover(false)} className='relative px-2' >
{item.label}
{item.type === 'submenu' && ( )} {item.type === 'submenu' && item.items && ( )} {item.type === 'submenu' && item.items && isHover && ( )}
) } const Menu = forwardRef( ( { position, items, onClose, forMeasure, }: { position: { x: number y: number transformOrigin?: | 'origin-top-left' | 'origin-top-right' | 'origin-bottom-left' | 'origin-bottom-right' } items: ContextMenuItem[] onClose: (e: MouseEvent) => void forMeasure?: boolean }, ref: ForwardedRef ) => { return ( {items.map((item, index) => ( ))} ) } ) Menu.displayName = 'Menu' const BasicContextMenu = ({ onClose, items, target, cursorPosition, options, }: { onClose: (e: MouseEvent) => void items: ContextMenuItem[] target: HTMLElement cursorPosition: { x: number; y: number } options?: { useCursorPosition?: boolean } | null }) => { const menuRef = useRef(null) const [measureRef, menu] = useMeasure() const [position, setPosition] = useState<{ x: number; y: number } | null>( null ) useClickAway(menuRef, onClose) useLockMainScroll(!!position) useLayoutEffect(() => { if (options?.useCursorPosition) { const leftX = cursorPosition.x const rightX = cursorPosition.x - menu.width const bottomY = cursorPosition.y const topY = cursorPosition.y - menu.height const position = { x: leftX + menu.width < window.innerWidth ? leftX : rightX, y: bottomY + menu.height < window.innerHeight ? bottomY : topY, } setPosition(position) } else { const button = target.getBoundingClientRect() const leftX = button.x const rightX = button.x - menu.width + button.width const bottomY = button.y + button.height + 8 const topY = button.y - menu.height - 8 const position = { x: leftX + menu.width < window.innerWidth ? leftX : rightX, y: bottomY + menu.height < window.innerHeight ? bottomY : topY, } setPosition(position) } }, [target, menu, options?.useCursorPosition, cursorPosition]) return ( <> {position && ( )} ) } export default BasicContextMenu