Skip to content

Commit 6cd2716

Browse files
authored
fix(material/snack-bar): fix Firefox/JAWS not reading out snackbar message (#21552)
1 parent cdfc46b commit 6cd2716

File tree

9 files changed

+90
-3
lines changed

9 files changed

+90
-3
lines changed

src/material-experimental/mdc-snack-bar/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ ng_test_library(
6969
":mdc-snack-bar",
7070
"//src/cdk/a11y",
7171
"//src/cdk/overlay",
72+
"//src/cdk/platform",
7273
"@npm//@angular/common",
7374
"@npm//@angular/platform-browser",
7475
],

src/material-experimental/mdc-snack-bar/snack-bar-container.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@
1010
</div>
1111

1212
<!-- Will receive the snack bar content from the non-live div, move will happen a short delay after opening -->
13-
<div [attr.aria-live]="_live"></div>
13+
<div [attr.aria-live]="_live" [attr.role]="_role"></div>
1414
</div>
1515
</div>

src/material-experimental/mdc-snack-bar/snack-bar-container.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ export class MatSnackBarContainer extends BasePortalOutlet
8686
/** Whether the snack bar is currently exiting. */
8787
_exiting = false;
8888

89+
/**
90+
* Role of the live region. This is only for Firefox as there is a known issue where Firefox +
91+
* JAWS does not read out aria-live message.
92+
*/
93+
_role?: 'status' | 'alert';
94+
8995
private _mdcAdapter: MDCSnackbarAdapter = {
9096
addClass: (className: string) => this._setClass(className, true),
9197
removeClass: (className: string) => this._setClass(className, false),
@@ -132,6 +138,17 @@ export class MatSnackBarContainer extends BasePortalOutlet
132138
this._live = 'polite';
133139
}
134140

141+
// Only set role for Firefox. Set role based on aria-live because setting role="alert" implies
142+
// aria-live="assertive" which may cause issues if aria-live is set to "polite" above.
143+
if (this._platform.FIREFOX) {
144+
if (this._live === 'polite') {
145+
this._role = 'status';
146+
}
147+
if (this._live === 'assertive') {
148+
this._role = 'alert';
149+
}
150+
}
151+
135152
// `MatSnackBar` will use the config's timeout to determine when the snack bar should be closed.
136153
// Set this to `-1` to mark it as indefinitely open so that MDC does not close itself.
137154
this._mdcFoundation.setTimeoutMs(-1);

src/material-experimental/mdc-snack-bar/snack-bar.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
MatSnackBarModule,
2222
MatSnackBarRef,
2323
} from './index';
24+
import {Platform} from '@angular/cdk/platform';
2425

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

