Skip to content

Commit d0f4e7b

Browse files
crisbetovivian-hu-zz
authored andcommittedNov 8, 2018
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.
1 parent 8b2dc82 commit d0f4e7b

File tree

2 files changed

+31
-2
lines changed

2 files changed

+31
-2
lines changed
 

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

+24
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,30 @@ describe('LiveAnnouncer', () => {
108108
.toBe(1, 'Expected only one live announcer element in the DOM.');
109109
}));
110110

111+
it('should clear any previous timers when a new one is started', fakeAsync(() => {
112+
expect(ariaLiveElement.textContent).toBeFalsy();
113+
114+
announcer.announce('One');
115+
tick(50);
116+
117+
announcer.announce('Two');
118+
tick(75);
119+
120+
expect(ariaLiveElement.textContent).toBeFalsy();
121+
122+
tick(25);
123+
124+
expect(ariaLiveElement.textContent).toBe('Two');
125+
}));
126+
127+
it('should clear pending timeouts on destroy', fakeAsync(() => {
128+
announcer.announce('Hey Google');
129+
announcer.ngOnDestroy();
130+
131+
// Since we're testing whether the timeouts were flushed, we don't need any
132+
// assertions here. `fakeAsync` will fail the test if a timer was left over.
133+
}));
134+
111135
});
112136

113137
describe('with a custom element', () => {

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

+7-2
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ export type AriaLivePoliteness = 'off' | 'polite' | 'assertive';
2929

3030
@Injectable({providedIn: 'root'})
3131
export class LiveAnnouncer implements OnDestroy {
32-
private readonly _liveElement: HTMLElement;
32+
private _liveElement: HTMLElement;
3333
private _document: Document;
34+
private _previousTimeout?: number;
3435

3536
constructor(
3637
@Optional() @Inject(LIVE_ANNOUNCER_ELEMENT_TOKEN) elementToken: any,
@@ -63,7 +64,8 @@ export class LiveAnnouncer implements OnDestroy {
6364
// (using JAWS 17 at time of this writing).
6465
return this._ngZone.runOutsideAngular(() => {
6566
return new Promise(resolve => {
66-
setTimeout(() => {
67+
clearTimeout(this._previousTimeout);
68+
this._previousTimeout = setTimeout(() => {
6769
this._liveElement.textContent = message;
6870
resolve();
6971
}, 100);
@@ -72,8 +74,11 @@ export class LiveAnnouncer implements OnDestroy {
7274
}
7375

7476
ngOnDestroy() {
77+
clearTimeout(this._previousTimeout);
78+
7579
if (this._liveElement && this._liveElement.parentNode) {
7680
this._liveElement.parentNode.removeChild(this._liveElement);
81+
this._liveElement = null!;
7782
}
7883
}
7984

0 commit comments

Comments
 (0)
Please sign in to comment.