From 552cfed4c93f72e853db9c7c94f50d1c40b5fe0c Mon Sep 17 00:00:00 2001 From: Brad Stiff Date: Thu, 15 Jan 2026 14:09:53 -0700 Subject: [PATCH 1/3] (fix): defer unmounting Portal until hide animation finishes --- src/components/Menu/Menu.tsx | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx index 55922c1fc2..e55ab6723f 100644 --- a/src/components/Menu/Menu.tsx +++ b/src/components/Menu/Menu.tsx @@ -397,7 +397,7 @@ const Menu = ({ return; } - if (!display && prevRendered.current) { + if (!display) { hide(); } @@ -432,15 +432,23 @@ const Menu = ({ if (prevVisible.current !== visible) { prevVisible.current = visible; - if (visible !== rendered) { - setRendered(visible); + if (visible) { + if (!rendered) { + // Mount the Portal before attempting to show. + setRendered(true); + } + } else { + // Keep the Portal mounted so the hide animation can finish. + updateVisibility(false); } } - }, [visible, rendered]); + }, [visible, rendered, updateVisibility]); React.useEffect(() => { - updateVisibility(rendered); - }, [rendered, updateVisibility]); + if (rendered && visible) { + updateVisibility(true); + } + }, [rendered, visible, updateVisibility]); // I don't know why but on Android measure function is wrong by 24 const additionalVerticalValue = Platform.select({ @@ -641,6 +649,7 @@ const Menu = ({ accessibilityLabel={overlayAccessibilityLabel} accessibilityRole="button" onPress={onDismiss} + pointerEvents={visible ? 'auto' : 'none'} style={styles.pressableOverlay} /> Date: Thu, 15 Jan 2026 16:03:17 -0700 Subject: [PATCH 2/3] fix(menu): wait a tick before ensuring the component --- src/components/__tests__/Menu.test.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/__tests__/Menu.test.tsx b/src/components/__tests__/Menu.test.tsx index b8f7c0d7fe..3ee3547164 100644 --- a/src/components/__tests__/Menu.test.tsx +++ b/src/components/__tests__/Menu.test.tsx @@ -123,7 +123,11 @@ it('uses the default anchorPosition of top', async () => { // componentDidUpdate isn't called by default in jest. Forcing the update // than triggers measureInWindow, which is how Menu decides where to show // itself. - screen.update(makeMenu(true)); + await act(async () => { + screen.update(makeMenu(true)); + // Menu waits a tick for Portal refs to be up-to-date. + await Promise.resolve(); + }); await waitFor(() => { const menu = screen.getByTestId('menu-view'); @@ -163,7 +167,11 @@ it('respects anchorPosition bottom', async () => { .spyOn(View.prototype, 'measureInWindow') .mockImplementation((fn) => fn(100, 100, 80, 32)); - screen.update(makeMenu(true)); + await act(async () => { + screen.update(makeMenu(true)); + // Menu waits a tick for Portal refs to be up-to-date. + await Promise.resolve(); + }); await waitFor(() => { const menu = screen.getByTestId('menu-view'); From 7438feb4b765ea8cc0091a09ff2881b660170a10 Mon Sep 17 00:00:00 2001 From: Brad Stiff Date: Thu, 15 Jan 2026 16:03:41 -0700 Subject: [PATCH 3/3] fix(menu): add pointerEvents to snapshot styles --- src/components/__tests__/__snapshots__/Menu.test.tsx.snap | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/__tests__/__snapshots__/Menu.test.tsx.snap b/src/components/__tests__/__snapshots__/Menu.test.tsx.snap index 801552dab1..fc3ac61473 100644 --- a/src/components/__tests__/__snapshots__/Menu.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Menu.test.tsx.snap @@ -209,6 +209,7 @@ exports[`renders menu with content styles 1`] = ` onResponderTerminate={[Function]} onResponderTerminationRequest={[Function]} onStartShouldSetResponder={[Function]} + pointerEvents="auto" style={ { "bottom": 0, @@ -933,6 +934,7 @@ exports[`renders visible menu 1`] = ` onResponderTerminate={[Function]} onResponderTerminationRequest={[Function]} onStartShouldSetResponder={[Function]} + pointerEvents="auto" style={ { "bottom": 0,