diff --git a/packages/react/src/dialog/root/DialogRoot.test.tsx b/packages/react/src/dialog/root/DialogRoot.test.tsx index 08ff285fd0..e960fcc5d6 100644 --- a/packages/react/src/dialog/root/DialogRoot.test.tsx +++ b/packages/react/src/dialog/root/DialogRoot.test.tsx @@ -337,6 +337,36 @@ describe('', () => { }); expect(handleOpenChange.callCount).to.equal(1); }); + + it('dismisses when clicking a descendant element within the user backdrop', async () => { + const handleOpenChange = spy(); + + const { queryByRole } = await render( + + + + Child + + + + , + ); + + const backdropChild = screen.getByTestId('backdrop-child'); + + fireEvent.mouseDown(backdropChild); + + expect(queryByRole('dialog')).not.to.equal(null); + expect(handleOpenChange.callCount).to.equal(0); + + fireEvent.click(backdropChild); + + await waitFor(() => { + expect(queryByRole('dialog')).to.equal(null); + }); + expect(handleOpenChange.callCount).to.equal(1); + expect(handleOpenChange.firstCall.args[1].reason).to.equal('outside-press'); + }); }); it('waits for the exit transition to finish before unmounting', async ({ skip }) => { diff --git a/packages/react/src/dialog/root/useDialogRoot.ts b/packages/react/src/dialog/root/useDialogRoot.ts index 383872680a..51c266cfdf 100644 --- a/packages/react/src/dialog/root/useDialogRoot.ts +++ b/packages/react/src/dialog/root/useDialogRoot.ts @@ -12,7 +12,7 @@ import { useInteractions, useRole, } from '../../floating-ui-react'; -import { getTarget } from '../../floating-ui-react/utils'; +import { contains, getTarget } from '../../floating-ui-react/utils'; import { useScrollLock } from '../../utils/useScrollLock'; import { useTransitionStatus, type TransitionStatus } from '../../utils/useTransitionStatus'; import type { RequiredExcept, HTMLProps, FloatingUIOpenChangeDetails } from '../../utils/types'; @@ -134,14 +134,15 @@ export function useDialogRoot(params: useDialogRoot.Parameters): useDialogRoot.R } const target = getTarget(event) as Element | null; if (isTopmost && dismissible) { - const eventTarget = target as Element | null; // Only close if the click occurred on the dialog's owning backdrop. // This supports multiple modal dialogs that aren't nested in the React tree: // https://github.com/mui/base-ui/issues/1320 if (modal) { - return internalBackdropRef.current || backdropRef.current - ? internalBackdropRef.current === eventTarget || backdropRef.current === eventTarget - : true; + const internalBackdrop = internalBackdropRef.current; + const userBackdrop = backdropRef.current; + if (internalBackdrop || userBackdrop) { + return contains(internalBackdrop, target) || contains(userBackdrop, target); + } } return true; }