Skip to content

Commit aec14a9

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 4b15b78 commit aec14a9

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)