Skip to content

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

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

Closed
wants to merge 1 commit into from

Conversation

Splaktar
Copy link
Contributor

@Splaktar Splaktar commented Jul 22, 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 tests to use different dialog components when they need to open multiple
      dialogs quickly

Fixes #21840


This PR takes a different approach than PR #23085 which required adding flush() to tests to ensure that the animations had a chance to complete. This PR uses a more naive approach that only gives animations 300ms to complete and won't allow another dialog of the same type to open within that time. The a11y piece of both PRs is the same (only move the focus to the dialog once).

@Splaktar Splaktar requested review from jelbourn and zarend July 22, 2021 21:56
@Splaktar Splaktar self-assigned this Jul 22, 2021
@google-cla google-cla bot added the cla: yes PR author has agreed to Google's Contributor License Agreement label Jul 22, 2021
@Splaktar Splaktar requested a review from crisbeto July 22, 2021 21:57
@Splaktar Splaktar added area: material/dialog 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) labels Jul 22, 2021
@Splaktar Splaktar force-pushed the dialog-fix-opening-a11y branch from c1f361f to 92a5dce Compare July 22, 2021 22:41
@Splaktar Splaktar added the target: patch This PR is targeted for the next patch release label Jul 23, 2021
@Splaktar Splaktar marked this pull request as ready for review July 23, 2021 00:31
@Splaktar Splaktar requested review from andrewseguin, devversion and a team as code owners July 23, 2021 00:31
@zarend
Copy link
Contributor

zarend commented Jul 23, 2021

Kicked off an internal presubmit

