Overlay: Events bubbling up to the page can conflict with global shortcuts
See original GitHub issuevia @dusave on slack: Is there a way to stop propagation of Escape when an anchored overlay is open? I just want the anchored overlay to close, but it’s also bubbling up outside of the anchored overlay
Outdated repo (This feature was removed in memex, watch this video instead)
https://github.com/orgs/github/projects/4205/settings/fields/55540
- Click on the dates of a given iteration, make the date picker show
- Hit Escape
- Notice it closes the date picker and settings. We only want it to close the date picker
Update:
Problem:
By default, pressing Escape in an Overlay bubbles up to the page outside the Overlay. Adding event.stopPropagation
inside onEscape
does not stop the events from bubbling up because of the way the handlers are wired.
const Page = () => {
React.useEffect(() => {
// global keydown listener on page.
document.addEventListener('keydown', console.log('global handler:', event.key))
}, [])
return (
<Overlay
onEscape={event => {
closeOverlay()
// You'd expect this to prevent the event from bubbling
// to the global handler on the page, but it doesn't
event.stopPropogation()
}}
>
...
</Overlay>
)
Scope
This issue of course isn’t just limited to “Escape”. If an application has global shortcuts, events bubbling out of Overlay can fire global shortcuts.
Why does this happen?
When escape is pressed, you would expect it to be before because Overlay is a child of the Page, but this isn’t the case. Overlay uses the useOnEscapePress
hook which attaches an event handler on the document, this is great because no matter where the focus is, pressing Escape will close the top most Overlay.
However, because this event listener is on the document, we cannot predict the order in which it will be fired - will it be before the global handler on the page or after it? And that’s why stopPropagation
and preventDefault
will not stop the global handler on the Page from catching this event. Here is a loom with a demo of this
There is a repro of this issue in our storybook setup and a failing test in our test suite that would help in finding a fix for this.
Prior work / failed attempts:
We made an attempt to fix this issue in https://github.com/primer/react/pull/1824 by moving the event listener to the Overlay instead of putting it on the document (with container.addEventListener
) and adding a event.stopPropagation
automatically.
This stopped events from bubbling up, but created a few other bugs:
- The Overlay eagerly caught keydown events that were attached to children with
onKeyDown
inside the Overlay before they fired on the children first. (Story for regression testing) - The Overlay does not catch Escape presses when it is not in focus (which probably a bug anyway?)
Potential fix
After the changes in https://github.com/primer/react/pull/1861, we can attempt to move the event listener back to the Overlay container.
-
We will have to pair it with adding focus trap on the Overlay, so that focus does not move outside the Overlay and all Escape presses are caught by the Overlay.
-
Lastly, you should be able to call
event.stopPropagation
inside a keydown event on a child component inside Overlay (like a TextInput) to prevent the Overlay from closing if Escape is pressed. (Story for testing). If the Overlay is eagerly catching events meant for its children, we can move the event listener to aonKeyDown
instead ofcontainer.addEventListener
. Useful reference to event delegation in React
Issue Analytics
- State:
- Created 2 years ago
- Comments:8 (8 by maintainers)
Top GitHub Comments
This is a bug with Overlay abstraction (not AnchoredOverlay), Added repro with our storybook to the description ⬆️
No one is actively asking for this right now so we’re going to close for the moment. Let’s reopen if needed.