diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0263eca7f7..296b954b1c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixes
- Only add `type=button` to real buttons ([#709](https://github.com/tailwindlabs/headlessui/pull/709))
+- Fix `escape` bug not closing Dialog after clicking in Dialog ([#754](https://github.com/tailwindlabs/headlessui/pull/754))
## [Unreleased - Vue]
@@ -17,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Only add `type=button` to real buttons ([#709](https://github.com/tailwindlabs/headlessui/pull/709))
- Add Vue emit types ([#679](https://github.com/tailwindlabs/headlessui/pull/679), [#712](https://github.com/tailwindlabs/headlessui/pull/712))
+- Fix `escape` bug not closing Dialog after clicking in Dialog ([#754](https://github.com/tailwindlabs/headlessui/pull/754))
## [@headlessui/react@v1.4.0] - 2021-07-29
diff --git a/packages/@headlessui-react/src/components/dialog/dialog.test.tsx b/packages/@headlessui-react/src/components/dialog/dialog.test.tsx
index 6fa61270ef..fcdc88f0f2 100644
--- a/packages/@headlessui-react/src/components/dialog/dialog.test.tsx
+++ b/packages/@headlessui-react/src/components/dialog/dialog.test.tsx
@@ -430,6 +430,90 @@ describe('Keyboard interactions', () => {
assertDialog({ state: DialogState.InvisibleUnmounted })
})
)
+
+ it(
+ 'should be possible to close the dialog with Escape, when a field is focused',
+ suppressConsoleLogs(async () => {
+ function Example() {
+ let [isOpen, setIsOpen] = useState(false)
+ return (
+ <>
+
+
+ >
+ )
+ }
+ render()
+
+ assertDialog({ state: DialogState.InvisibleUnmounted })
+
+ // Open dialog
+ await click(document.getElementById('trigger'))
+
+ // Verify it is open
+ assertDialog({
+ state: DialogState.Visible,
+ attributes: { id: 'headlessui-dialog-1' },
+ })
+
+ // Close dialog
+ await press(Keys.Escape)
+
+ // Verify it is close
+ assertDialog({ state: DialogState.InvisibleUnmounted })
+ })
+ )
+
+ it(
+ 'should not be possible to close the dialog with Escape, when a field is focused but cancels the event',
+ suppressConsoleLogs(async () => {
+ function Example() {
+ let [isOpen, setIsOpen] = useState(false)
+ return (
+ <>
+
+
+ >
+ )
+ }
+ render()
+
+ assertDialog({ state: DialogState.InvisibleUnmounted })
+
+ // Open dialog
+ await click(document.getElementById('trigger'))
+
+ // Verify it is open
+ assertDialog({
+ state: DialogState.Visible,
+ attributes: { id: 'headlessui-dialog-1' },
+ })
+
+ // Try to close the dialog
+ await press(Keys.Escape)
+
+ // Verify it is still open
+ assertDialog({ state: DialogState.Visible })
+ })
+ )
})
})
diff --git a/packages/@headlessui-react/src/components/dialog/dialog.tsx b/packages/@headlessui-react/src/components/dialog/dialog.tsx
index 2055bd4ac6..839ca69087 100644
--- a/packages/@headlessui-react/src/components/dialog/dialog.tsx
+++ b/packages/@headlessui-react/src/components/dialog/dialog.tsx
@@ -7,15 +7,14 @@ import React, {
useMemo,
useReducer,
useRef,
+ useState,
// Types
ContextType,
ElementType,
MouseEvent as ReactMouseEvent,
- KeyboardEvent as ReactKeyboardEvent,
MutableRefObject,
Ref,
- useState,
} from 'react'
import { Props } from '../../types'
@@ -217,6 +216,16 @@ let DialogRoot = forwardRefWithAs(function Dialog<
close()
})
+ // Handle `Escape` to close
+ useWindowEvent('keydown', event => {
+ if (event.key !== Keys.Escape) return
+ if (dialogState !== DialogStates.Open) return
+ if (hasNestedDialogs) return
+ event.preventDefault()
+ event.stopPropagation()
+ close()
+ })
+
// Scroll lock
useEffect(() => {
if (dialogState !== DialogStates.Open) return
@@ -282,16 +291,6 @@ let DialogRoot = forwardRefWithAs(function Dialog<
onClick(event: ReactMouseEvent) {
event.stopPropagation()
},
-
- // Handle `Escape` to close
- onKeyDown(event: ReactKeyboardEvent) {
- if (event.key !== Keys.Escape) return
- if (dialogState !== DialogStates.Open) return
- if (hasNestedDialogs) return
- event.preventDefault()
- event.stopPropagation()
- close()
- },
}
let passthroughProps = rest
diff --git a/packages/@headlessui-vue/src/components/dialog/dialog.test.ts b/packages/@headlessui-vue/src/components/dialog/dialog.test.ts
index e01d98ee9c..e706b1d5fa 100644
--- a/packages/@headlessui-vue/src/components/dialog/dialog.test.ts
+++ b/packages/@headlessui-vue/src/components/dialog/dialog.test.ts
@@ -526,6 +526,111 @@ describe('Keyboard interactions', () => {
assertDialog({ state: DialogState.InvisibleUnmounted })
})
)
+
+ it(
+ 'should be possible to close the dialog with Escape, when a field is focused',
+ suppressConsoleLogs(async () => {
+ renderTemplate({
+ template: `
+
+
+
+
+ `,
+ setup() {
+ let isOpen = ref(false)
+ return {
+ isOpen,
+ setIsOpen(value: boolean) {
+ isOpen.value = value
+ },
+ toggleOpen() {
+ isOpen.value = !isOpen.value
+ },
+ }
+ },
+ })
+
+ assertDialog({ state: DialogState.InvisibleUnmounted })
+
+ // Open dialog
+ await click(document.getElementById('trigger'))
+
+ // Verify it is open
+ assertDialog({
+ state: DialogState.Visible,
+ attributes: { id: 'headlessui-dialog-1' },
+ })
+
+ // Close dialog
+ await press(Keys.Escape)
+
+ // Verify it is close
+ assertDialog({ state: DialogState.InvisibleUnmounted })
+ })
+ )
+
+ it(
+ 'should not be possible to close the dialog with Escape, when a field is focused but cancels the event',
+ suppressConsoleLogs(async () => {
+ renderTemplate({
+ template: `
+
+
+
+
+ `,
+ setup() {
+ let isOpen = ref(false)
+ return {
+ isOpen,
+ setIsOpen(value: boolean) {
+ isOpen.value = value
+ },
+ toggleOpen() {
+ isOpen.value = !isOpen.value
+ },
+ cancel(event: KeyboardEvent) {
+ event.preventDefault()
+ event.stopPropagation()
+ },
+ }
+ },
+ })
+
+ assertDialog({ state: DialogState.InvisibleUnmounted })
+
+ // Open dialog
+ await click(document.getElementById('trigger'))
+
+ // Verify it is open
+ assertDialog({
+ state: DialogState.Visible,
+ attributes: { id: 'headlessui-dialog-1' },
+ })
+
+ // Try to close the dialog
+ await press(Keys.Escape)
+
+ // Verify it is still open
+ assertDialog({ state: DialogState.Visible })
+ })
+ )
})
})
diff --git a/packages/@headlessui-vue/src/components/dialog/dialog.ts b/packages/@headlessui-vue/src/components/dialog/dialog.ts
index 346e7deaa3..d2a7f84ad3 100644
--- a/packages/@headlessui-vue/src/components/dialog/dialog.ts
+++ b/packages/@headlessui-vue/src/components/dialog/dialog.ts
@@ -87,7 +87,6 @@ export let Dialog = defineComponent({
'aria-labelledby': this.titleId,
'aria-describedby': this.describedby,
onClick: this.handleClick,
- onKeydown: this.handleKeyDown,
}
let { open: _, initialFocus, ...passThroughProps } = this.$props
@@ -205,6 +204,16 @@ export let Dialog = defineComponent({
nextTick(() => target?.focus())
})
+ // Handle `Escape` to close
+ useWindowEvent('keydown', event => {
+ 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()
+ })
+
// Scroll lock
watchEffect(onInvalidate => {
if (dialogState.value !== DialogStates.Open) return
@@ -260,16 +269,6 @@ export let Dialog = defineComponent({
handleClick(event: MouseEvent) {
event.stopPropagation()
},
-
- // Handle `Escape` to close
- handleKeyDown(event: KeyboardEvent) {
- 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()
- },
}
},
})