diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx index 6044956c06..b862a5de88 100644 --- a/src/components/Menu/Menu.tsx +++ b/src/components/Menu/Menu.tsx @@ -226,7 +226,7 @@ const Menu = ({ const scaleY = useSharedValue(0); const keyboardHeightRef = React.useRef(0); - const prevVisible = React.useRef(null); + const prevVisible = React.useRef(visible); const anchorRef = React.useRef(null); const menuRef = React.useRef(null); const isShownRef = React.useRef(false); diff --git a/src/components/__tests__/Menu.test.tsx b/src/components/__tests__/Menu.test.tsx index c35880b48f..90ff0d2df5 100644 --- a/src/components/__tests__/Menu.test.tsx +++ b/src/components/__tests__/Menu.test.tsx @@ -1,6 +1,13 @@ -import { Dimensions, StyleSheet, View } from 'react-native'; - -import { expect, it, jest } from '@jest/globals'; +import { Dimensions, Platform, StyleSheet, View } from 'react-native'; + +import { + afterEach, + beforeEach, + describe, + expect, + it, + jest, +} from '@jest/globals'; import { act, screen, waitFor } from '@testing-library/react-native'; import { render } from '../../test-utils'; @@ -10,6 +17,105 @@ import Button from '../Button/Button'; import Menu from '../Menu/Menu'; import Portal from '../Portal/Portal'; +describe('web focus', () => { + const focus = jest.fn(); + const originalPlatform = Platform.OS; + const originalHTMLElement = Object.getOwnPropertyDescriptor( + globalThis, + 'HTMLElement' + ); + const originalDocument = Object.getOwnPropertyDescriptor( + globalThis, + 'document' + ); + const originalQuerySelector = Object.getOwnPropertyDescriptor( + View.prototype, + 'querySelector' + ); + const originalMeasureInWindow = Object.getOwnPropertyDescriptor( + View.prototype, + 'measureInWindow' + ); + + beforeEach(() => { + Platform.OS = 'web'; + focus.mockClear(); + // Native View refs stand in for DOM elements in the native test renderer. + Object.defineProperty(globalThis, 'HTMLElement', { + configurable: true, + value: View, + }); + Object.defineProperty(globalThis, 'document', { + configurable: true, + value: { + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + }, + }); + Object.defineProperty(View.prototype, 'querySelector', { + configurable: true, + value: () => ({ focus }), + }); + const measureInWindow: View['measureInWindow'] = (fn) => + fn(100, 100, 80, 32); + Object.defineProperty(View.prototype, 'measureInWindow', { + configurable: true, + value: measureInWindow, + }); + }); + + afterEach(() => { + Platform.OS = originalPlatform; + for (const [target, key, descriptor] of [ + [globalThis, 'HTMLElement', originalHTMLElement], + [globalThis, 'document', originalDocument], + [View.prototype, 'querySelector', originalQuerySelector], + [View.prototype, 'measureInWindow', originalMeasureInWindow], + ] as const) { + if (descriptor) { + Object.defineProperty(target, key, descriptor); + } else { + Reflect.deleteProperty(target, key); + } + } + }); + + const makeMenu = (visible: boolean) => ( + + Open menu}> + + + + ); + + it('does not move focus when mounted hidden', async () => { + await render(makeMenu(false)); + await act(() => jest.advanceTimersByTime(300)); + + expect(focus).not.toHaveBeenCalled(); + }); + + it('moves focus when opened and restores it when closed', async () => { + const { rerender } = await render(makeMenu(false)); + await rerender(makeMenu(true)); + await act(() => jest.advanceTimersByTime(300)); + + expect(focus).toHaveBeenCalledTimes(1); + + await rerender(makeMenu(false)); + await act(() => jest.advanceTimersByTime(300)); + + expect(focus).toHaveBeenCalledTimes(2); + }); + + it('moves focus when mounted visible', async () => { + await render(makeMenu(true)); + await act(() => jest.advanceTimersByTime(300)); + + expect(focus).toHaveBeenCalledTimes(1); + }); +}); + it('renders visible menu', async () => { const tree = ( await render(