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(focus-trap): apply aria-hidden to focus trap tab anchors #14644

Merged
merged 1 commit into from
Jan 15, 2019

Conversation

stevenyxu
Copy link
Contributor

These anchors at the book ends of a FocusTrap'ed element have focus listeners that redirect focus
back to the element. However, some screen readers may access these focus traps without moving
programmatic focus, leaving the SR user wondering why an empty control lives on the page. Android
TalkBack currently treats this as a stop with no announcement (because it has no content). Adding
the aria-hidden should prevent these from being accessed in SR contexts while preserving the core
functionality of redirecting focus when it's moved linearly (e.g., with tab).

cdk FocusTrap adds focus trap divs with tabindex=0 at the beginning and end of the trapped container to prevent focus from leaving when pressing tab or linearly navigating. When these elements are focused, a focus handler redirects the browser focus as to effectively cycle focus within an element. This works fine for e.g., OS X VoiceOver, which generally moves focus when navigating the page. However some screenreaders may routinely browse elements without moving focus. A notable example is TalkBack, which is one of Angular Material's supported environments https://github.com/angular/material2#browser-and-screen-reader-support. This is also true in some popular configurations of JAWS/NVDA.

Settings these to have aria-hidden=true will typically cause for SRs to ignore these when linearly navigating the page, if they're not already ignored.

I've tested a primitive version of this fix on TalkBack and it works well.

https://stackblitz.com/edit/angular-e9gqbr?file=app%2Fdialog-content-example.ts
https://www.youtube.com/watch?v=SN1chzTfvTs

