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,