From a150494202b6444684c733c3c0c2058c2f21e1ef Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 8 Nov 2018 18:00:43 +0000 Subject: [PATCH] fix(a11y): aria-live directive announcing the same text multiple times (#13467) The `CdkAriaLive` directive uses a `MutationObserver` to monitor for DOM content changes and to announce them through the `LiveAnnouncer`. Since the `MutationObserver` also fires for things like attribute changes, the same text can be announced more than once. This is visible on the calendar where the same text is announced twice when changing views. --- .../a11y/live-announcer/live-announcer.spec.ts | 16 ++++++++++++++++ src/cdk/a11y/live-announcer/live-announcer.ts | 11 +++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/cdk/a11y/live-announcer/live-announcer.spec.ts b/src/cdk/a11y/live-announcer/live-announcer.spec.ts index d9b18ec0d92..73c661027f2 100644 --- a/src/cdk/a11y/live-announcer/live-announcer.spec.ts +++ b/src/cdk/a11y/live-announcer/live-announcer.spec.ts @@ -231,6 +231,22 @@ describe('CdkAriaLive', () => { expect(announcer.announce).toHaveBeenCalledWith('Newest content', 'assertive'); })); + + it('should not announce the same text multiple times', fakeAsync(() => { + fixture.componentInstance.content = 'Content'; + fixture.detectChanges(); + invokeMutationCallbacks(); + flush(); + + expect(announcer.announce).toHaveBeenCalledTimes(1); + + fixture.detectChanges(); + invokeMutationCallbacks(); + flush(); + + expect(announcer.announce).toHaveBeenCalledTimes(1); + })); + }); diff --git a/src/cdk/a11y/live-announcer/live-announcer.ts b/src/cdk/a11y/live-announcer/live-announcer.ts index 9dfd0aa9a1e..a8807f2de13 100644 --- a/src/cdk/a11y/live-announcer/live-announcer.ts +++ b/src/cdk/a11y/live-announcer/live-announcer.ts @@ -131,14 +131,21 @@ export class CdkAriaLive implements OnDestroy { .observe(this._elementRef) .subscribe(() => { // Note that we use textContent here, rather than innerText, in order to avoid a reflow. - const element = this._elementRef.nativeElement; - this._liveAnnouncer.announce(element.textContent, this._politeness); + const elementText = this._elementRef.nativeElement.textContent; + + // The `MutationObserver` fires also for attribute + // changes which we don't want to announce. + if (elementText !== this._previousAnnouncedText) { + this._liveAnnouncer.announce(elementText, this._politeness); + this._previousAnnouncedText = elementText; + } }); }); } } private _politeness: AriaLivePoliteness = 'off'; + private _previousAnnouncedText?: string; private _subscription: Subscription | null; constructor(private _elementRef: ElementRef, private _liveAnnouncer: LiveAnnouncer,