From eec9637f341fa917856d2ca3b22bee4e9545659f Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Tue, 12 Jan 2021 15:29:49 +0800 Subject: [PATCH] fix(material/snack-bar): fix Firefox/JAWS not reading out snackbar message --- .../mdc-snack-bar/BUILD.bazel | 1 + .../mdc-snack-bar/snack-bar-container.html | 2 +- .../mdc-snack-bar/snack-bar-container.ts | 17 +++++++++++++ .../mdc-snack-bar/snack-bar.spec.ts | 25 +++++++++++++++++++ src/material/snack-bar/BUILD.bazel | 1 + .../snack-bar/snack-bar-container.html | 4 +-- src/material/snack-bar/snack-bar-container.ts | 17 +++++++++++++ src/material/snack-bar/snack-bar.spec.ts | 25 +++++++++++++++++++ .../public_api_guard/material/snack-bar.d.ts | 1 + 9 files changed, 90 insertions(+), 3 deletions(-) diff --git a/src/material-experimental/mdc-snack-bar/BUILD.bazel b/src/material-experimental/mdc-snack-bar/BUILD.bazel index b4e1dfd266b6..bdf80ba2c8b7 100644 --- a/src/material-experimental/mdc-snack-bar/BUILD.bazel +++ b/src/material-experimental/mdc-snack-bar/BUILD.bazel @@ -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", ], diff --git a/src/material-experimental/mdc-snack-bar/snack-bar-container.html b/src/material-experimental/mdc-snack-bar/snack-bar-container.html index eb5fdb1b7f61..25e3e89b3e19 100644 --- a/src/material-experimental/mdc-snack-bar/snack-bar-container.html +++ b/src/material-experimental/mdc-snack-bar/snack-bar-container.html @@ -10,6 +10,6 @@ -
+
diff --git a/src/material-experimental/mdc-snack-bar/snack-bar-container.ts b/src/material-experimental/mdc-snack-bar/snack-bar-container.ts index 258032aceb9c..296a75d1a514 100644 --- a/src/material-experimental/mdc-snack-bar/snack-bar-container.ts +++ b/src/material-experimental/mdc-snack-bar/snack-bar-container.ts @@ -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), @@ -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); diff --git a/src/material-experimental/mdc-snack-bar/snack-bar.spec.ts b/src/material-experimental/mdc-snack-bar/snack-bar.spec.ts index c7bd0574f4c3..e485934eeeee 100644 --- a/src/material-experimental/mdc-snack-bar/snack-bar.spec.ts +++ b/src/material-experimental/mdc-snack-bar/snack-bar.spec.ts @@ -21,6 +21,7 @@ import { MatSnackBarModule, MatSnackBarRef, } from './index'; +import {Platform} from '@angular/cdk/platform'; describe('MatSnackBar', () => { let snackBar: MatSnackBar; @@ -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); diff --git a/src/material/snack-bar/BUILD.bazel b/src/material/snack-bar/BUILD.bazel index fe9d3b934f17..010da3ff7c8c 100644 --- a/src/material/snack-bar/BUILD.bazel +++ b/src/material/snack-bar/BUILD.bazel @@ -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", ], diff --git a/src/material/snack-bar/snack-bar-container.html b/src/material/snack-bar/snack-bar-container.html index 32a293bba0c7..334df5db9bdb 100644 --- a/src/material/snack-bar/snack-bar-container.html +++ b/src/material/snack-bar/snack-bar-container.html @@ -1,7 +1,7 @@ - + -
+
diff --git a/src/material/snack-bar/snack-bar-container.ts b/src/material/snack-bar/snack-bar-container.ts index 9116587a7b37..d0b3112940b7 100644 --- a/src/material/snack-bar/snack-bar-container.ts +++ b/src/material/snack-bar/snack-bar-container.ts @@ -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, @@ -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. */ diff --git a/src/material/snack-bar/snack-bar.spec.ts b/src/material/snack-bar/snack-bar.spec.ts index bb59a7728584..a44876348116 100644 --- a/src/material/snack-bar/snack-bar.spec.ts +++ b/src/material/snack-bar/snack-bar.spec.ts @@ -22,6 +22,7 @@ import { MatSnackBarRef, SimpleSnackBar, } from './index'; +import {Platform} from '@angular/cdk/platform'; describe('MatSnackBar', () => { let snackBar: MatSnackBar; @@ -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(); diff --git a/tools/public_api_guard/material/snack-bar.d.ts b/tools/public_api_guard/material/snack-bar.d.ts index d878072916a8..bfe1e8446c37 100644 --- a/tools/public_api_guard/material/snack-bar.d.ts +++ b/tools/public_api_guard/material/snack-bar.d.ts @@ -54,6 +54,7 @@ export declare class MatSnackBarContainer extends BasePortalOutlet implements On readonly _onEnter: Subject; readonly _onExit: Subject; _portalOutlet: CdkPortalOutlet; + _role?: 'status' | 'alert'; attachDomPortal: (portal: DomPortal) => void; snackBarConfig: MatSnackBarConfig; constructor(_ngZone: NgZone, _elementRef: ElementRef, _changeDetectorRef: ChangeDetectorRef, _platform: Platform,