I can imagine some rare edge cases where this could cause a regression in functionality. In cases where cdk FocusTrap is used but were relying on SRs to "see" the focus trap anchors and shift focus to them, so that the active focus handler would redirect focus, this may break things, and SRs may wrongly shift focus to a bad spot. However, doing it this way would have been extremely finicky in the first place. If you wanted SRs not to be able to escape the focus trap, you should have set aria-hidden on the rest of the page in the first place (MatDialog does this https://github.com/angular/material2/blob/d22f48c742c0bbdc971f79ea1261ec5c20d15a69/src/lib/dialog/dialog.ts#L345, and individual application code may wrap cdk Overlay to do this; it's a bit of a mystery to me why cdk Overlay or cdk FocusTrap doesn't do this itself, but that's a separate concern). Such a page would have been a pain for SRs in the first place since heading and landmark navigation, common SR navigation techniques, could easily have shifted focus to the underlying page anyway. These users should be encouraged to apply aria-hidden appropriately. That said, we have tested this on the following SRs on the latest versions and things appear to be no worse:

Android TalkBack - my fix should address the hidden SR stop
NVDA/JAWS with virtual cursor - similar fix, but virtual cursor configuration varies greatly among users; generally, the hidden tab stops aren't problematic
OS X VO - always ignored the empty tabindex=0 anchors anyway

Internal bug reference b/122041252.

These anchors at the book ends of a FocusTrap'ed element have focus listeners that redirect focus
back to the element. However, some screen readers may access these focus traps without moving
programmatic focus, leaving the SR user wondering why an empty control lives on the page.  Android
TalkBack currently treats this as a stop with no announcement (because it has no content). Adding
the aria-hidden should prevent these from being accessed in SR contexts while preserving the core
functionality of redirecting focus when it's moved linearly (e.g., with tab).
@googlebot googlebot added the cla: yes PR author has agreed to Google's Contributor License Agreement label Dec 27, 2018
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. Leaving to @jelbourn for final approval since I can see this having some side effects.

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, thanks!

AFAICT there shouldn't be any adverse effects from this

@jelbourn jelbourn added pr: lgtm Accessibility This issue is related to accessibility (a11y) action: merge The PR is ready for merge by the caretaker labels Jan 9, 2019
@josephperrott josephperrott added the target: patch This PR is targeted for the next patch release label Jan 9, 2019
@vivian-hu-zz vivian-hu-zz merged commit f66302d into angular:master Jan 15, 2019
s2-abdo pushed a commit to s2-abdo/material2 that referenced this pull request Jan 18, 2019

Verified

This commit was signed with the committer’s verified signature.
fpletz Franz Pletz
…#14644)

These anchors at the book ends of a FocusTrap'ed element have focus listeners that redirect focus
back to the element. However, some screen readers may access these focus traps without moving
programmatic focus, leaving the SR user wondering why an empty control lives on the page.  Android
TalkBack currently treats this as a stop with no announcement (because it has no content). Adding
the aria-hidden should prevent these from being accessed in SR contexts while preserving the core
functionality of redirecting focus when it's moved linearly (e.g., with tab).
s2-abdo pushed a commit to s2-abdo/material2 that referenced this pull request Jan 18, 2019

Verified

This commit was signed with the committer’s verified signature.
fpletz Franz Pletz
…#14644)

These anchors at the book ends of a FocusTrap'ed element have focus listeners that redirect focus
back to the element. However, some screen readers may access these focus traps without moving
programmatic focus, leaving the SR user wondering why an empty control lives on the page.  Android
TalkBack currently treats this as a stop with no announcement (because it has no content). Adding
the aria-hidden should prevent these from being accessed in SR contexts while preserving the core
functionality of redirecting focus when it's moved linearly (e.g., with tab).
vivian-hu-zz pushed a commit that referenced this pull request Jan 18, 2019
These anchors at the book ends of a FocusTrap'ed element have focus listeners that redirect focus
back to the element. However, some screen readers may access these focus traps without moving
programmatic focus, leaving the SR user wondering why an empty control lives on the page.  Android
TalkBack currently treats this as a stop with no announcement (because it has no content). Adding
the aria-hidden should prevent these from being accessed in SR contexts while preserving the core
functionality of redirecting focus when it's moved linearly (e.g., with tab).
@MrGrigri
Copy link

This will cause a failure with Deque University Accessibility Checker Axe. More info can be found here:. I believe that this is a bandaid and not a solution.

@stevenyxu
Copy link
Contributor Author

Thanks for raising this Michael.

I can understand why the Axe checker recommends as it does but I believe the recommendation is imprecise or make an alternate suggestion to address the modal dialog use case. The spirit of the recommendation to my reading is to avoid accidentally obscuring semantically important focusable elements. However there are two major cases, both related to modal dialogs, where aria-hidden hides elements that fundamentally should be semantically hidden.

  1. Hiding tab stop anchors (this PR). Tab traps are necessary in many modal and non-modal popup applications. In the modal case, it prevents focus from wrongly going to document behind the popup. In the non-modal case where reparenting (popup moved out of DOM order but semantically kept in order using aria-owns) is used, the tab focus traps redirect focus correctly when it leaves the popup. In both cases, NVDA, JAWS, and Talkback can all move the virtual cursor without moving focus, and these tab stops, which exist for no purpose other than to redirect programmatic focus, are wrongly exposed.
  2. Hiding base page content. When a modal popup is opened, the base page should be hidden because in practice aria-modal="true" doesn't prevent the base page from being accessed in virtual cursor mode on any screen reader I've tested. In these cases, we must apply aria-hidden to the entire base page, which includes many interactive elements. Until SR vendors have wide support for aria-modal which I agree feels less bandaid-y, I think we have to use this approach.

If there's an alternative that satisfies these needs to display a usable modal dialog, please point me to it. https://www.w3.org/TR/wai-aria-practices/examples/dialog-modal/dialog.html actually fails on point 2 and doesn't even correctly hide the tab stops on point 1, causing overannouncing of semantically useless elements on some SRs. Our current approach to modal dialogs satisfies both points, has been reported as accessible by our testers, and only falls short of the letter of the Axe test.

@MrGrigri
Copy link

MrGrigri commented Apr 11, 2019

Thanks, @stevenyxu for your feedback. I was merely stating an observation that I had come across. The only other option that I can think of -- still not standardized, although a polyfill is available -- is the ability to mark DOM elements as inert. Thereby eliminating the need for tab stops altogether.

This would also handle the debate as to whether a keyboard trap on a dialog should exclude changing the focus from the viewport to the browser itself. I.e. allowing a user to navigate via a keyboard (without the use of an SR) to the URL address bar without the need to dismiss the dialog.

@stevenyxu
Copy link
Contributor Author

Thanks Michael. We will monitor inert and how the proposal advances.

The polyfill in its current state has the same defect you reported since it relies on aria-hidden to prevent landmark/type navigation and uses tabindex="-1" to make focusable nodes untabbable, which doesn't stop them from being focusable and still violates the letter of the rule you cited.

Additionally, inert does not address the non-modal use of tabindex traps, which would require an additional new standard (comparable to aria-owns but working for standard tabbing) and polyfill.

@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 10, 2019
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 target: patch This PR is targeted for the next patch release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants