Skip to content

fix(material/dialog): improve screen reader support when opened #23085

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

Merged
merged 1 commit into from
Aug 13, 2021

Conversation

Splaktar
Copy link
Contributor

@Splaktar Splaktar commented Jun 29, 2021

  • notify screen reader users that they have entered a dialog
  • previously only the focused element would be read
      i.e. "Close Button Press Search plus Space to activate"
  • now the screen reader user gets the normal dialog behavior, which is to
      read the dialog title, role, content, and then tell the user about the
      focused element
      - this matches the guidance here:
        https://www.w3.org/TR/wai-aria-practices-1.1/examples/dialog-modal/dialog.html
  • Avoid opening multiple of the same dialog before animations complete by returning
      the previous MatDialogRef
  • update material/dialog API golden file

Fixes #21840

@Splaktar Splaktar requested a review from crisbeto June 29, 2021 02:25
@Splaktar Splaktar requested a review from jelbourn as a code owner June 29, 2021 02:25
@google-cla google-cla bot added the cla: yes PR author has agreed to Google's Contributor License Agreement label Jun 29, 2021
@Splaktar Splaktar added G This is is related to a Google internal issue P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent Accessibility This issue is related to accessibility (a11y) area: material/dialog target: patch This PR is targeted for the next patch release labels Jun 29, 2021
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.

Also worth noting that the commit message mentions the MDC dialog, but all the changes are to the non-MDC one.

@Splaktar Splaktar marked this pull request as draft June 29, 2021 16:10
@Splaktar
Copy link
Contributor Author

Splaktar commented Jun 29, 2021

the commit message mentions the MDC dialog, but all the changes are to the non-MDC one.

@crisbeto I was only testing against the MDC-dialog, but they both use the same code for this. How should I write the commit message in that case? fix(multiple): or just use fix(material/dialog): even though it also effects mdc-dialog?

@Splaktar
Copy link
Contributor Author

Splaktar commented Jun 30, 2021

So after looking into this some more, here's what I've got to work with

  • _MatDialogBase is a "service" which holds all of the open dialogs and contains the open() method. It seems to be the right place to add a check to see if there is a dialog animating open before trying to open a new dialog.
    • However, it does not currently care about whether a dialog that it is opening has animations or not.
  • MatDialogContainer is a Component that holds the _state of the dialog's animation and some private callback functions related to animation state. They emit the state via _MatDialogContainerBase._animationStateChanged. MatDialogContainer's host bindings hook up the state and callbacks to matDialogAnimations.
  • _MatDialogContainerBase is the abstract class extended by MatDialogContainer. It has _animationStateChanged = new EventEmitter<DialogAnimationEvent>(). This event holds the totalTime and the state ('opened' | 'opening' | 'closing' | 'closed').
    • This class also contains _initializeWithAttachedContent() which is where we _setupFocusTrap(), _capturePreviouslyFocusedElement(), and _preventDuplicateDialogOpens(). However, by the time we've made it here, in _MatDialogBase we've already created the overlay, attached the dialog container, attached the dialog content, hidden non-dialog content from screen readers, pushed the dialog ref into the openDialogs array, subscribed to afterClosed(), and called afterOpened.next(dialogRef). We should really block a second attempt at opening the dialog (when the original attempt is still animating) before we get this far into the process.

@Splaktar Splaktar force-pushed the mdc-dialog-fix-opening-a11y branch 3 times, most recently from 0e636aa to abffbf3 Compare June 30, 2021 04:49
@Splaktar Splaktar removed the target: patch This PR is targeted for the next patch release label Jun 30, 2021
@Splaktar Splaktar force-pushed the mdc-dialog-fix-opening-a11y branch from abffbf3 to d59d5eb Compare June 30, 2021 04:56
@Splaktar Splaktar changed the title fix(material-experimental/mdc-dialog): improve screen reader support when opened fix(material/dialog): improve screen reader support when opened Jun 30, 2021
@Splaktar Splaktar marked this pull request as ready for review June 30, 2021 05:12
@Splaktar Splaktar requested a review from devversion as a code owner June 30, 2021 05:12
@crisbeto
Copy link
Member

I think the check should happen in the dialog service since it is the one that determines if a dialog can open. One thing that I hadn't really considered when I posted my comment is what we would return if we decide not to open a dialog. We can't create a dialog ref without actually creating the dialog.

@Splaktar
Copy link
Contributor Author

One thing that I hadn't really considered when I posted my comment is what we would return if we decide not to open a dialog. We can't create a dialog ref without actually creating the dialog.

Right. For the moment, I'm throwing an Error in that case.

@crisbeto
Copy link
Member

I'm not sure that throwing the error would be the right thing either, because it would be a breaking change and consumers wouldn't have a way of fixing it.

