From 779573455320bda73bbcc2138be8b238e63dfee2 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 30 Mar 2022 10:49:09 +0200 Subject: [PATCH] fix(cdk/a11y): live announcer promise never resolved if new announcement comes in We return a promise that indicates when we've added the live announcer content to the DOM, however the promise will never be resolved if a new message comes in during the 100ms that it takes for us to make the announcement. These changes add some extra logic to ensure it is always resolved. Note that I was also considering rejecting the old promise instead, but that may be a poor experience for users since they may not have control over messages that are coming in from other places in the app. Fixes #24686. --- .../live-announcer/live-announcer.spec.ts | 10 ++++++ src/cdk/a11y/live-announcer/live-announcer.ts | 32 ++++++++++++------- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/cdk/a11y/live-announcer/live-announcer.spec.ts b/src/cdk/a11y/live-announcer/live-announcer.spec.ts index accde0d9b5a1..99605d10d3f9 100644 --- a/src/cdk/a11y/live-announcer/live-announcer.spec.ts +++ b/src/cdk/a11y/live-announcer/live-announcer.spec.ts @@ -112,6 +112,16 @@ describe('LiveAnnouncer', () => { expect(spy).toHaveBeenCalled(); })); + it('should resolve the returned promise if another announcement is made before the timeout has expired', fakeAsync(() => { + const spy = jasmine.createSpy('announce spy'); + announcer.announce('something').then(spy); + tick(10); + announcer.announce('something').then(spy); + tick(100); + + expect(spy).toHaveBeenCalledTimes(2); + })); + it('should ensure that there is only one live element at a time', fakeAsync(() => { fixture.destroy(); diff --git a/src/cdk/a11y/live-announcer/live-announcer.ts b/src/cdk/a11y/live-announcer/live-announcer.ts index 4eb584b6993d..cd8c4f3b6063 100644 --- a/src/cdk/a11y/live-announcer/live-announcer.ts +++ b/src/cdk/a11y/live-announcer/live-announcer.ts @@ -31,6 +31,8 @@ export class LiveAnnouncer implements OnDestroy { private _liveElement: HTMLElement; private _document: Document; private _previousTimeout: number; + private _currentPromise: Promise | undefined; + private _currentResolve: (() => void) | undefined; constructor( @Optional() @Inject(LIVE_ANNOUNCER_ELEMENT_TOKEN) elementToken: any, @@ -115,17 +117,23 @@ export class LiveAnnouncer implements OnDestroy { // second time without clearing and then using a non-zero delay. // (using JAWS 17 at time of this writing). return this._ngZone.runOutsideAngular(() => { - return new Promise(resolve => { - clearTimeout(this._previousTimeout); - this._previousTimeout = setTimeout(() => { - this._liveElement.textContent = message; - resolve(); - - if (typeof duration === 'number') { - this._previousTimeout = setTimeout(() => this.clear(), duration); - } - }, 100); - }); + if (!this._currentPromise) { + this._currentPromise = new Promise(resolve => (this._currentResolve = resolve)); + } + + clearTimeout(this._previousTimeout); + this._previousTimeout = setTimeout(() => { + this._liveElement.textContent = message; + + if (typeof duration === 'number') { + this._previousTimeout = setTimeout(() => this.clear(), duration); + } + + this._currentResolve!(); + this._currentPromise = this._currentResolve = undefined; + }, 100); + + return this._currentPromise; }); } @@ -144,6 +152,8 @@ export class LiveAnnouncer implements OnDestroy { clearTimeout(this._previousTimeout); this._liveElement?.remove(); this._liveElement = null!; + this._currentResolve?.(); + this._currentPromise = this._currentResolve = undefined; } private _createLiveElement(): HTMLElement {