Skip to content

Commit 4896871

Browse files
committed
fix(cdk/a11y): live announcer promise never resolved if new announcement comes in (#24700)
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. (cherry picked from commit b372f68)
1 parent 28b3714 commit 4896871

File tree

2 files changed

+31
-11
lines changed

2 files changed

+31
-11
lines changed

src/cdk/a11y/live-announcer/live-announcer.spec.ts

+10
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,16 @@ describe('LiveAnnouncer', () => {
112112
expect(spy).toHaveBeenCalled();
113113
}));
114114

115+
it('should resolve the returned promise if another announcement is made before the timeout has expired', fakeAsync(() => {
116+
const spy = jasmine.createSpy('announce spy');
117+
announcer.announce('something').then(spy);
118+
tick(10);
119+
announcer.announce('something').then(spy);
120+
tick(100);
121+
122+
expect(spy).toHaveBeenCalledTimes(2);
123+
}));
124+
115125
it('should ensure that there is only one live element at a time', fakeAsync(() => {
116126
fixture.destroy();
117127

src/cdk/a11y/live-announcer/live-announcer.ts

+21-11
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export class LiveAnnouncer implements OnDestroy {
3131
private _liveElement: HTMLElement;
3232
private _document: Document;
3333
private _previousTimeout: number;
34+
private _currentPromise: Promise<void> | undefined;
35+
private _currentResolve: (() => void) | undefined;
3436

3537
constructor(
3638
@Optional() @Inject(LIVE_ANNOUNCER_ELEMENT_TOKEN) elementToken: any,
@@ -115,17 +117,23 @@ export class LiveAnnouncer implements OnDestroy {
115117
// second time without clearing and then using a non-zero delay.
116118
// (using JAWS 17 at time of this writing).
117119
return this._ngZone.runOutsideAngular(() => {
118-
return new Promise(resolve => {
119-
clearTimeout(this._previousTimeout);
120-
this._previousTimeout = setTimeout(() => {
121-
this._liveElement.textContent = message;
122-
resolve();
123-
124-
if (typeof duration === 'number') {
125-
this._previousTimeout = setTimeout(() => this.clear(), duration);
126-
}
127-
}, 100);
128-
});
120+
if (!this._currentPromise) {
121+
this._currentPromise = new Promise(resolve => (this._currentResolve = resolve));
122+
}
123+
124+
clearTimeout(this._previousTimeout);
125+
this._previousTimeout = setTimeout(() => {
126+
this._liveElement.textContent = message;
127+
128+
if (typeof duration === 'number') {
129+
this._previousTimeout = setTimeout(() => this.clear(), duration);
130+
}
131+
132+
this._currentResolve!();
133+
this._currentPromise = this._currentResolve = undefined;
134+
}, 100);
135+
136+
return this._currentPromise;
129137
});
130138
}
131139

@@ -144,6 +152,8 @@ export class LiveAnnouncer implements OnDestroy {
144152
clearTimeout(this._previousTimeout);
145153
this._liveElement?.remove();
146154
this._liveElement = null!;
155+
this._currentResolve?.();
156+
this._currentPromise = this._currentResolve = undefined;
147157
}
148158

149159
private _createLiveElement(): HTMLElement {

0 commit comments

Comments
 (0)