@jelbourn
Copy link
Member

jelbourn commented Jun 30, 2021

I don't quite follow how these changes would be fixing the dialog information not being read. What mechanically is changing about how focus is set that leads to the cause in behavior change?

@Splaktar
Copy link
Contributor Author

I'm not sure that throwing the error would be the right thing either, because it would be a breaking change and consumers wouldn't have a way of fixing it.

Agreed, but I don't have a better approach in mind atm. We could do a different breaking change to have open() return a MatDialogRef | null, but that's not great either.

@Splaktar
Copy link
Contributor Author

I don't quite follow how these changes would be fixing the dialog information not being read. What mechanically is changing about how focus is set that leads to the cause in behavior change?

The quick swap of the focus from the trigger button to the dialog container and then to the first focusable element in the dialog is what causes the screen reader to bail out and not announce anything other than being on a focusable element. This approach does work to avoid multiple dialogs being opened while one is animating open.

I've verified two paths to fixing this, while still avoiding multiple dialogs being opened while one is animating open

  • the previous approach of blurring the trigger button on open
    • which causes some test failures on iOS as it puts the focus on the body which iOS doesn't like since it's not a form element
  • the current approach in this PR which blocks opening of new dialogs while a dialog is animating open

@jelbourn
Copy link
Member

So to make sure I understand correctly:

Previously, the dialog always focused itself, and then moved focus to the first tabbable elements / cdkFocusInitial element once the animation finishes?

With the PR, this focus only ever goes to the configured element once the animation finishes without going to the container?

How does the case of multiple dialogs opening simultaneously play into this? Is it just that they fight for focus in a way that messes up the reader? Rather than throwing an error for this case, we could try to figure out if we can decide not to care as much about that case, since multiple/nested modal dialogs are already somewhat of an anti-pattern.

@crisbeto
Copy link
Member

To provide some context: this bouncing around of focus was introduced, because if you have a button that opens a dialog and you press it using the keyboard, you could end up triggering multiple "clicks" if your finger lingers on the button a bit longer than expected. This is made more likely by the fact that we only move focus away from the button once the ~300ms dialog animation has finished.

@jelbourn
Copy link
Member

Ah, I see. What do you think about setting the activeElement to pointer-events: none until the focus lands in the new place?

@crisbeto
Copy link
Member

I haven't actually tried it, but I would assume that setting pointer-events: none would either:

  1. Not block keyboard events since the keyboard isn't a pointer.
  2. Blur the element, similarly to what happens a button becomes disabled.

We can always try it and see what happens. We could also try to do something where we save the trigger element when a dialog is opened. If another one is triggered while the dialog is animating, we return the previous dialog ref.

@zarend
Copy link
Contributor

zarend commented Aug 3, 2021

Yes, I'll run the presubmit tests overnight tonight. 👍

On Mon, Aug 2, 2021 at 12:16 PM Michael Prentice @.***> wrote: I was also able to remove all of the flush(); calls and the unit tests still pass. @zarend https://github.com/zarend can you please presubmit? — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub <#23085 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/AB242NJJ75ZECRA7J3OZR4TT23VJHANCNFSM47PELB7Q . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&utm_campaign=notification-email .

presubmit (internal) had build failures last night 🤦 . I fixed the build failures, and will run the tests tonight 🤞 .

@Splaktar
Copy link
Contributor Author

Splaktar commented Aug 3, 2021

@devversion this code seems to run fine on all platforms other than iOS (Sauce Labs).

// Fake the behavior of pressing the SPACE key on a button element. Browsers fire a `click`
// event with a MouseEvent, which has coordinates that are out of the element boundaries.
dispatchMouseEvent(closeButton, 'click', 0, 0);
viewContainerFixture.detectChanges();
tick(500);
expect(lastFocusOrigin!)
.withContext('Expected the trigger button to be focused via keyboard')
.toBe('keyboard');

// The dialog close button detects the focus origin by inspecting the click event. If
// coordinates of the click are not present, it assumes that the click has been triggered
// by keyboard.
dispatchMouseEvent(closeButton, 'click', 10, 10);
viewContainerFixture.detectChanges();
tick(500);
expect(lastFocusOrigin!)
.withContext('Expected the trigger button to be focused via mouse')
.toBe('mouse');

I'm not sure why it was passing before, but now it's failing with the changes in this PR, but only on iOS.

@devversion
Copy link
Member

@Splaktar Not sure from the top of my head. This seems like something that needs to be investigated within the emulator.

@zarend
Copy link
Contributor

zarend commented Aug 5, 2021

The internal test results are available, 13 broken targets.

@Splaktar
Copy link
Contributor Author

Splaktar commented Aug 5, 2021

Great! Are any of those related to iOS?

