Skip to content

fix(material/snack-bar): fix Firefox/JAWS not reading out snackbar me… #21552

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
Jan 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/material-experimental/mdc-snack-bar/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ ng_test_library(
":mdc-snack-bar",
"//src/cdk/a11y",
"//src/cdk/overlay",
"//src/cdk/platform",
"@npm//@angular/common",
"@npm//@angular/platform-browser",
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
</div>

<!-- Will receive the snack bar content from the non-live div, move will happen a short delay after opening -->
<div [attr.aria-live]="_live"></div>
<div [attr.aria-live]="_live" [attr.role]="_role"></div>
</div>
</div>
17 changes: 17 additions & 0 deletions src/material-experimental/mdc-snack-bar/snack-bar-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ export class MatSnackBarContainer extends BasePortalOutlet
/** Whether the snack bar is currently exiting. */
_exiting = false;

/**
* Role of the live region. This is only for Firefox as there is a known issue where Firefox +
* JAWS does not read out aria-live message.
*/
_role?: 'status' | 'alert';

private _mdcAdapter: MDCSnackbarAdapter = {
addClass: (className: string) => this._setClass(className, true),
removeClass: (className: string) => this._setClass(className, false),
Expand Down Expand Up @@ -132,6 +138,17 @@ export class MatSnackBarContainer extends BasePortalOutlet
this._live = 'polite';
}

// Only set role for Firefox. Set role based on aria-live because setting role="alert" implies
// aria-live="assertive" which may cause issues if aria-live is set to "polite" above.
if (this._platform.FIREFOX) {
if (this._live === 'polite') {
this._role = 'status';
}
if (this._live === 'assertive') {
this._role = 'alert';
}
}

// `MatSnackBar` will use the config's timeout to determine when the snack bar should be closed.
// Set this to `-1` to mark it as indefinitely open so that MDC does not close itself.
this._mdcFoundation.setTimeoutMs(-1);
Expand Down
25 changes: 25 additions & 0 deletions src/material-experimental/mdc-snack-bar/snack-bar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
MatSnackBarModule,
MatSnackBarRef,
} from './index';
import {Platform} from '@angular/cdk/platform';

