Skip to content

Commit d9c9eaa

Browse files
committed
fix(a11y): aria-live directive announcing the same text multiple times
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.
1 parent 8b2dc82 commit d9c9eaa

File tree

2 files changed

+25
-2
lines changed

2 files changed

+25
-2
lines changed

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,22 @@ describe('CdkAriaLive', () => {
207207

208208
expect(announcer.announce).toHaveBeenCalledWith('Newest content', 'assertive');
209209
}));
210+
211+
it('should not announce the same text multiple times', fakeAsync(() => {
212+
fixture.componentInstance.content = 'Content';
213+
fixture.detectChanges();
214+
invokeMutationCallbacks();
215+
flush();
216+
217+
expect(announcer.announce).toHaveBeenCalledTimes(1);
218+
219+
fixture.detectChanges();
220+
invokeMutationCallbacks();
221+
flush();
222+
223+
expect(announcer.announce).toHaveBeenCalledTimes(1);
224+
}));
225+
210226
});
211227

212228

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,14 +126,21 @@ export class CdkAriaLive implements OnDestroy {
126126
.observe(this._elementRef)
127127
.subscribe(() => {
128128
// Note that we use textContent here, rather than innerText, in order to avoid a reflow.
129-
const element = this._elementRef.nativeElement;
130-
this._liveAnnouncer.announce(element.textContent, this._politeness);
129+
const elementText = this._elementRef.nativeElement.textContent;
130+
131+
// The `MutationObserver` fires also for attribute
132+
// changes which we don't want to announce.
133+
if (elementText !== this._previousAnnouncedText) {
134+
this._liveAnnouncer.announce(elementText, this._politeness);
135+
this._previousAnnouncedText = elementText;
136+
}
131137
});
132138
});
133139
}
134140
}
135141
private _politeness: AriaLivePoliteness = 'off';
136142

143+
private _previousAnnouncedText?: string;
137144
private _subscription: Subscription | null;
138145

139146
constructor(private _elementRef: ElementRef, private _liveAnnouncer: LiveAnnouncer,

0 commit comments

Comments
 (0)