fix(overlay): stop using capture phase for overlay keyboard handling (#16019)

OverlayKeyboardDispatcher is a document-level keyboard listener that
dispatches events to the topmost overlay that's listening for them.
It does it on capture as of e30852aa in order to avoid missing events
whose propagation have been stopped. However, there are legitimate use
cases for stopping propagation and not wanting the overlay to hear about
it. For example, grid navigation based on the ARIA best practice allows
for enter to trap focus inside a grid cell and escape to leave it and
restore focus to the grid cell.

https://www.w3.org/TR/wai-aria-practices-1.1/#gridNav_inside

In these cases, we must be able to call stopPropagation and prevent the
escape keypress event from reaching the overlay.

Fixes #9928.

BREAKING CHANGE: OverlayKeyboardDispatcher (used in dialog and overlay
    and available in the CDK) now listens on bubbling/propagation phase
    instead of capture phase. This means that overlay keydown handlers
    are now called after any applicable handlers inside of an overlay,
    and it respects any stopPropagation() calls from inside the overlay.
This commit is contained in:
Steven Xu
2019-06-04 09:42:09 -07:00
committed by Joey Perrott
parent c11653334b
commit 2d93c6d7fd
2 changed files with 6 additions and 6 deletions
@@ -69,7 +69,7 @@ describe('OverlayKeyboardDispatcher', () => {
expect(overlayTwoSpy).toHaveBeenCalled();
});
it('should dispatch keyboard events when propagation is stopped', () => {
it('should not dispatch keyboard events when propagation is stopped', () => {
const overlayRef = overlay.create();
const spy = jasmine.createSpy('keyboard event spy');
const button = document.createElement('button');
@@ -81,7 +81,7 @@ describe('OverlayKeyboardDispatcher', () => {
keyboardDispatcher.add(overlayRef);
dispatchKeyboardEvent(button, 'keydown', ESCAPE);
expect(spy).toHaveBeenCalled();
expect(spy).not.toHaveBeenCalled();
button.parentNode!.removeChild(button);
});
@@ -137,10 +137,10 @@ describe('OverlayKeyboardDispatcher', () => {
spyOn(body, 'removeEventListener');
keyboardDispatcher.add(overlayRef);
expect(body.addEventListener).toHaveBeenCalledWith('keydown', jasmine.any(Function), true);
expect(body.addEventListener).toHaveBeenCalledWith('keydown', jasmine.any(Function));
overlayRef.dispose();
expect(body.removeEventListener).toHaveBeenCalledWith('keydown', jasmine.any(Function), true);
expect(body.removeEventListener).toHaveBeenCalledWith('keydown', jasmine.any(Function));
});
it('should skip overlays that do not have keydown event subscriptions', () => {
@@ -47,7 +47,7 @@ export class OverlayKeyboardDispatcher implements OnDestroy {
// Lazily start dispatcher once first overlay is added
if (!this._isAttached) {
this._document.body.addEventListener('keydown', this._keydownListener, true);
this._document.body.addEventListener('keydown', this._keydownListener);
this._isAttached = true;
}
@@ -71,7 +71,7 @@ export class OverlayKeyboardDispatcher implements OnDestroy {
/** Detaches the global keyboard event listener. */
private _detach() {
if (this._isAttached) {
this._document.body.removeEventListener('keydown', this._keydownListener, true);
this._document.body.removeEventListener('keydown', this._keydownListener);
this._isAttached = false;
}
}