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

fix(a11y): focus monitor not working inside shadow dom #19135

Merged
merged 1 commit into from
Apr 24, 2020

Conversation

crisbeto
Copy link
Member

In #18667 event delegation was implemented for the FocusMonitor which is based around binding a single event on the document. The problem is that the browser won't invoke the focus and blur handlers on the document, if focus is moved within the same shadow root. These changes switch to delegating the events either through the closest shadow root or the document.

Fixes #19133.

Note: I spent some time trying to reproduce this in a unit test, but I wasn't able to because the way we simulate focus programmatically isn't how things behave in the browser, and we have to use programmatic focus in order to avoid flakes on the CI.

In angular#18667 event delegation was implemented for the `FocusMonitor` which is based around binding a single event on the `document`. The problem is that the browser won't invoke the `focus` and `blur` handlers on the `document`, if focus is moved within the same shadow root. These changes switch to delegating the events either through the closest shadow root or the `document`.

Fixes angular#19133.
@crisbeto crisbeto added P2 The issue is important to a large percentage of users, with a workaround target: patch This PR is targeted for the next patch release labels Apr 21, 2020
@googlebot googlebot added the cla: yes PR author has agreed to Google's Contributor License Agreement label Apr 21, 2020
@crisbeto crisbeto added Accessibility This issue is related to accessibility (a11y) and removed cla: yes PR author has agreed to Google's Contributor License Agreement labels Apr 21, 2020
@googlebot
Copy link

☹️ Sorry, but only Googlers may change the label cla: yes.

@googlebot googlebot added the cla: yes PR author has agreed to Google's Contributor License Agreement label Apr 21, 2020
* handlers differently from the rest of the events, because the browser won't emit events
* to the document when focus moves inside of a shadow root.
*/
private _rootNodeFocusListenerCount = new Map<HTMLElement|Document, number>();
Copy link
Contributor

Choose a reason for hiding this comment

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

To be safe, we should scan through this in ngOnDestroy and unlisten to any remaining entries

Copy link
Member Author

Choose a reason for hiding this comment

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

We're looping through and calling stopMonitoring on all the remaining elements which eventually empties this out as well.

cachedInfo!.checkChildren = checkChildren;
return cachedInfo!.subject.asObservable();
const cachedInfo = this._elementInfo.get(nativeElement)!;
cachedInfo.checkChildren = checkChildren;
Copy link
Contributor

Choose a reason for hiding this comment

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

I see that you didn't change this, but is this right? Won't this cause all previous calls to monitor on this element to behave according to the checkChildren parameter on the current call?

Copy link
Member Author

Choose a reason for hiding this comment

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

Judging by how it's written, I think that's the point. E.g. you could turn a shallow watch into a deep one by calling monitor again with checkChildren. We could separate it out and have 2 separate streams, but we'd have to make a cache key out of the options.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I'm not sure it should work that way. The main way I see it being called twice is if there's e.g. a directive and a component that need to know about focus state on the same element. They might have different requirements about whether to include the children, and we wouldn't want them to affect each other's subscriptions.

So it seems like a bug, but a pre-existing one and I haven't seen anyone complain about it yet, so I think its safe to leave this for another PR.

Copy link
Contributor

@mmalerba mmalerba left a comment

Choose a reason for hiding this comment

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

LGTM

@mmalerba mmalerba added lgtm action: merge The PR is ready for merge by the caretaker labels Apr 21, 2020
Copy link
Member

@devversion devversion 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 merged commit 495edca into angular:master Apr 24, 2020
jelbourn pushed a commit that referenced this pull request Apr 24, 2020
In #18667 event delegation was implemented for the `FocusMonitor` which is based around binding a single event on the `document`. The problem is that the browser won't invoke the `focus` and `blur` handlers on the `document`, if focus is moved within the same shadow root. These changes switch to delegating the events either through the closest shadow root or the `document`.

Fixes #19133.
soro-google pushed a commit to soro-google/components that referenced this pull request Apr 24, 2020
In angular#18667 event delegation was implemented for the `FocusMonitor` which is based around binding a single event on the `document`. The problem is that the browser won't invoke the `focus` and `blur` handlers on the `document`, if focus is moved within the same shadow root. These changes switch to delegating the events either through the closest shadow root or the `document`.

Fixes angular#19133.
crisbeto added a commit to crisbeto/material2 that referenced this pull request May 3, 2020
…ultiple times with different parameters

With the current logic, if an element is being monitored with `checkChildren` set to `true` and then we start monitoring it with `false`, the initial monitor call will stop checking the children, because we take the `checkChildren` value from the most-recent call. This is something that we discussed in angular#19135 (comment), but now we have a case where it can happen: setting a tooltip on a sort header. The sort header looks for child focus, whereas the tooltip doesn't, causing the sort focus indication to break.

These changes fix the issue by treating all focus as `checkChildren`, as soon as there's at least one call to `monitor` with `checkChildren` turned on.

Fixes angular#19218.
crisbeto added a commit to crisbeto/material2 that referenced this pull request May 5, 2020
…ultiple times with different parameters

With the current logic, if an element is being monitored with `checkChildren` set to `true` and then we start monitoring it with `false`, the initial monitor call will stop checking the children, because we take the `checkChildren` value from the most-recent call. This is something that we discussed in angular#19135 (comment), but now we have a case where it can happen: setting a tooltip on a sort header. The sort header looks for child focus, whereas the tooltip doesn't, causing the sort focus indication to break.

These changes fix the issue by treating all focus as `checkChildren`, as soon as there's at least one call to `monitor` with `checkChildren` turned on.

Fixes angular#19218.
mmalerba pushed a commit that referenced this pull request May 13, 2020
…ultiple times with different parameters (#19237)

With the current logic, if an element is being monitored with `checkChildren` set to `true` and then we start monitoring it with `false`, the initial monitor call will stop checking the children, because we take the `checkChildren` value from the most-recent call. This is something that we discussed in #19135 (comment), but now we have a case where it can happen: setting a tooltip on a sort header. The sort header looks for child focus, whereas the tooltip doesn't, causing the sort focus indication to break.

These changes fix the issue by treating all focus as `checkChildren`, as soon as there's at least one call to `monitor` with `checkChildren` turned on.

Fixes #19218.
mmalerba pushed a commit that referenced this pull request May 15, 2020
…ultiple times with different parameters (#19237)

With the current logic, if an element is being monitored with `checkChildren` set to `true` and then we start monitoring it with `false`, the initial monitor call will stop checking the children, because we take the `checkChildren` value from the most-recent call. This is something that we discussed in #19135 (comment), but now we have a case where it can happen: setting a tooltip on a sort header. The sort header looks for child focus, whereas the tooltip doesn't, causing the sort focus indication to break.

These changes fix the issue by treating all focus as `checkChildren`, as soon as there's at least one call to `monitor` with `checkChildren` turned on.

Fixes #19218.
@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 May 25, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Accessibility This issue is related to accessibility (a11y) action: merge The PR is ready for merge by the caretaker cla: yes PR author has agreed to Google's Contributor License Agreement P2 The issue is important to a large percentage of users, with a workaround target: patch This PR is targeted for the next patch release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Angular Material focus styles not applying in Shadow DOM
5 participants