diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx index ca6e849e3d..1f8fb89d58 100644 --- a/src/components/Menu/Menu.tsx +++ b/src/components/Menu/Menu.tsx @@ -359,9 +359,11 @@ const Menu = ({ easing: EASING, useNativeDriver: true, }), - ]).start(() => { - focusFirstDOMNode(menuRef.current); - prevRendered.current = true; + ]).start(({ finished }) => { + if (finished) { + focusFirstDOMNode(menuRef.current); + prevRendered.current = true; + } }); }); }, [anchor, attachListeners, measureAnchorLayout, theme]); @@ -371,6 +373,12 @@ const Menu = ({ const { animation } = theme; + // Reset before the animation starts so that the menu can be re-opened + // even if the hide animation's completion callback never fires (e.g. on + // React Native >= 0.80 where the animation `finished` event is not + // delivered). Without this the menu stays closed and cannot be shown again. + prevRendered.current = false; + Animated.timing(opacityAnimationRef.current, { toValue: 0, duration: ANIMATION_DURATION * animation.scale, @@ -379,7 +387,6 @@ const Menu = ({ }).start(() => { setMenuLayout({ width: 0, height: 0 }); setRendered(false); - prevRendered.current = false; focusFirstDOMNode(anchorRef.current); }); }, [removeListeners, theme]); diff --git a/src/components/__tests__/Menu.test.tsx b/src/components/__tests__/Menu.test.tsx index 128fb87cdb..13d74e0ffc 100644 --- a/src/components/__tests__/Menu.test.tsx +++ b/src/components/__tests__/Menu.test.tsx @@ -290,3 +290,76 @@ it('renders menu with mode "flat"', async () => { expect(styles).not.toHaveProperty('shadowColor'); expect(styles).not.toHaveProperty('shadowOpacity'); }); + +it('reopens after being closed', async () => { + const dimensionsSpy = jest.spyOn(Dimensions, 'get').mockReturnValue({ + width: 400, + height: 800, + scale: 2, + fontScale: 2, + }); + const measureSpy = jest + .spyOn(View.prototype, 'measureInWindow') + .mockImplementation((fn) => fn(100, 100, 80, 32)); + + function makeMenu(visible: boolean) { + return ( + + + Open menu + + } + contentStyle={styles.contentStyle} + > + + + + + ); + } + + const { rerender } = await render(makeMenu(false)); + + // Open the menu. + await act(async () => { + await rerender(makeMenu(true)); + // Menu waits a tick for Portal refs to be up-to-date. + await Promise.resolve(); + }); + + await waitFor(() => { + expect(screen.getByTestId('menu-view')).toHaveStyle({ + position: 'absolute', + left: 100, + top: 100, + }); + }); + + // Close the menu. + await act(async () => { + await rerender(makeMenu(false)); + await Promise.resolve(); + }); + + // Reopen the menu. If `prevRendered` is not reset on close, the menu + // would stay hidden forever (see fix for #4763). + await act(async () => { + await rerender(makeMenu(true)); + await Promise.resolve(); + }); + + await waitFor(() => { + expect(screen.getByTestId('menu-view')).toHaveStyle({ + position: 'absolute', + left: 100, + top: 100, + }); + }); + + measureSpy.mockRestore(); + dimensionsSpy.mockRestore(); +});