Skip to content

Commit

Permalink
fix(a11y): avoid overlapping or left over timers in live announcer (#…
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
crisbeto authored and Vivian Hu committed Nov 12, 2018
1 parent 2902d0b commit a93d3a5
Show file tree
Hide file tree
Showing 2 changed files with 31 additions and 2 deletions.
24 changes: 24 additions & 0 deletions src/cdk/a11y/live-announcer/live-announcer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
9 changes: 7 additions & 2 deletions src/cdk/a11y/live-announcer/live-announcer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -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!;
}
}

Expand Down

0 comments on commit a93d3a5

Please sign in to comment.