Skip to content

Commit

Permalink
fix(cdk/a11y): live announcer promise never resolved if new announcem…
Browse files Browse the repository at this point in the history
…ent 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)
  • Loading branch information
crisbeto committed Mar 30, 2022
1 parent 28b3714 commit 4896871
Show file tree
Hide file tree
Showing 2 changed files with 31 additions and 11 deletions.
10 changes: 10 additions & 0 deletions src/cdk/a11y/live-announcer/live-announcer.spec.ts
Expand Up @@ -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();

Expand Down
32 changes: 21 additions & 11 deletions src/cdk/a11y/live-announcer/live-announcer.ts
Expand Up @@ -31,6 +31,8 @@ export class LiveAnnouncer implements OnDestroy {
private _liveElement: HTMLElement;
private _document: Document;
private _previousTimeout: number;
private _currentPromise: Promise<void> | undefined;
private _currentResolve: (() => void) | undefined;

constructor(
@Optional() @Inject(LIVE_ANNOUNCER_ELEMENT_TOKEN) elementToken: any,
Expand Down Expand Up @@ -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;
});
}

Expand All @@ -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 {
Expand Down

0 comments on commit 4896871

Please sign in to comment.