diff --git a/packages/@adobe/react-spectrum/src/dialog/DialogContainer.tsx b/packages/@adobe/react-spectrum/src/dialog/DialogContainer.tsx index 7891562c6df..3080acf84fb 100644 --- a/packages/@adobe/react-spectrum/src/dialog/DialogContainer.tsx +++ b/packages/@adobe/react-spectrum/src/dialog/DialogContainer.tsx @@ -33,6 +33,13 @@ export interface SpectrumDialogContainerProps { isDismissable?: boolean; /** Whether pressing the escape key to close the dialog should be disabled. */ isKeyboardDismissDisabled?: boolean; + /** + * Whether to disable focus management for the dialog, including focus containment + * and restoration. This option should be used very carefully. When focus management + * is disabled, you must implement focus containment and restoration to ensure the + * dialog is keyboard accessible. + */ + disableFocusManagement?: boolean; } /** @@ -41,7 +48,14 @@ export interface SpectrumDialogContainerProps { * or when the trigger unmounts while the dialog is open. */ export function DialogContainer(props: SpectrumDialogContainerProps): JSX.Element { - let {children, type = 'modal', onDismiss, isDismissable, isKeyboardDismissDisabled} = props; + let { + children, + type = 'modal', + onDismiss, + isDismissable, + isKeyboardDismissDisabled, + disableFocusManagement + } = props; let childArray = React.Children.toArray(children); if (childArray.length > 1) { @@ -83,7 +97,8 @@ export function DialogContainer(props: SpectrumDialogContainerProps): JSX.Elemen state={state} type={type} isDismissable={isDismissable} - isKeyboardDismissDisabled={isKeyboardDismissDisabled}> + isKeyboardDismissDisabled={isKeyboardDismissDisabled} + disableFocusManagement={disableFocusManagement}> {lastChild} ); diff --git a/packages/@adobe/react-spectrum/test/dialog/DialogContainer.test.js b/packages/@adobe/react-spectrum/test/dialog/DialogContainer.test.js index b7b21227fa9..327f7617918 100644 --- a/packages/@adobe/react-spectrum/test/dialog/DialogContainer.test.js +++ b/packages/@adobe/react-spectrum/test/dialog/DialogContainer.test.js @@ -272,6 +272,45 @@ describe('DialogContainer', function () { expect(document.activeElement).toBe(button); }); + it('should not restore focus to the trigger on close when disableFocusManagement is set', async function () { + // Given a DialogContainer rendered with disableFocusManagement + // When the dialog is opened via the trigger and then dismissed + // Then focus is NOT automatically restored to the trigger (FocusScope's + // restoreFocus is skipped, unlike the default behavior asserted by the + // "should be able to have dialogs open dialogs and still restore focus" test above). + let user = userEvent.setup({delay: null, pointerMap}); + let {getByRole, queryByRole} = render( + + + + ); + + let button = getByRole('button'); + expect(queryByRole('dialog')).toBeNull(); + + await user.click(button); + act(() => { + jest.runAllTimers(); + }); + + let dialog = getByRole('dialog'); + let confirmButton = within(dialog).getByText('Confirm'); + + await user.click(confirmButton); + act(() => { + jest.runAllTimers(); + }); + // A second timer flush lets the exit transition + FocusScope unmount/restore + // effect fully settle (matches the pattern used by the nested-dialog restore + // focus test above). + act(() => { + jest.runAllTimers(); + }); + + expect(queryByRole('dialog')).toBeNull(); + expect(document.activeElement).not.toBe(button); + }); + describe('portalContainer', () => { let user; beforeAll(() => {