import toast from 'react-hot-toast' import { ActionIcon, ActionIconProps } from './ActionIcon' import { MouseEvent, RefObject, useCallback, useEffect, useMemo, useRef, useState, } from 'react' import { useHotkeys } from 'react-hotkeys-hook' import { Dialog } from '@headlessui/react' export interface ContextMenuProps extends Omit, 'children'> { items?: React.ReactElement[] menuTargetElement?: RefObject guard?: (e: globalThis.MouseEvent) => boolean event?: 'contextmenu' | 'mouseup' } const DefaultContextMenuItems = [ , , // add more default context menu items here ] export function ContextMenu({ items = DefaultContextMenuItems, menuTargetElement, className, guard, event = 'contextmenu', ...props }: ContextMenuProps) { const dialogRef = useRef(null) const [open, setOpen] = useState(false) const [windowSize, setWindowSize] = useState({ width: globalThis?.window?.innerWidth, height: globalThis?.window?.innerHeight, }) const [position, setPosition] = useState({ x: 0, y: 0 }) useHotkeys('esc', () => setOpen(false), { enabled: open, }) const handleContextMenu = useCallback( (e: globalThis.MouseEvent) => { if (guard && !guard(e)) return e.preventDefault() setPosition({ x: e.clientX, y: e.clientY }) setOpen(true) }, [guard, setPosition, setOpen] ) const dialogPositionStyle = useMemo(() => { if (!dialogRef.current) return { top: 0, left: 0, right: 'auto', bottom: 'auto', } return { top: position.y + dialogRef.current.clientHeight > windowSize.height ? 'auto' : position.y, left: position.x + dialogRef.current.clientWidth > windowSize.width ? 'auto' : position.x, right: position.x + dialogRef.current.clientWidth > windowSize.width ? windowSize.width - position.x : 'auto', bottom: position.y + dialogRef.current.clientHeight > windowSize.height ? windowSize.height - position.y : 'auto', } }, [position, windowSize, dialogRef.current]) // Listen for window resize to update context menu position useEffect(() => { const handleResize = () => { setWindowSize({ width: globalThis?.window?.innerWidth, height: globalThis?.window?.innerHeight, }) } globalThis?.window?.addEventListener('resize', handleResize) return () => { globalThis?.window?.removeEventListener('resize', handleResize) } }, []) // Add context menu listener to target once mounted useEffect(() => { menuTargetElement?.current?.addEventListener(event, handleContextMenu) return () => { menuTargetElement?.current?.removeEventListener(event, handleContextMenu) } }, [menuTargetElement?.current]) return ( setOpen(false)}>
{ e.preventDefault() setPosition({ x: e.clientX, y: e.clientY }) }} >
    setOpen(false)} > {...items}
) } export function ContextMenuDivider() { return
} interface ContextMenuItemProps { children: React.ReactNode icon?: ActionIconProps['icon'] onClick?: () => void hotkey?: string 'data-testid'?: string } export function ContextMenuItem(props: ContextMenuItemProps) { const { children, icon, onClick, hotkey } = props return ( ) } export function ContextMenuItemRefresh() { return ( globalThis?.window?.location.reload()} > Refresh ) } interface ContextMenuItemCopyProps { toBeCopiedContent?: string toBeCopiedLabel?: string } export function ContextMenuItemCopy({ toBeCopiedContent = globalThis.window?.getSelection()?.toString(), toBeCopiedLabel = 'selection', }: ContextMenuItemCopyProps) { return ( { if (toBeCopiedContent) { globalThis?.navigator?.clipboard .writeText(toBeCopiedContent) .then(() => toast.success(`Copied ${toBeCopiedLabel} to clipboard`)) .catch(() => toast.error(`Failed to copy ${toBeCopiedLabel} to clipboard`) ) } }} > Copy ) }