* @return true if a dialog was opened less than 300ms ago, false otherwise.
*/
_shouldBlockAnotherDialogOpening(): boolean {
return new Date().getTime() - this._lastDialogOpenTime < 300;
Copy link
Member

Choose a reason for hiding this comment

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

Where does the 300ms number come from? Currently the dialog animation is 150ms.

Also I think that this is pretty fragile. A better approach would be to have an isAnimating flag on the dialog container. It should be straightforward to do, because we already have _onAnimationStart and _onAnimationDone callbacks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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 agree and I don't prefer this naive approach, but that's what past reviews and presubmits suggested that we go with.

In https://github.com/angular/components/pull/23085/files#diff-fcade7d193ab41c7b9e2b072af496bedcf5b73ee519e237e32f8f5e00328246dR81, I had _dialogAnimatingOpen in _MatDialogBase, but you are suggesting using an attribute on the <mat-dialog-container> DOM element instead?

Copy link
Member

Choose a reason for hiding this comment

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

Once focus has been moved, the keyboard events won't be a problem anymore because they'll fire from inside the dialog. We could have another flag like _hasCapturedFocus which is flipped inside the focusInitialElementWhenReady callback.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The _hasCapturedFocus would be on the first dialog's _MatDialogContainerBase. Then when the 2nd dialog open is called and we see a matching componentOrTemplateRef, we would find the matching DialogRef in this.openDialogs and check it's _containerInstance. _hasCapturedFocus?

  • If it's false, then we throw an error or return the DialogRef from the first dialog.
  • If it's true, then what? Are we thinking that it shouldn't be possible to get a duplicate open event in this case and we should go ahead with opening the second dialog (with the same componentOrTemplateRef)?

Copy link
Member

Choose a reason for hiding this comment

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

Correct. If focus made it into the dialog, then it shouldn't be possible to accidentally trigger another dialog from the same trigger.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would this introduce a race condition that could lead to inconsistent behavior?

Copy link
Member

Choose a reason for hiding this comment

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

Focus is moved as a result of the animation finishing so I don't think there would be a race condition.

@Splaktar
Copy link
Contributor Author

Kicked off an internal presubmit

@zarend how did this go?

@zarend
Copy link
Contributor

zarend commented Jul 26, 2021

Kicked off an internal presubmit

@zarend how did this go?

187 test failures 😳

@Splaktar
Copy link
Contributor Author

Doh. 🤦🏼‍♂️

@Splaktar Splaktar force-pushed the dialog-fix-opening-a11y branch 2 times, most recently from 641cb48 to 8f247b0 Compare July 27, 2021 18:55
@@ -14,6 +14,8 @@ import {
AnimationTriggerMetadata,
} from '@angular/animations';

export const _transitionTime = 150;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@crisbeto I didn't want to copy this value all over the place. Instead I defined this const here, but I'm not confident that this is the right way to do it in this repo. Can you provide some guidance here please?

Comment on lines +165 to +167
this._lastDialogOpenTime = new Date().getTime();
this._lastDialogOpenType = componentOrTemplateRef;
this._lastDialogRef = dialogRef;
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 wonder if these should get set to null in ngOnDestroy()? But I guess that would require changing the types to accept null.

Copy link
Member

Choose a reason for hiding this comment

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

I don't get why we need to deal with time at all. What's stopping from setting an isAnimating flag on the dialog container and reading it instead? We already have all the event handlers in place and the dialog refs are already tracked in MatDialog.openDialogs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's what caused the need for all of the flushing in the other PR. But I guess you feel like this specific approach would avoid that?

Copy link
Member

Choose a reason for hiding this comment

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

I thought that the flushing was necessary, because we introduced a hard error if you didn't flush. Since now we return a dialog ref, the flushing should be necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Confirmed, when I removed the throwing of the error, I was able to remove the flush(); calls from the other PR.

@Splaktar Splaktar force-pushed the dialog-fix-opening-a11y branch from 8f247b0 to 6cc3644 Compare July 27, 2021 19:53
@Splaktar Splaktar removed the target: patch This PR is targeted for the next patch release label Jul 27, 2021
@zarend
Copy link
Contributor

zarend commented Jul 27, 2021

presubmit (internal)

@zarend
Copy link
Contributor

zarend commented Jul 28, 2021

presubmit (internal)

Test run (internal) Resulted in 94 test failures

@Splaktar Splaktar requested a review from amysorto August 2, 2021 18:10
@Splaktar Splaktar force-pushed the dialog-fix-opening-a11y branch from 6cc3644 to 78ea4b4 Compare August 2, 2021 18:15
@Splaktar
Copy link
Contributor Author

Splaktar commented Aug 2, 2021

So the recently presubmit failures are due to a case where the MatDialog is wrapped by another component and then reused all over a large app. This wrapper always uses the same ComponentRef every time, just with different content. With the changes in this PR, the same dialog contents are often returned when in fact they should be different - i.e. this approach of checking the componentOrTemplateRef against the previously used componentOrTemplateRef doesn't work in this case.

@Splaktar Splaktar force-pushed the dialog-fix-opening-a11y branch from 78ea4b4 to 087ed2c Compare August 2, 2021 19:24
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, although I think that we can iterate on it afterwards so that we don't have to check the time to know if an animation is running.

@@ -290,6 +291,9 @@ export class MatDialogTitle implements OnInit {
// @public
export function throwMatDialogContentAlreadyAttachedError(): void;

// @public
export const _transitionTime = 150;
Copy link
Member

Choose a reason for hiding this comment

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

Can we avoid exposing the publicly?

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 would like to, that's kind of what I was trying to ask in #23228 (review).

@Splaktar Splaktar force-pushed the dialog-fix-opening-a11y branch from 087ed2c to a891034 Compare August 6, 2021 19:23
- 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 tests to use different dialog components when they need to open multiple
  dialogs quickly

Fixes angular#21840
@Splaktar Splaktar force-pushed the dialog-fix-opening-a11y branch from a891034 to 72bd6c6 Compare August 6, 2021 19:45
@Splaktar
Copy link
Contributor Author

Splaktar commented Aug 9, 2021

Currently the preferred solution for this is in PR #23085.

@Splaktar Splaktar marked this pull request as draft August 9, 2021 18:24
@josephperrott josephperrott removed the request for review from a team August 10, 2021 18:38
@Splaktar Splaktar closed this Aug 16, 2021
@Splaktar Splaktar deleted the dialog-fix-opening-a11y branch August 16, 2021 17:39
@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) 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
Projects
None yet
Development

Successfully merging this pull request may close these issues.

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