This is a minimal reproduction demonstrating that the Headless UI Dialog
component does not prevent the Escape key event from propagating to other keyboard event listeners when the dialog is closed.
When a Dialog is open and the user presses Escape:
- ✅ The Dialog closes correctly (via
onClose
) - ❌ Bug: The Escape key event also propagates to other global keyboard listeners
This is problematic in applications where other components listen for the Escape key (e.g., navigation back buttons, closing other UI elements, etc.).
When a Dialog is open and captures the Escape key to close:
- The Escape key event should be consumed by the Dialog
- Other keyboard listeners should NOT receive the Escape key event
- This is standard modal behavior - modals should capture keyboard events and prevent them from affecting the underlying page
The Dialog closes, but the Escape key event continues to propagate, triggering other keyboard event listeners in the application.
-
Install dependencies:
npm install
-
Start the dev server:
npm run dev
-
Open the application in your browser
-
Click "Open Drawer" to open the Dialog
-
Press the Escape key
-
Observe the Event Log:
- You'll see "Dialog closed via onClose" (expected)
- You'll also see "Global Escape listener triggered!" (bug)
In our production application, we have:
- A
Dialog
/Drawer
component for detail views - A "Back" button component that listens for Escape to navigate back
- When a drawer is open and the user presses Escape, both the drawer closes AND the back navigation is triggered, causing unwanted navigation
Currently, we have to add checks in every component that listens for Escape:
useEffect(() => {
const handleEscape = event => {
if (event.key === 'Escape') {
// Workaround: Check if a dialog is open
if (document.querySelector('[role="dialog"]')) {
return; // Don't handle if dialog is open
}
// Handle escape...
}
};
window.addEventListener('keydown', handleEscape);
return () => window.removeEventListener('keydown', handleEscape);
}, []);
This is not ideal because:
- It requires every Escape listener to know about dialogs
- It's easy to forget and leads to bugs
- It couples unrelated components
- The Dialog should handle this internally
The Dialog component should call event.stopPropagation()
and/or event.preventDefault()
when handling the Escape key, preventing it from reaching other listeners.
- @headlessui/react: 2.2.0
- React: 18.3.1
- Browser: All modern browsers
This is similar to how other modal libraries handle keyboard events - they consume the Escape key when closing to prevent it from affecting the rest of the application.