@Splaktar
Copy link
Contributor Author

Splaktar commented Aug 6, 2021

I am able to reproduce these iOS test failures in a Simulator locally, investigating...

@Splaktar Splaktar force-pushed the mdc-dialog-fix-opening-a11y branch from e6b838d to 235b0bc Compare August 6, 2021 19:37
@zarend
Copy link
Contributor

zarend commented Aug 6, 2021

Great! Are any of those related to iOS?

I did a quick glance at the internal failures. They're mostly screenshot failures, and don't look iOS related. Jeremy and I are going over them in more detail later this afternoon.

@Splaktar
Copy link
Contributor Author

Splaktar commented Aug 6, 2021

OK, I think that I've got a solution for the iOS test failures on Sauce Labs. It works locally and now I'm just waiting on this CI run to confirm.

@Splaktar Splaktar requested a review from crisbeto August 6, 2021 20:00
- notify screen reader users that they have entered a dialog
- previously only the focused element would be read
  i.e. "Close Button Press Search plus Space to activate"
- now the screen reader user gets the normal dialog behavior, which is to
  read the dialog title, role, content, and then tell the user about the
  focused element
  - this matches the guidance here:
    https://www.w3.org/TR/wai-aria-practices-1.1/examples/dialog-modal/dialog.html
- Avoid opening multiple of the same dialog before animations complete by returning
  the previous `MatDialogRef`
- update material/dialog API golden file

Fixes angular#21840
@Splaktar Splaktar force-pushed the mdc-dialog-fix-opening-a11y branch from 235b0bc to 5dd0503 Compare August 9, 2021 17:51
@Splaktar
Copy link
Contributor Author

Splaktar commented Aug 9, 2021

Resolved conflict in test.

@Splaktar Splaktar added the target: patch This PR is targeted for the next patch release label Aug 9, 2021
@josephperrott josephperrott removed the request for review from a team August 10, 2021 18:38
@Splaktar
Copy link
Contributor Author

@zarend any progress with these presubmit failures?

@zarend
Copy link
Contributor

zarend commented Aug 12, 2021

@zarend any progress with these presubmit failures?

yes, I have a passing presubmit, which I got to pass by patching failing tests 🎉 . I am the caretaker today and tomorrow, so I'll merge and sync this PR after we finish the sync that's in progress.

@zarend
Copy link
Contributor

zarend commented Aug 12, 2021

ohh, we still need PR reviews for this.

@@ -218,7 +215,13 @@ export abstract class _MatDialogContainerBase extends BasePortalOutlet {
break;
case true:
case 'first-tabbable':
this._focusTrap.focusInitialElementWhenReady();
this._focusTrap.focusInitialElementWhenReady().then(focusedSuccessfully => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Im not too sure if this can have a target of patch. This is touching code from #22780, which will go into the next minor release.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, does sidenav or bottom sheet need special handling too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@amysorto Special handling when opening to support screen readers? Probably not, unless they did the bouncing of focus that dialog did prior to this PR.

Copy link
Member

Choose a reason for hiding this comment

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

We should double-check that they don't

Copy link
Contributor Author

@Splaktar Splaktar Aug 13, 2021

Choose a reason for hiding this comment

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

  • Check that bottom sheet behaves properly in a screen reader when opened
  • Check that sidenav behaves properly in a screen reader when opened

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@zarend found some similar issues with Bottom Sheet and he's investigating them now.

@Splaktar Splaktar added target: minor This PR is targeted for the next minor release and removed target: patch This PR is targeted for the next patch release presubmit failures This PR has failures in Google's internal presubmit process and cannot be immediately merged labels Aug 12, 2021
Copy link
Contributor

@zarend zarend left a comment

Choose a reason for hiding this comment

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

this seems reasonable to me 👍

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

@@ -218,7 +215,13 @@ export abstract class _MatDialogContainerBase extends BasePortalOutlet {
break;
case true:
case 'first-tabbable':
this._focusTrap.focusInitialElementWhenReady();
this._focusTrap.focusInitialElementWhenReady().then(focusedSuccessfully => {
Copy link
Member

Choose a reason for hiding this comment

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

We should double-check that they don't

@jelbourn jelbourn added the action: merge The PR is ready for merge by the caretaker label Aug 12, 2021
@zarend zarend merged commit 728cf1c into angular:master Aug 13, 2021
@Splaktar Splaktar deleted the mdc-dialog-fix-opening-a11y branch August 16, 2021 17:38
@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 16, 2021
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 area: material/dialog cla: yes PR author has agreed to Google's Contributor License Agreement G This is is related to a Google internal issue P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent target: minor This PR is targeted for the next minor release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

bug(dialog): Chromevox doesn't announce dialog role
6 participants