-
Notifications
You must be signed in to change notification settings - Fork 6.8k
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
feat(overlay): add keyboard dispatcher for targeting correct overlay #6682
Conversation
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.
Mostly looks good to me- @crisbeto any thoughts?
src/cdk/overlay/overlay-ref.ts
Outdated
@@ -140,6 +145,16 @@ export class OverlayRef implements PortalHost { | |||
return this._detachments.asObservable(); | |||
} | |||
|
|||
/** Returns an observable that emits when the overlay has been disposed. */ |
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.
nit: prefer "Gets" to "Returns"
src/cdk/overlay/overlay.ts
Outdated
* Subscribe to attach and detach events and register them accordingly | ||
* with the keyboard dispatcher service. | ||
*/ | ||
private _trackOverlayAttachments(overlayRef: OverlayRef): void { |
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.
Could each OverlayRef
to this itself in its constructor?
@jelbourn comments addressed. Also added a demo to the overlay. |
src/lib/dialog/dialog.ts
Outdated
RxChain.from(overlayRef.keyboardEvents()) | ||
.call(filter, event => event.type === 'keydown' && event.keyCode === ESCAPE) | ||
.subscribe(() => { | ||
if (!dialogRef.disableClose) { |
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.
Might be a bit more concise to have this in filter
, instead of checking it in the subscribe
.
const bodyKeyboardEvents = merge( | ||
fromEvent(document.body, 'keydown'), | ||
fromEvent(document.body, 'keypress'), | ||
fromEvent(document.body, 'keyup')) as Observable<KeyboardEvent>; |
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.
Instead of casting it to an Observable<KeyboardEvent>
, you can pass the KeyboardEvent
as a generic: merge<KeyboardEvent>
.
private _dispatchKeyboardEvents(): void { | ||
const bodyKeyboardEvents = merge( | ||
fromEvent(document.body, 'keydown'), | ||
fromEvent(document.body, 'keypress'), |
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'm not sure that this should handle keydown
, keypress
and keyup
@jelbourn. Since for 99% of cases you'd only need keydown
, it means that every time you consume the service, you'd have to have a filter(event => event.type === 'keydown')
.
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'd be happy with this. I was aiming for generic, but the api could be "rebranded" 'to just keydownEvents
.
/** Select the appropriate overlay from a list of overlays and a keyboard event. */ | ||
private _selectOverlayFromEvent(overlays: OverlayRef[], event: KeyboardEvent): OverlayRef { | ||
// Check if any overlays contain the event | ||
const targetedOverlay = this._attachedOverlays.find(overlay => |
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 doesn't handle if the keyboard event came directly from the overlay element. You can tweak the predicate to include overlay.overlayElement === event.target
.
} | ||
|
||
/** Select the appropriate overlay from a list of overlays and a keyboard event. */ | ||
private _selectOverlayFromEvent(overlays: OverlayRef[], event: KeyboardEvent): OverlayRef { |
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.
Since the overlays
is always going to be this._attachedOverlays
, you can take it from the class property instead of passing it in.
src/cdk/overlay/overlay-ref.ts
Outdated
@@ -22,12 +23,16 @@ export class OverlayRef implements PortalHost { | |||
private _backdropClick: Subject<any> = new Subject(); | |||
private _attachments = new Subject<void>(); | |||
private _detachments = new Subject<void>(); | |||
private _disposed = new Subject<void>(); | |||
|
|||
_dispatchedKeyboardEvents = new Subject<KeyboardEvent>(); |
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.
Instead of having the OverlayRef
add and remove itself from the dispatcher, a cleaner approach would be to only have it expose the stream of keyboard events. Then the OverlayRef.keyboardEvents
can be simplified to:
// note: don't get too caught up in the naming
return this.keyboardDispatcher.events.filter(event => event.target === this.overlayElement);
This leaves less room for mistakes when managing the overlays and it also removes the circular dependency between the OverlayRef
and the dispatcher.
Edit: While we're at it, this could avoid having to add a dispatcher in the first place. The OverlayRef
can bind the keyboard listener on its own and only filter out relevant events.
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.
Hm I don't entirely follow.
- I kind of see how I could remove the
Subject
; maybe if I created a new event type that paired overlays to keydowns, and then the overlay just filters out ones directed at itself and maps to the keydown. - An important piece of this is dispatching an event to the most recently attached overlay. Often, events land on the body with no other context. That's the whole point of tracking overlays as they are attached and detached.
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.
Alright, I see. I guess that in most cases the top-most overlay would be the one dispatching the keyboard events, but that won't always be the case (like with md-autocomplete
). Let's keep it.
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.
One of the original problems was that if you opened a dialog without focusable elements, focus would still be on trigger or wherever it was before, but I see that's since been fixed. This plunker from the original issue shows that clicking on the backdrop still moves focus to the body.
I'll go ahead with the other changes and we can make sure it all still makes sense.
bdf8ca7
to
e074fa0
Compare
@crisbeto comments addressed. Please take another look! |
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.
Mostly LGTM, a few minor notes left.
private _selectOverlayFromEvent(event: KeyboardEvent): OverlayRef { | ||
// Check if any overlays contain the event | ||
const targetedOverlay = this._attachedOverlays.find(overlay => { | ||
return overlay.overlayElement.contains(event.target 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'd move the overlay.overlayElement === event.target
check first since it could potentially exit sooner.
}); | ||
|
||
// Use that overlay if it exists, otherwise choose the most recently attached one | ||
return targetedOverlay ? |
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.
return targetedOverlay || this._attachedOverlays[this._attachedOverlays.length - 1]
.
src/cdk/overlay/overlay-ref.ts
Outdated
@@ -23,11 +24,14 @@ export class OverlayRef implements PortalHost { | |||
private _attachments = new Subject<void>(); | |||
private _detachments = new Subject<void>(); | |||
|
|||
_dispatchedKeydownEvents = new Subject<KeyboardEvent>(); |
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.
Add a description to this one. Also potentially rename it to something like _keydownEvents
.
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.
LGTM
@willshowell looks like the |
@crisbeto I had issues with |
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.
Sorry, but one final nitpick.
* Subscribe to keydown events that land on the body and dispatch those | ||
* events to the appropriate overlay. | ||
*/ | ||
private _dispatchKeydownEvents(): void { |
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.
Judging by the name of the method, I'd assume that it would dispatch the events immediately, but that's not what it does. Something along the lines of _subscribeToKeydownEvents
might be more appropriate.
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.
LGTM
b1e56ac
to
e90c917
Compare
241cf71
to
95f9016
Compare
Rebased. cc @kara |
@kara is there anything I need to do regarding the presubmit failure? |
95f9016
to
a3d3536
Compare
Getting some AOT issue: Error: Error encountered resolving symbol values statically. Only initialized variables and constants can be referenced because the value of this variable is needed by the template compiler (position 10:22 in the original .ts file), resolving symbol DOCUMENT in /platform-browser/src/dom/dom_tokens.d.ts, resolving symbol DOCUMENT in /platform-browser/src/platform-browser.d.ts, resolving symbol DOCUMENT in /platform-browser/public_api.d.ts, resolving symbol DOCUMENT in /platform-browser/index.d.ts, resolving symbol DOCUMENT in platform_browser_safe/index.d.ts, resolving symbol OVERLAY_KEYBOARD_DISPATCHER_PROVIDER in /src/cdk/overlay/keyboard/overlay-keyboard-dispatcher.ts, resolving symbol OVERLAY_PROVIDERS in /src/cdk/overlay/overlay-module.ts, resolving symbol OverlayModule in /src/cdk/overlay/overlay-module.ts, resolving symbol OverlayModule in /src/cdk/overlay/overlay-module.ts Can you try building in AOT? |
a3d3536
to
2034dfc
Compare
@andrewseguin I wasn't able to reproduce, but I've rebased and updated the license. I'm passing on |
Is injecting |
@mmalerba we should only need to use |
I think |
Oh yeah, now I remember. I had to do the same thing for the tests for {provide: DIR_DOCUMENT, useExisting: DOCUMENT}, |
2034dfc
to
bbbf598
Compare
Doesn't seem there's any need to mock |
@mmalerba should be ready for another spin |
After the switch to `ActiveDescendantKeyManager`, the select was no longer handling the escape key functionality internally, but was delegating to the underlying overlay. Since the overlay listens to keyboard events on the document, it means that escape key presses will close any parent overlays along the way. These changes add the escape listener to the select itself and stop the event propagation. **Note:** the overlay directive listener should be scoped to the overlay itself, but it's better if we defer refactoring it until angular#6682 gets in. Fixes angular#7981.
bbbf598
to
43c8490
Compare
Fix typo and add comment Address comments Add keyboard tracking demo to overlay Address comments Revert no-longer-needed dispose changes Address comments Fix prerender error by lazily starting dispatcher Address naming comment
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
Fixes #6330
Fixes #3251
Closes #3460