diff --git a/packages/@react-aria/interactions/src/focusSafely.ts b/packages/@react-aria/interactions/src/focusSafely.ts index 384f3f363f7..cd171d34950 100644 --- a/packages/@react-aria/interactions/src/focusSafely.ts +++ b/packages/@react-aria/interactions/src/focusSafely.ts @@ -30,12 +30,12 @@ export function focusSafely(element: FocusableElement): void { // causing the page to scroll when moving focus if the element is transitioning // from off the screen. const ownerDocument = getOwnerDocument(element); - const activeElement = getActiveElement(ownerDocument); if (getInteractionModality() === 'virtual') { - let lastFocusedElement = activeElement; + let lastFocusedElement = getActiveElement(ownerDocument); runAfterTransition(() => { - // If focus did not move and the element is still in the document, focus it. - if (getActiveElement(ownerDocument) === lastFocusedElement && element.isConnected) { + const activeElement = getActiveElement(ownerDocument); + // If focus did not move or focus was lost to the body, and the element is still in the document, focus it. + if ((activeElement === lastFocusedElement || activeElement === ownerDocument.body) && element.isConnected) { focusWithoutScrolling(element); } }); diff --git a/packages/react-aria-components/test/Dialog.test.js b/packages/react-aria-components/test/Dialog.test.js index c047f886513..4bd6c76f1a6 100644 --- a/packages/react-aria-components/test/Dialog.test.js +++ b/packages/react-aria-components/test/Dialog.test.js @@ -10,17 +10,23 @@ * governing permissions and limitations under the License. */ +import {act, pointerMap, render, within} from '@react-spectrum/test-utils-internal'; import { Button, Dialog, DialogTrigger, Heading, + Input, + Label, + Menu, + MenuItem, + MenuTrigger, Modal, ModalOverlay, OverlayArrow, - Popover + Popover, + TextField } from '../'; -import {pointerMap, render, within} from '@react-spectrum/test-utils-internal'; import React, {useRef} from 'react'; import {UNSAFE_PortalProvider} from '@react-aria/overlays'; import userEvent from '@testing-library/user-event'; @@ -29,6 +35,7 @@ describe('Dialog', () => { let user; beforeAll(() => { user = userEvent.setup({delay: null, pointerMap}); + jest.useFakeTimers(); }); it('should have a base default set of attributes', () => { @@ -379,4 +386,59 @@ describe('Dialog', () => { await user.click(document.body); }); }); + + it('ensure Input autoFocus works when opening Modal from MenuItem via keyboard', async () => { + function App() { + const [isOpen, setOpen] = React.useState(false); + return ( + <> + + + + + setOpen(true)}>Add account + Sign out + + + + + + +
+ Sign up + + + + + + + + +
+
+
+
+ + ); + } + + const {getAllByRole, getByRole, getByTestId} = render(); + const button = getByRole('button'); + await user.tab(); + expect(document.activeElement).toBe(button); + await user.keyboard('{Enter}'); + act(() => { + jest.runAllTimers(); + }); + + const menuitem = getAllByRole('menuitem')[0]; + expect(document.activeElement).toBe(menuitem); + await user.keyboard('{Enter}'); + act(() => { + jest.runAllTimers(); + }); + + const input = getByTestId('email'); + expect(document.activeElement).toBe(input); + }); });