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.
This commit is contained in:
Kristiyan Kostadinov
2018-11-08 17:58:16 +00:00
committed by vivian-hu
parent 8b2dc82592
commit d0f4e7b220
2 changed files with 31 additions and 2 deletions
@@ -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', () => {
@@ -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!;
}
}