describe('MatSnackBar', () => {
let snackBar: MatSnackBar;
Expand Down Expand Up @@ -161,6 +162,30 @@ describe('MatSnackBar', () => {
.toBe('off', 'Expected snack bar container live region to have aria-live="off"');
});

it('should have role of `alert` with an `assertive` politeness (Firefox only)', () => {
const platform = TestBed.inject(Platform);
snackBar.openFromComponent(BurritosNotification, {politeness: 'assertive'});
viewContainerFixture.detectChanges();

const containerElement = overlayContainerElement.querySelector('mat-mdc-snack-bar-container')!;
const liveElement = containerElement.querySelector('[aria-live]')!;

expect(liveElement.getAttribute('role'))
.toBe(platform.FIREFOX ? 'alert' : null);
});

it('should have role of `status` with an `polite` politeness (Firefox only)', () => {
const platform = TestBed.inject(Platform);
snackBar.openFromComponent(BurritosNotification, {politeness: 'polite'});
viewContainerFixture.detectChanges();

const containerElement = overlayContainerElement.querySelector('mat-mdc-snack-bar-container')!;
const liveElement = containerElement.querySelector('[aria-live]')!;

expect(liveElement.getAttribute('role'))
.toBe(platform.FIREFOX ? 'status' : null);
});

it('should have exactly one MDC label element when opened through simple snack bar', () => {
let config: MatSnackBarConfig = {viewContainerRef: testViewContainerRef};
snackBar.open(simpleMessage, simpleActionLabel, config);
Expand Down
1 change: 1 addition & 0 deletions src/material/snack-bar/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ ng_test_library(
":snack-bar",
"//src/cdk/a11y",
"//src/cdk/overlay",
"//src/cdk/platform",
"@npm//@angular/common",
"@npm//@angular/platform-browser",
],
Expand Down
4 changes: 2 additions & 2 deletions src/material/snack-bar/snack-bar-container.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!-- Initialy holds the snack bar content, will be empty after announcing to screen readers. -->
<!-- Initially holds the snack bar content, will be empty after announcing to screen readers. -->
<div aria-hidden="true">
<ng-template cdkPortalOutlet></ng-template>
</div>

<!-- Will receive the snack bar content from the non-live div, move will happen a short delay after opening -->
<div [attr.aria-live]="_live"></div>
<div [attr.aria-live]="_live" [attr.role]="_role"></div>
17 changes: 17 additions & 0 deletions src/material/snack-bar/snack-bar-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ export class MatSnackBarContainer extends BasePortalOutlet
/** aria-live value for the live region. */
_live: AriaLivePoliteness;

/**
* Role of the live region. This is only for Firefox as there is a known issue where Firefox +
* JAWS does not read out aria-live message.
*/
_role?: 'status' | 'alert';

constructor(
private _ngZone: NgZone,
private _elementRef: ElementRef<HTMLElement>,
Expand All @@ -117,6 +123,17 @@ export class MatSnackBarContainer extends BasePortalOutlet
} else {
this._live = 'polite';
}

// Only set role for Firefox. Set role based on aria-live because setting role="alert" implies
// aria-live="assertive" which may cause issues if aria-live is set to "polite" above.
if (this._platform.FIREFOX) {
if (this._live === 'polite') {
this._role = 'status';
}
if (this._live === 'assertive') {
this._role = 'alert';
}
}
}

/** Attach a component portal as content to this snack bar container. */
Expand Down
25 changes: 25 additions & 0 deletions src/material/snack-bar/snack-bar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
MatSnackBarRef,
SimpleSnackBar,
} from './index';
import {Platform} from '@angular/cdk/platform';

describe('MatSnackBar', () => {
let snackBar: MatSnackBar;
Expand Down Expand Up @@ -159,6 +160,30 @@ describe('MatSnackBar', () => {
.toBe('off', 'Expected snack bar container live region to have aria-live="off"');
});

it('should have role of `alert` with an `assertive` politeness (Firefox only)', () => {
const platform = TestBed.inject(Platform);
snackBar.openFromComponent(BurritosNotification, {politeness: 'assertive'});
viewContainerFixture.detectChanges();

const containerElement = overlayContainerElement.querySelector('snack-bar-container')!;
const liveElement = containerElement.querySelector('[aria-live]')!;

expect(liveElement.getAttribute('role'))
.toBe(platform.FIREFOX ? 'alert' : null);
});

it('should have role of `status` with an `polite` politeness (Firefox only)', () => {
const platform = TestBed.inject(Platform);
snackBar.openFromComponent(BurritosNotification, {politeness: 'polite'});
viewContainerFixture.detectChanges();

const containerElement = overlayContainerElement.querySelector('snack-bar-container')!;
const liveElement = containerElement.querySelector('[aria-live]')!;

expect(liveElement.getAttribute('role'))
.toBe(platform.FIREFOX ? 'status' : null);
});

it('should open and close a snackbar without a ViewContainerRef', fakeAsync(() => {
let snackBarRef = snackBar.open('Snack time!', 'Chew');
viewContainerFixture.detectChanges();
Expand Down
1 change: 1 addition & 0 deletions tools/public_api_guard/material/snack-bar.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export declare class MatSnackBarContainer extends BasePortalOutlet implements On
readonly _onEnter: Subject<void>;
readonly _onExit: Subject<void>;
_portalOutlet: CdkPortalOutlet;
_role?: 'status' | 'alert';
attachDomPortal: (portal: DomPortal) => void;
snackBarConfig: MatSnackBarConfig;
constructor(_ngZone: NgZone, _elementRef: ElementRef<HTMLElement>, _changeDetectorRef: ChangeDetectorRef, _platform: Platform,
Expand Down