diff --git a/packages/react/src/internal/popup-menu/components/submenu-trigger/submenu-trigger.tsx b/packages/react/src/internal/popup-menu/components/submenu-trigger/submenu-trigger.tsx index 5ae36c00..2bdaf803 100644 --- a/packages/react/src/internal/popup-menu/components/submenu-trigger/submenu-trigger.tsx +++ b/packages/react/src/internal/popup-menu/components/submenu-trigger/submenu-trigger.tsx @@ -921,6 +921,30 @@ export const PopupMenuSubmenuTrigger = React.forwardRef< // Highlight on hover (use storeId for store operations) parentStore.setHighlightedId(item.storeId) + + // Pointer move can be the first allowed event after aim-guard unblocks + // this row (e.g. pointerenter was blocked earlier), so allow it to open. + if (!openOnHighlight || open) { + return + } + + clearLeaveMonitor() + clearCloseTimer() + + const pointerDelay = delay.pointer + if (pointerDelay <= 0) { + setOpen(true) + return + } + + if (openTimerRef.current !== null) { + return + } + + openTimerRef.current = setTimeout(() => { + openTimerRef.current = null + setOpen(true) + }, pointerDelay) }, [ onPointerMove, @@ -931,6 +955,12 @@ export const PopupMenuSubmenuTrigger = React.forwardRef< guardedTriggerIdRef, item.id, item.storeId, + openOnHighlight, + open, + clearLeaveMonitor, + clearCloseTimer, + delay.pointer, + setOpen, logAimTrace, parentStore, ], diff --git a/packages/react/src/internal/popup-menu/popup-menu.test.tsx b/packages/react/src/internal/popup-menu/popup-menu.test.tsx index 5b6ba27e..21a4a9e4 100644 --- a/packages/react/src/internal/popup-menu/popup-menu.test.tsx +++ b/packages/react/src/internal/popup-menu/popup-menu.test.tsx @@ -417,6 +417,27 @@ function NestedMenuForDataAttrs({ + + + Submenu sibling + + + + + + + + Sibling submenu item + + + + + + + @@ -1382,6 +1403,51 @@ describe('PopupMenu', () => { scenario.cleanup() } }) + + it('opens sibling submenu on pointermove after guard timeout', async () => { + const scenario = await setupAimMonitoringScenario(0) + + try { + fireEvent.pointerMove(window, { clientX: 120, clientY: 90 }) + fireEvent.pointerMove(window, { clientX: 150, clientY: 92 }) + fireEvent.pointerMove(window, { clientX: 180, clientY: 94 }) + + fireEvent.pointerLeave(scenario.submenuTrigger, { + clientX: 190, + clientY: 94, + }) + + const siblingTrigger = screen.getByTestId('submenu-trigger-sibling') + + fireEvent.pointerEnter(siblingTrigger, { + clientX: 182, + clientY: 136, + }) + fireEvent.pointerMove(siblingTrigger, { + clientX: 182, + clientY: 136, + }) + + expect( + screen.queryByTestId('popup-submenu-sibling'), + ).not.toBeInTheDocument() + + await sleep(650) + + fireEvent.pointerMove(siblingTrigger, { + clientX: 186, + clientY: 140, + }) + + await waitFor(() => { + expect( + screen.getByTestId('popup-submenu-sibling'), + ).toBeInTheDocument() + }) + } finally { + scenario.cleanup() + } + }) }) describe('search and filtering', () => {