Skip to content
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

Merged
merged 3 commits into from
Oct 27, 2017

Conversation

willshowell
Copy link
Contributor

Fixes #6330
Fixes #3251
Closes #3460

@googlebot googlebot added the cla: yes PR author has agreed to Google's Contributor License Agreement label Aug 28, 2017
Copy link
Member

@jelbourn jelbourn left a 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?

@@ -140,6 +145,16 @@ export class OverlayRef implements PortalHost {
return this._detachments.asObservable();
}

/** Returns an observable that emits when the overlay has been disposed. */
Copy link
Member

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"

* Subscribe to attach and detach events and register them accordingly
* with the keyboard dispatcher service.
*/
private _trackOverlayAttachments(overlayRef: OverlayRef): void {
Copy link
Member

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?

@willshowell
Copy link
Contributor Author

@jelbourn comments addressed. Also added a demo to the overlay.

RxChain.from(overlayRef.keyboardEvents())
.call(filter, event => event.type === 'keydown' && event.keyCode === ESCAPE)
.subscribe(() => {
if (!dialogRef.disableClose) {
Copy link
Member

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>;
Copy link
Member

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'),
Copy link
Member

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').

Copy link
Contributor Author

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 =>
Copy link
Member

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 {
Copy link
Member

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.

@@ -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>();
Copy link
Member

@crisbeto crisbeto Aug 31, 2017

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.

Copy link
Contributor Author

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.

  1. 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.
  2. 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.

Copy link
Member

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.

Copy link
Contributor Author

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.

@willshowell
Copy link
Contributor Author

@crisbeto comments addressed. Please take another look!

Copy link
Member

@crisbeto crisbeto left a 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) ||
Copy link
Member

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 ?
Copy link
Member

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].

@@ -23,11 +24,14 @@ export class OverlayRef implements PortalHost {
private _attachments = new Subject<void>();
private _detachments = new Subject<void>();

_dispatchedKeydownEvents = new Subject<KeyboardEvent>();
Copy link
Member

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.

Copy link
Member

@crisbeto crisbeto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@crisbeto
Copy link
Member

@willshowell looks like the prerender task is still failing. You can debug it locally using gulp ci:prerender. Ping me to mark as merge ready.

@willshowell
Copy link
Contributor Author

@crisbeto I had issues with fromEvent not working well with the DOCUMENT token, so I opted to make the dispatch start lazily. PTAL.

Copy link
Member

@crisbeto crisbeto left a 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 {
Copy link
Member

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.

Copy link
Member

@jelbourn jelbourn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@jelbourn jelbourn added the action: merge The PR is ready for merge by the caretaker label Sep 5, 2017
@willshowell willshowell force-pushed the feat/key-dispatcher branch 2 times, most recently from b1e56ac to e90c917 Compare September 25, 2017 14:42
@andrewseguin andrewseguin added pr: needs rebase and removed action: merge The PR is ready for merge by the caretaker labels Sep 29, 2017
@willshowell willshowell force-pushed the feat/key-dispatcher branch 3 times, most recently from 241cf71 to 95f9016 Compare October 3, 2017 17:10
@willshowell
Copy link
Contributor Author

Rebased. cc @kara

@crisbeto crisbeto added action: merge The PR is ready for merge by the caretaker and removed pr: needs rebase labels Oct 3, 2017
@kara kara added the presubmit failures This PR has failures in Google's internal presubmit process and cannot be immediately merged label Oct 4, 2017
@willshowell
Copy link
Contributor Author

@kara is there anything I need to do regarding the presubmit failure?

@andrewseguin
Copy link
Contributor

andrewseguin commented Oct 12, 2017

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?

@willshowell
Copy link
Contributor Author

@andrewseguin I wasn't able to reproduce, but I've rebased and updated the license.

I'm passing on gulp ci:lint, aot, prerender (ci:test is broken from something else).

@jelbourn jelbourn added the P1 Impacts a large percentage of users; if a workaround exists it is partial or overly painful label Oct 21, 2017
@mmalerba
Copy link
Contributor

Is injecting DOCUMENT instead of using document critical here? Removing the DOCUMENT stuff fixes the error @andrewseguin mentioned

@jelbourn
Copy link
Member

@mmalerba we should only need to use DOCUMENT if we're mocking something in a test

@mmalerba
Copy link
Contributor

I think DOCUMENT is special somehow, I tried creating a different injection token, OVERLAY_DOCUMENT, and everything worked fine. So we should switch it and if we need it for testing just create our own provider. @willshowell Once that's done I can try again on presubmitting.

@jelbourn
Copy link
Member

Oh yeah, now I remember. I had to do the same thing for the tests for Directionality. BidiModule has

{provide: DIR_DOCUMENT, useExisting: DOCUMENT},

@willshowell
Copy link
Contributor Author

Doesn't seem there's any need to mock document for the tests, so I've removed the token.

@jelbourn jelbourn removed the presubmit failures This PR has failures in Google's internal presubmit process and cannot be immediately merged label Oct 25, 2017
@jelbourn
Copy link
Member

@mmalerba should be ready for another spin

crisbeto added a commit to crisbeto/material2 that referenced this pull request Oct 26, 2017
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.
@mmalerba mmalerba merged commit a2ca4d6 into angular:master Oct 27, 2017
@willshowell willshowell deleted the feat/key-dispatcher branch October 27, 2017 17:17
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
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 7, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker cla: yes PR author has agreed to Google's Contributor License Agreement P1 Impacts a large percentage of users; if a workaround exists it is partial or overly painful
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[overlay] keyboard event dispatcher expose dialog close attempts
7 participants