diff --git a/src/cdk/a11y/live-announcer/live-announcer.spec.ts b/src/cdk/a11y/live-announcer/live-announcer.spec.ts index 73c661027f2..b95707b071a 100644 --- a/src/cdk/a11y/live-announcer/live-announcer.spec.ts +++ b/src/cdk/a11y/live-announcer/live-announcer.spec.ts @@ -60,6 +60,34 @@ describe('LiveAnnouncer', () => { expect(ariaLiveElement.getAttribute('aria-live')).toBe('polite'); })); + it('should be able to clear out the aria-live element manually', fakeAsync(() => { + announcer.announce('Hey Google'); + tick(100); + expect(ariaLiveElement.textContent).toBe('Hey Google'); + + announcer.clear(); + expect(ariaLiveElement.textContent).toBeFalsy(); + })); + + it('should be able to clear out the aria-live element by setting a duration', fakeAsync(() => { + announcer.announce('Hey Google', 2000); + tick(100); + expect(ariaLiveElement.textContent).toBe('Hey Google'); + + tick(2000); + expect(ariaLiveElement.textContent).toBeFalsy(); + })); + + it('should clear the duration of previous messages when announcing a new one', fakeAsync(() => { + announcer.announce('Hey Google', 2000); + tick(100); + expect(ariaLiveElement.textContent).toBe('Hey Google'); + + announcer.announce('Hello there'); + tick(2500); + expect(ariaLiveElement.textContent).toBe('Hello there'); + })); + it('should remove the aria-live element from the DOM on destroy', fakeAsync(() => { announcer.announce('Hey Google'); diff --git a/src/cdk/a11y/live-announcer/live-announcer.ts b/src/cdk/a11y/live-announcer/live-announcer.ts index a8807f2de13..1ba8312ea0b 100644 --- a/src/cdk/a11y/live-announcer/live-announcer.ts +++ b/src/cdk/a11y/live-announcer/live-announcer.ts @@ -47,15 +47,55 @@ export class LiveAnnouncer implements OnDestroy { /** * Announces a message to screenreaders. - * @param message Message to be announced to the screenreader - * @param politeness The politeness of the announcer element + * @param message Message to be announced to the screenreader. * @returns Promise that will be resolved when the message is added to the DOM. */ - announce(message: string, politeness: AriaLivePoliteness = 'polite'): Promise { - this._liveElement.textContent = ''; + announce(message: string): Promise; + + /** + * Announces a message to screenreaders. + * @param message Message to be announced to the screenreader. + * @param politeness The politeness of the announcer element. + * @returns Promise that will be resolved when the message is added to the DOM. + */ + announce(message: string, politeness?: AriaLivePoliteness): Promise; + + /** + * Announces a message to screenreaders. + * @param message Message to be announced to the screenreader. + * @param duration Time in milliseconds after which to clear out the announcer element. Note + * that this takes effect after the message has been added to the DOM, which can be up to + * 100ms after `announce` has been called. + * @returns Promise that will be resolved when the message is added to the DOM. + */ + announce(message: string, duration?: number): Promise; + + /** + * Announces a message to screenreaders. + * @param message Message to be announced to the screenreader. + * @param politeness The politeness of the announcer element. + * @param duration Time in milliseconds after which to clear out the announcer element. Note + * that this takes effect after the message has been added to the DOM, which can be up to + * 100ms after `announce` has been called. + * @returns Promise that will be resolved when the message is added to the DOM. + */ + announce(message: string, politeness?: AriaLivePoliteness, duration?: number): Promise; + + announce(message: string, ...args: any[]): Promise { + let politeness: AriaLivePoliteness; + let duration: number; + + if (args.length === 1 && typeof args[0] === 'number') { + duration = args[0]; + } else { + [politeness, duration] = args; + } + + this.clear(); + clearTimeout(this._previousTimeout); // TODO: ensure changing the politeness works on all environments we support. - this._liveElement.setAttribute('aria-live', politeness); + this._liveElement.setAttribute('aria-live', politeness! || 'polite'); // This 100ms timeout is necessary for some browser + screen-reader combinations: // - Both JAWS and NVDA over IE11 will not announce anything without a non-zero timeout. @@ -68,11 +108,26 @@ export class LiveAnnouncer implements OnDestroy { this._previousTimeout = setTimeout(() => { this._liveElement.textContent = message; resolve(); + + if (typeof duration === 'number') { + this._previousTimeout = setTimeout(() => this.clear(), duration); + } }, 100); }); }); } + /** + * Clears the current text from the announcer element. Can be used to prevent + * screen readers from reading the text out again while the user is going + * through the page landmarks. + */ + clear() { + if (this._liveElement) { + this._liveElement.textContent = ''; + } + } + ngOnDestroy() { clearTimeout(this._previousTimeout); diff --git a/src/lib/snack-bar/snack-bar.ts b/src/lib/snack-bar/snack-bar.ts index 0492ce702ef..d2028d0dd1b 100644 --- a/src/lib/snack-bar/snack-bar.ts +++ b/src/lib/snack-bar/snack-bar.ts @@ -208,6 +208,10 @@ export class MatSnackBar implements OnDestroy { if (this._openedSnackBarRef == snackBarRef) { this._openedSnackBarRef = null; } + + if (config.announcementMessage) { + this._live.clear(); + } }); if (this._openedSnackBarRef) {