Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions packages/@adobe/react-spectrum/src/dialog/DialogContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -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) {
Expand Down Expand Up @@ -83,7 +97,8 @@ export function DialogContainer(props: SpectrumDialogContainerProps): JSX.Elemen
state={state}
type={type}
isDismissable={isDismissable}
isKeyboardDismissDisabled={isKeyboardDismissDisabled}>
isKeyboardDismissDisabled={isKeyboardDismissDisabled}
disableFocusManagement={disableFocusManagement}>
<DialogContext.Provider value={context}>{lastChild}</DialogContext.Provider>
</Modal>
);
Expand Down
39 changes: 39 additions & 0 deletions packages/@adobe/react-spectrum/test/dialog/DialogContainer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Provider theme={theme}>
<DialogContainerExample disableFocusManagement />
</Provider>
);

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