From a93d3a5503349174f72b385984f5d688fa409188 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 8 Nov 2018 17:58:16 +0000 Subject: [PATCH] fix(a11y): avoid overlapping or left over timers in live announcer (#13602) * Avoids timers overlapping in the `LiveAnnouncer`, which can happen if a new message is announced within 100ms of the previous one. This can be an issue if the screen reader started reading out the previous message and then gets interrupted by the new one. * Avoids leftover timers if the service is destroyed. * Fixes the reference to the `_liveElement` not being cleared after it's removed from the DOM, potentially leaving it in memory. --- .../live-announcer/live-announcer.spec.ts | 24 +++++++++++++++++++ src/cdk/a11y/live-announcer/live-announcer.ts | 9 +++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/cdk/a11y/live-announcer/live-announcer.spec.ts b/src/cdk/a11y/live-announcer/live-announcer.spec.ts index 7a37b21d4340..d9b18ec0d927 100644 --- a/src/cdk/a11y/live-announcer/live-announcer.spec.ts +++ b/src/cdk/a11y/live-announcer/live-announcer.spec.ts @@ -108,6 +108,30 @@ describe('LiveAnnouncer', () => { .toBe(1, 'Expected only one live announcer element in the DOM.'); })); + it('should clear any previous timers when a new one is started', fakeAsync(() => { + expect(ariaLiveElement.textContent).toBeFalsy(); + + announcer.announce('One'); + tick(50); + + announcer.announce('Two'); + tick(75); + + expect(ariaLiveElement.textContent).toBeFalsy(); + + tick(25); + + expect(ariaLiveElement.textContent).toBe('Two'); + })); + + it('should clear pending timeouts on destroy', fakeAsync(() => { + announcer.announce('Hey Google'); + announcer.ngOnDestroy(); + + // Since we're testing whether the timeouts were flushed, we don't need any + // assertions here. `fakeAsync` will fail the test if a timer was left over. + })); + }); describe('with a custom element', () => { diff --git a/src/cdk/a11y/live-announcer/live-announcer.ts b/src/cdk/a11y/live-announcer/live-announcer.ts index 0d5a8d7dfa15..9dfd0aa9a1e8 100644 --- a/src/cdk/a11y/live-announcer/live-announcer.ts +++ b/src/cdk/a11y/live-announcer/live-announcer.ts @@ -29,8 +29,9 @@ export type AriaLivePoliteness = 'off' | 'polite' | 'assertive'; @Injectable({providedIn: 'root'}) export class LiveAnnouncer implements OnDestroy { - private readonly _liveElement: HTMLElement; + private _liveElement: HTMLElement; private _document: Document; + private _previousTimeout?: number; constructor( @Optional() @Inject(LIVE_ANNOUNCER_ELEMENT_TOKEN) elementToken: any, @@ -63,7 +64,8 @@ export class LiveAnnouncer implements OnDestroy { // (using JAWS 17 at time of this writing). return this._ngZone.runOutsideAngular(() => { return new Promise(resolve => { - setTimeout(() => { + clearTimeout(this._previousTimeout); + this._previousTimeout = setTimeout(() => { this._liveElement.textContent = message; resolve(); }, 100); @@ -72,8 +74,11 @@ export class LiveAnnouncer implements OnDestroy { } ngOnDestroy() { + clearTimeout(this._previousTimeout); + if (this._liveElement && this._liveElement.parentNode) { this._liveElement.parentNode.removeChild(this._liveElement); + this._liveElement = null!; } }