-
Notifications
You must be signed in to change notification settings - Fork 2.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[core] fix(Overlay): use focus traps to fix enforceFocus #4894
Conversation
eb5846f
to
4e74ba7
Compare
When enforceFocus is enabled, surround Overlay content with dummy div elements with focus event listeners that keep focus inside the Overlay.
4e74ba7
to
a8b0b4b
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
private handleDocumentFocus = (e: FocusEvent) => { | ||
// get the actually target even if we are in an open mode Shadow DOM | ||
const eventTarget = e.composed ? e.composedPath()[0] : e.target; | ||
if ( | ||
this.props.enforceFocus && | ||
this.containerElement != null && | ||
eventTarget instanceof Node && | ||
!this.containerElement.contains(eventTarget as HTMLElement) | ||
) { | ||
// prevent default focus behavior (sometimes auto-scrolls the page) | ||
e.preventDefault(); | ||
e.stopImmediatePropagation(); | ||
this.bringFocusInsideOverlay(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when enforceFocus=true
, focus will start on the overlay and will only leave if another overlay with autoFocus or enforceFocus is opened. because we take care to focus the last opened overlay when closing the current overlay and we call bringFocusInsideOverlay
in the mousedown event handler, we don't need this focus event handler anymore
This comment has been minimized.
This comment has been minimized.
@@ -415,7 +501,7 @@ export class Overlay extends AbstractPureComponent2<OverlayProps, IOverlayState> | |||
if (openStack.length > 0) { | |||
const lastOpenedOverlay = Overlay.getLastOpened(); | |||
if (lastOpenedOverlay.props.enforceFocus) { | |||
document.addEventListener("focus", lastOpenedOverlay.handleDocumentFocus, /* useCapture */ true); | |||
lastOpenedOverlay.bringFocusInsideOverlay(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this handler was a roundabout way to bring focus back to the last opened overlay. we can just call bringFocusInsideOverlay
directly.
after(() => { | ||
document.documentElement.removeChild(testsContainerElement); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not really important, but seems good to clean up?
Fix tests by unmounting properlyPreviews: documentation | landing | table |
@@ -319,11 +335,11 @@ export class Overlay extends AbstractPureComponent2<OverlayProps, IOverlayState> | |||
if (isFocusOutsideModal) { | |||
// element marked autofocus has higher priority than the other clowns | |||
const autofocusElement = this.containerElement.querySelector("[autofocus]") as HTMLElement; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed that InputGroup
's autoFocus
prop is passed to the input
element as is. React doesn't actually render the autofocus attribute to the DOM (see facebook/react#11851 (comment)), so this querySelector is unlikely to work unless the consumer explicitly sets the DOM autofocus
(no caps) attribute instead. Not sure if we want to do anything about this though, since React will warn if you set the DOM attribute directly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should do something about this in a FLUP. not a high priority though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Filed #4908
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
overall looks pretty good!
This allows bringFocusInsideOverlay to work even when there are no keyboard-focusable elements.
I found a bug where this doesn't properly enforce focus if focus is coming back to the document from another part of the browser (e.g. the URL or bookmarks bar). I will restore the focus event handler logic, but I will listen instead for update: as suggested, I'll address this in a flup |
This comment has been minimized.
This comment has been minimized.
Fix selectorPreviews: documentation | landing | table |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice 👍
@michael-yx-wu - The appears to be a bug or unintentional behaviour with this change on dialogs with scrolls. See: #4920 |
Fixes #3445
Checklist
Changes proposed in this pull request:
When enforceFocus is enabled, surround Overlay content with dummy div
elements with focus event listeners that keep focus inside the Overlay.
Reviewers should focus on:
I want to flag that I've removed the
tabindex
attribute from the backdrop to prevent it from being keyboard focusable.This change also fixes an issue where multiple open overlays will compete with each for focus. As an example, go to https://blueprintjs.com/docs/#core/components/overlay and open the overlay and then hit shift+s to open the omnibar search (also powered by Overlay). The search input will not get autofocused. Repeat the same workflow on https://46971-71939872-gh.circle-artifacts.com/0/packages/docs-app/dist/index.html#core/components/overlay and note that the omnibar search does get focus correctly.
Screenshot
Before
When using a portal, the portal is the last element on the page so tabbing will eventually cause focus to leave the document and none of the existing event handlers run before that happens. Note that shift+tabbing is not closing the overlay, the gif is just looping.
After
Tab/shift+tab will never allow focus to leave the overlay if
enforceFocus
.