From ab5bcc9af2fec20061297fc04ff413d9e0f15b0d Mon Sep 17 00:00:00 2001 From: default Date: Tue, 11 Aug 2026 20:29:04 +0000 Subject: [PATCH] fix(core): keep submenu open when hovering into it The ActionPanel source submenu used openOnHover with delay 0/closeDelay 0, which relies on floating-ui's safePolygon hover-intent. The polygon geometry is degenerate when the popup is taller/wider than its trigger and flips to the opposite side, so the popup closes while the pointer is still inside it (or while moving into it), making the action buttons hard to click. Replace with controlled open state and a standard hover-intent pattern: a 150ms close grace period that is cancelled whenever the pointer is over the trigger or the popup. Also set the selected source when opening via hover so the plugin action panels (preview/comments) render on first open. --- packages/core/src/components/ActionPanel.tsx | 39 +++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/packages/core/src/components/ActionPanel.tsx b/packages/core/src/components/ActionPanel.tsx index 8f4889e..6449210 100644 --- a/packages/core/src/components/ActionPanel.tsx +++ b/packages/core/src/components/ActionPanel.tsx @@ -7,7 +7,7 @@ import { } from '@react-trace/ui-components' import { useAtom, useAtomValue, useSetAtom } from 'jotai' import type { ReactNode } from 'react' -import { useCallback } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { portalContainerAtom, @@ -257,6 +257,8 @@ export function ActionPanel({ plugins }: ActionPanelProps) { ) } +const SUBMENU_CLOSE_DELAY_MS = 150 + function Submenu({ entryContent, plugins, @@ -268,15 +270,40 @@ function Submenu({ }) { const portalContainer = useAtomValue(portalContainerAtom) const setSelectedSource = useSetAtom(selectedSourceAtom) + const [open, setOpen] = useState(false) + const closeTimerRef = useRef(undefined) + + useEffect(() => { + return () => window.clearTimeout(closeTimerRef.current) + }, []) + + const keepOpen = useCallback(() => { + window.clearTimeout(closeTimerRef.current) + setOpen(true) + setSelectedSource(source) + }, [source, setSelectedSource]) + + const scheduleClose = useCallback(() => { + window.clearTimeout(closeTimerRef.current) + closeTimerRef.current = window.setTimeout(() => setOpen(false), SUBMENU_CLOSE_DELAY_MS) + }, []) return ( open && setSelectedSource(source)} + open={open} + onOpenChange={(next) => { + setOpen(next) + if (next) { + window.clearTimeout(closeTimerRef.current) + setSelectedSource(source) + } + }} > entryStyle(state.open)} > {entryContent} @@ -301,6 +328,8 @@ function Submenu({ style={{ zIndex: 999999, pointerEvents: 'auto' }} > 0 ? 4 : 0,