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
14 changes: 14 additions & 0 deletions packages/react-aria/src/overlays/useModalOverlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,23 @@ export function useModalOverlay(
state: OverlayTriggerState,
ref: RefObject<HTMLElement | null>
): ModalOverlayAria {
let {shouldCloseOnInteractOutside} = props;
let {overlayProps, underlayProps} = useOverlay(
{
...props,
shouldCloseOnInteractOutside: (element) => {
// A modal's underlay covers the viewport, so the document element is never a
// legitimate outside-press target. It only appears as one when the browser
// retargets a press whose original target was removed from the DOM mid-press
// (e.g. a button swapped out after an async mutation), and dismissing then
// would close the modal under the user's pointer. Non-modal overlays must keep
// treating it as valid: on pages with a short body, presses below the body
// target the document element and should still dismiss (see #1367).
if (element === element.ownerDocument.documentElement) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happened to the isConnected check you proposed?

return false;
}
return shouldCloseOnInteractOutside ? shouldCloseOnInteractOutside(element) : true;
},
isOpen: state.isOpen,
onClose: state.close
},
Expand Down
33 changes: 33 additions & 0 deletions packages/react-aria/test/overlays/useModalOverlay.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,38 @@ describe('useModalOverlay', function () {
fireEvent.click(document.body);
expect(onOpenChange).not.toHaveBeenCalled();
});

it('should not hide the overlay when a press is retargeted to the document element', function () {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move these tests up to the RAC level and demonstrate the use case you brought up?

// When the pressed element is removed from the DOM mid-press (e.g. a button swapped
// out after an async mutation), the browser retargets the press events to the
// document element. That must not be treated as a press outside the modal.
let onOpenChange = jest.fn();
render(
<Example
isOpen
onOpenChange={onOpenChange}
isDismissable />
);
pressStart(document.documentElement);
pressEnd(document.documentElement);
fireEvent.click(document.documentElement);
Comment on lines +77 to +79

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how is this pressing an element that is swapped mid press? it's clicking the documentElement in every event

expect(onOpenChange).not.toHaveBeenCalled();
});

it('should ignore a retargeted press even if shouldCloseOnInteractOutside returns true', function () {
let onOpenChange = jest.fn();
render(
<Example
isOpen
onOpenChange={onOpenChange}
isDismissable
shouldCloseOnInteractOutside={() => true}
/>
);
pressStart(document.documentElement);
pressEnd(document.documentElement);
fireEvent.click(document.documentElement);
expect(onOpenChange).not.toHaveBeenCalled();
});
});
});