diff --git a/packages/@headlessui-react/src/components/dialog/dialog.test.tsx b/packages/@headlessui-react/src/components/dialog/dialog.test.tsx
index f9d4a6bcf6..eb4e7a9282 100644
--- a/packages/@headlessui-react/src/components/dialog/dialog.test.tsx
+++ b/packages/@headlessui-react/src/components/dialog/dialog.test.tsx
@@ -496,4 +496,74 @@ describe('Mouse interactions', () => {
assertActiveElement(getByText('Hello'))
})
)
+
+ it(
+ 'should stop propagating click events when clicking on the Dialog.Overlay',
+ suppressConsoleLogs(async () => {
+ let wrapperFn = jest.fn()
+ function Example() {
+ let [isOpen, setIsOpen] = useState(true)
+ return (
+
+
+
+ )
+ }
+ render()
+
+ // Verify it is open
+ assertDialog({ state: DialogState.Visible })
+
+ // Verify that the wrapper function has not been called yet
+ expect(wrapperFn).toHaveBeenCalledTimes(0)
+
+ // Click the Dialog.Overlay to close the Dialog
+ await click(getDialogOverlay())
+
+ // Verify it is closed
+ assertDialog({ state: DialogState.InvisibleUnmounted })
+
+ // Verify that the wrapper function has not been called yet
+ expect(wrapperFn).toHaveBeenCalledTimes(0)
+ })
+ )
+
+ it(
+ 'should stop propagating click events when clicking on an element inside the Dialog',
+ suppressConsoleLogs(async () => {
+ let wrapperFn = jest.fn()
+ function Example() {
+ let [isOpen, setIsOpen] = useState(true)
+ return (
+
+
+
+ )
+ }
+ render()
+
+ // Verify it is open
+ assertDialog({ state: DialogState.Visible })
+
+ // Verify that the wrapper function has not been called yet
+ expect(wrapperFn).toHaveBeenCalledTimes(0)
+
+ // Click the button inside the the Dialog
+ await click(getByText('Inside'))
+
+ // Verify it is closed
+ assertDialog({ state: DialogState.InvisibleUnmounted })
+
+ // Verify that the wrapper function has not been called yet
+ expect(wrapperFn).toHaveBeenCalledTimes(0)
+ })
+ )
})
diff --git a/packages/@headlessui-react/src/components/dialog/dialog.tsx b/packages/@headlessui-react/src/components/dialog/dialog.tsx
index 32d2fcf9f1..f5388912f4 100644
--- a/packages/@headlessui-react/src/components/dialog/dialog.tsx
+++ b/packages/@headlessui-react/src/components/dialog/dialog.tsx
@@ -92,7 +92,13 @@ let DEFAULT_DIALOG_TAG = 'div' as const
interface DialogRenderPropArg {
open: boolean
}
-type DialogPropsWeControl = 'id' | 'role' | 'aria-modal' | 'aria-describedby' | 'aria-labelledby'
+type DialogPropsWeControl =
+ | 'id'
+ | 'role'
+ | 'aria-modal'
+ | 'aria-describedby'
+ | 'aria-labelledby'
+ | 'onClick'
let DialogRenderFeatures = Features.RenderStrategy | Features.Static
@@ -176,6 +182,8 @@ let DialogRoot = forwardRefWithAs(function Dialog<
if (event.key !== Keys.Escape) return
if (dialogState !== DialogStates.Open) return
if (containers.current.size > 1) return // 1 is myself, otherwise other elements in the Stack
+ event.preventDefault()
+ event.stopPropagation()
close()
})
@@ -243,6 +251,10 @@ let DialogRoot = forwardRefWithAs(function Dialog<
'aria-modal': dialogState === DialogStates.Open ? true : undefined,
'aria-labelledby': state.titleId,
'aria-describedby': describedby,
+ onClick(event: ReactMouseEvent) {
+ event.preventDefault()
+ event.stopPropagation()
+ },
}
let passthroughProps = rest
@@ -302,6 +314,8 @@ let Overlay = forwardRefWithAs(function Overlay<
let handleClick = useCallback(
(event: ReactMouseEvent) => {
if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault()
+ event.preventDefault()
+ event.stopPropagation()
close()
},
[close]
diff --git a/packages/@headlessui-vue/src/components/dialog/dialog.test.ts b/packages/@headlessui-vue/src/components/dialog/dialog.test.ts
index 07bfe0d43f..ec91b014ab 100644
--- a/packages/@headlessui-vue/src/components/dialog/dialog.test.ts
+++ b/packages/@headlessui-vue/src/components/dialog/dialog.test.ts
@@ -607,4 +607,90 @@ describe('Mouse interactions', () => {
assertActiveElement(getByText('Hello'))
})
)
+
+ it(
+ 'should stop propagating click events when clicking on the Dialog.Overlay',
+ suppressConsoleLogs(async () => {
+ let wrapperFn = jest.fn()
+ renderTemplate({
+ template: `
+
+
+
+ `,
+ setup() {
+ let isOpen = ref(true)
+ return {
+ isOpen,
+ wrapperFn,
+ setIsOpen(value: boolean) {
+ isOpen.value = value
+ },
+ }
+ },
+ })
+
+ // Verify it is open
+ assertDialog({ state: DialogState.Visible })
+
+ // Verify that the wrapper function has not been called yet
+ expect(wrapperFn).toHaveBeenCalledTimes(0)
+
+ // Click the Dialog.Overlay to close the Dialog
+ await click(getDialogOverlay())
+
+ // Verify it is closed
+ assertDialog({ state: DialogState.InvisibleUnmounted })
+
+ // Verify that the wrapper function has not been called yet
+ expect(wrapperFn).toHaveBeenCalledTimes(0)
+ })
+ )
+
+ it(
+ 'should stop propagating click events when clicking on an element inside the Dialog',
+ suppressConsoleLogs(async () => {
+ let wrapperFn = jest.fn()
+ renderTemplate({
+ template: `
+
+
+
+ `,
+ setup() {
+ let isOpen = ref(true)
+ return {
+ isOpen,
+ wrapperFn,
+ setIsOpen(value: boolean) {
+ isOpen.value = value
+ },
+ }
+ },
+ })
+
+ // Verify it is open
+ assertDialog({ state: DialogState.Visible })
+
+ // Verify that the wrapper function has not been called yet
+ expect(wrapperFn).toHaveBeenCalledTimes(0)
+
+ // Click the button inside the the Dialog
+ await click(getByText('Inside'))
+
+ // Verify it is closed
+ assertDialog({ state: DialogState.InvisibleUnmounted })
+
+ // Verify that the wrapper function has not been called yet
+ expect(wrapperFn).toHaveBeenCalledTimes(0)
+ })
+ )
})
diff --git a/packages/@headlessui-vue/src/components/dialog/dialog.ts b/packages/@headlessui-vue/src/components/dialog/dialog.ts
index 79c379ecc5..0e73eb62da 100644
--- a/packages/@headlessui-vue/src/components/dialog/dialog.ts
+++ b/packages/@headlessui-vue/src/components/dialog/dialog.ts
@@ -85,6 +85,7 @@ export let Dialog = defineComponent({
'aria-modal': this.dialogState === DialogStates.Open ? true : undefined,
'aria-labelledby': this.titleId,
'aria-describedby': this.describedby,
+ onClick: this.handleClick,
}
let { open, initialFocus, ...passThroughProps } = this.$props
let slot = { open: this.dialogState === DialogStates.Open }
@@ -188,6 +189,8 @@ export let Dialog = defineComponent({
if (event.key !== Keys.Escape) return
if (dialogState.value !== DialogStates.Open) return
if (containers.value.size > 1) return // 1 is myself, otherwise other elements in the Stack
+ event.preventDefault()
+ event.stopPropagation()
api.close()
})
@@ -241,6 +244,10 @@ export let Dialog = defineComponent({
dialogState,
titleId,
describedby,
+ handleClick(event: MouseEvent) {
+ event.preventDefault()
+ event.stopPropagation()
+ },
}
},
})
@@ -276,7 +283,9 @@ export let DialogOverlay = defineComponent({
return {
id,
- handleClick() {
+ handleClick(event: MouseEvent) {
+ event.preventDefault()
+ event.stopPropagation()
api.close()
},
}
diff --git a/packages/@headlessui-vue/src/test-utils/vue-testing-library.ts b/packages/@headlessui-vue/src/test-utils/vue-testing-library.ts
index d3b596ac78..81eb7bbb66 100644
--- a/packages/@headlessui-vue/src/test-utils/vue-testing-library.ts
+++ b/packages/@headlessui-vue/src/test-utils/vue-testing-library.ts
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
-import { logDOM, fireEvent } from '@testing-library/dom'
+import { logDOM, fireEvent, screen } from '@testing-library/dom'
let mountedWrappers = new Set()
@@ -58,4 +58,4 @@ if (typeof afterEach === 'function') {
afterEach(() => cleanup())
}
-export { fireEvent }
+export { fireEvent, screen }