165+
it('should have role of `alert` with an `assertive` politeness (Firefox only)', () => {
166+
const platform = TestBed.inject(Platform);
167+
snackBar.openFromComponent(BurritosNotification, {politeness: 'assertive'});
168+
viewContainerFixture.detectChanges();
169+
170+
const containerElement = overlayContainerElement.querySelector('mat-mdc-snack-bar-container')!;
171+
const liveElement = containerElement.querySelector('[aria-live]')!;
172+
173+
expect(liveElement.getAttribute('role'))
174+
.toBe(platform.FIREFOX ? 'alert' : null);
175+
});
176+
177+
it('should have role of `status` with an `polite` politeness (Firefox only)', () => {
178+
const platform = TestBed.inject(Platform);
179+
snackBar.openFromComponent(BurritosNotification, {politeness: 'polite'});
180+
viewContainerFixture.detectChanges();
181+
182+
const containerElement = overlayContainerElement.querySelector('mat-mdc-snack-bar-container')!;
183+
const liveElement = containerElement.querySelector('[aria-live]')!;
184+
185+
expect(liveElement.getAttribute('role'))
186+
.toBe(platform.FIREFOX ? 'status' : null);
187+
});
188+
164189
it('should have exactly one MDC label element when opened through simple snack bar', () => {
165190
let config: MatSnackBarConfig = {viewContainerRef: testViewContainerRef};
166191
snackBar.open(simpleMessage, simpleActionLabel, config);

src/material/snack-bar/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ ng_test_library(
6565
":snack-bar",
6666
"//src/cdk/a11y",
6767
"//src/cdk/overlay",
68+
"//src/cdk/platform",
6869
"@npm//@angular/common",
6970
"@npm//@angular/platform-browser",
7071
],
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
<!-- Initialy holds the snack bar content, will be empty after announcing to screen readers. -->
1+
<!-- Initially holds the snack bar content, will be empty after announcing to screen readers. -->
22
<div aria-hidden="true">
33
<ng-template cdkPortalOutlet></ng-template>
44
</div>
55

66
<!-- Will receive the snack bar content from the non-live div, move will happen a short delay after opening -->
7-
<div [attr.aria-live]="_live"></div>
7+
<div [attr.aria-live]="_live" [attr.role]="_role"></div>

src/material/snack-bar/snack-bar-container.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ export class MatSnackBarContainer extends BasePortalOutlet
9898
/** aria-live value for the live region. */
9999
_live: AriaLivePoliteness;
100100

101+
/**
102+
* Role of the live region. This is only for Firefox as there is a known issue where Firefox +
103+
* JAWS does not read out aria-live message.
104+
*/
105+
_role?: 'status' | 'alert';
106+
101107
constructor(
102108
private _ngZone: NgZone,
103109
private _elementRef: ElementRef<HTMLElement>,
@@ -117,6 +123,17 @@ export class MatSnackBarContainer extends BasePortalOutlet
117123
} else {
118124
this._live = 'polite';
119125
}
126+
127+
// Only set role for Firefox. Set role based on aria-live because setting role="alert" implies
128+
// aria-live="assertive" which may cause issues if aria-live is set to "polite" above.
129+
if (this._platform.FIREFOX) {
130+
if (this._live === 'polite') {
131+
this._role = 'status';
132+
}
133+
if (this._live === 'assertive') {
134+
this._role = 'alert';
135+
}
136+
}
120137
}
121138

122139
/** Attach a component portal as content to this snack bar container. */

src/material/snack-bar/snack-bar.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
MatSnackBarRef,
2323
SimpleSnackBar,
2424
} from './index';
25+
import {Platform} from '@angular/cdk/platform';
2526

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

163+
it('should have role of `alert` with an `assertive` politeness (Firefox only)', () => {
164+
const platform = TestBed.inject(Platform);
165+
snackBar.openFromComponent(BurritosNotification, {politeness: 'assertive'});
166+
viewContainerFixture.detectChanges();
167+
168+
const containerElement = overlayContainerElement.querySelector('snack-bar-container')!;
169+
const liveElement = containerElement.querySelector('[aria-live]')!;
170+
171+
expect(liveElement.getAttribute('role'))
172+
.toBe(platform.FIREFOX ? 'alert' : null);
173+
});
174+
175+
it('should have role of `status` with an `polite` politeness (Firefox only)', () => {
176+
const platform = TestBed.inject(Platform);
177+
snackBar.openFromComponent(BurritosNotification, {politeness: 'polite'});
178+
viewContainerFixture.detectChanges();
179+
180+
const containerElement = overlayContainerElement.querySelector('snack-bar-container')!;
181+
const liveElement = containerElement.querySelector('[aria-live]')!;
182+
183+
expect(liveElement.getAttribute('role'))
184+
.toBe(platform.FIREFOX ? 'status' : null);
185+
});
186+
162187
it('should open and close a snackbar without a ViewContainerRef', fakeAsync(() => {
163188
let snackBarRef = snackBar.open('Snack time!', 'Chew');
164189
viewContainerFixture.detectChanges();

tools/public_api_guard/material/snack-bar.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export declare class MatSnackBarContainer extends BasePortalOutlet implements On
5454
readonly _onEnter: Subject<void>;
5555
readonly _onExit: Subject<void>;
5656
_portalOutlet: CdkPortalOutlet;
57+
_role?: 'status' | 'alert';
5758
attachDomPortal: (portal: DomPortal) => void;
5859
snackBarConfig: MatSnackBarConfig;
5960
constructor(_ngZone: NgZone, _elementRef: ElementRef<HTMLElement>, _changeDetectorRef: ChangeDetectorRef, _platform: Platform,

0 commit comments

Comments
 (0)