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(() => {