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 4e2bf36a..78a8f5f3 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
@@ -995,12 +995,14 @@ export const PopupMenuSubmenuTrigger = React.forwardRef<
// Check if aim guard is blocking this trigger
if (
aimGuardActiveRef.current &&
+ guardedDepthRef.current === parentDepth &&
guardedTriggerIdRef.current !== item.id
) {
logAimTrace('pointerenter-blocked-by-guard', {
clientX: event.clientX,
clientY: event.clientY,
blockedByTriggerId: guardedTriggerIdRef.current,
+ blockedByDepth: guardedDepthRef.current,
})
return
}
@@ -1050,6 +1052,7 @@ export const PopupMenuSubmenuTrigger = React.forwardRef<
onPointerEnter,
disabled,
aimGuardActiveRef,
+ guardedDepthRef,
guardedTriggerIdRef,
item.id,
item.storeId,
@@ -1087,12 +1090,14 @@ export const PopupMenuSubmenuTrigger = React.forwardRef<
// Check if aim guard is blocking this trigger
if (
aimGuardActiveRef.current &&
+ guardedDepthRef.current === parentDepth &&
guardedTriggerIdRef.current !== item.id
) {
logAimTrace('pointerleave-blocked-by-guard', {
clientX: event.clientX,
clientY: event.clientY,
blockedByTriggerId: guardedTriggerIdRef.current,
+ blockedByDepth: guardedDepthRef.current,
})
return
}
@@ -1203,6 +1208,7 @@ export const PopupMenuSubmenuTrigger = React.forwardRef<
disabled,
clearOpenTimer,
aimGuardActiveRef,
+ guardedDepthRef,
guardedTriggerIdRef,
item.id,
item.storeId,
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 c06f424a..e51e1f05 100644
--- a/packages/react/src/internal/popup-menu/popup-menu.test.tsx
+++ b/packages/react/src/internal/popup-menu/popup-menu.test.tsx
@@ -783,6 +783,98 @@ describe('PopupMenu', () => {
popupRectSpy.mockRestore()
})
+ it('shows hover safe triangle for nested submenu trigger while parent guard is active', async () => {
+ const user = userEvent.setup()
+ render()
+
+ await user.click(screen.getByTestId('trigger'))
+
+ await waitFor(() => {
+ expect(screen.getByTestId('popup-root')).toBeInTheDocument()
+ })
+
+ const submenuTrigger1 = screen.getByTestId('submenu-trigger-1')
+
+ await user.hover(submenuTrigger1)
+
+ await waitFor(() => {
+ expect(screen.getByTestId('popup-submenu-1')).toBeInTheDocument()
+ })
+
+ const submenuPopup1 = screen.getByTestId('popup-submenu-1')
+ const trigger1RectSpy = vi
+ .spyOn(submenuTrigger1, 'getBoundingClientRect')
+ .mockImplementation(() =>
+ createRect({ top: 60, left: 80, width: 120, height: 30 }),
+ )
+ const popup1RectSpy = vi
+ .spyOn(submenuPopup1, 'getBoundingClientRect')
+ .mockImplementation(() =>
+ createRect({ top: 40, left: 240, width: 180, height: 160 }),
+ )
+
+ try {
+ // Activate aim guard for the root submenu trigger.
+ fireEvent.pointerMove(window, { clientX: 120, clientY: 90 })
+ fireEvent.pointerMove(window, { clientX: 150, clientY: 92 })
+ fireEvent.pointerMove(window, { clientX: 180, clientY: 94 })
+ fireEvent.pointerLeave(submenuTrigger1, { clientX: 190, clientY: 94 })
+ fireEvent.pointerMove(window, { clientX: 190, clientY: 94 })
+
+ await waitFor(() => {
+ expect(getSafeTriangle('activated')).toBeInTheDocument()
+ })
+
+ const submenuTrigger2 = screen.getByTestId('submenu-trigger-2')
+
+ fireEvent.pointerEnter(submenuTrigger2, {
+ pointerType: 'mouse',
+ clientX: 270,
+ clientY: 95,
+ })
+ fireEvent.pointerMove(submenuTrigger2, {
+ pointerType: 'mouse',
+ clientX: 270,
+ clientY: 95,
+ })
+
+ await waitFor(() => {
+ expect(screen.getByTestId('popup-submenu-2')).toBeInTheDocument()
+ })
+
+ const submenuPopup2 = screen.getByTestId('popup-submenu-2')
+ const trigger2RectSpy = vi
+ .spyOn(submenuTrigger2, 'getBoundingClientRect')
+ .mockImplementation(() =>
+ createRect({ top: 90, left: 250, width: 120, height: 30 }),
+ )
+ const popup2RectSpy = vi
+ .spyOn(submenuPopup2, 'getBoundingClientRect')
+ .mockImplementation(() =>
+ createRect({ top: 70, left: 430, width: 180, height: 160 }),
+ )
+
+ try {
+ // Force a position update after geometry overrides are in place.
+ fireEvent.pointerMove(window, {
+ pointerType: 'mouse',
+ clientX: 270,
+ clientY: 95,
+ })
+
+ await waitFor(() => {
+ expect(getSafeTriangle('hover')).toBeInTheDocument()
+ })
+ } finally {
+ trigger2RectSpy.mockRestore()
+ popup2RectSpy.mockRestore()
+ }
+ } finally {
+ trigger1RectSpy.mockRestore()
+ popup1RectSpy.mockRestore()
+ }
+ })
+
it('renders the safe triangle in green when aim guard is activated on leave', async () => {
const user = userEvent.setup()
render()