Skip to content

Commit 7c2a095

Browse files
crisbetoVivian Hu
authored and
Vivian Hu
committed
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.
1 parent b7ef6af commit 7c2a095

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
@@ -231,6 +231,22 @@ describe('CdkAriaLive', () => {
231231

232232
expect(announcer.announce).toHaveBeenCalledWith('Newest content', 'assertive');
233233
}));
234+
235+
it('should not announce the same text multiple times', fakeAsync(() => {
236+
fixture.componentInstance.content = 'Content';
237+
fixture.detectChanges();
238+
invokeMutationCallbacks();
239+
flush();
240+
241+
expect(announcer.announce).toHaveBeenCalledTimes(1);
242+
243+
fixture.detectChanges();
244+
invokeMutationCallbacks();
245+
flush();
246+
247+
expect(announcer.announce).toHaveBeenCalledTimes(1);
248+
}));
249+
234250
});
235251

236252

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,14 +131,21 @@ export class CdkAriaLive implements OnDestroy {
131131
.observe(this._elementRef)
132132
.subscribe(() => {
133133
// Note that we use textContent here, rather than innerText, in order to avoid a reflow.
134-
const element = this._elementRef.nativeElement;
135-
this._liveAnnouncer.announce(element.textContent, this._politeness);
134+
const elementText = this._elementRef.nativeElement.textContent;
135+
136+
// The `MutationObserver` fires also for attribute
137+
// changes which we don't want to announce.
138+
if (elementText !== this._previousAnnouncedText) {
139+
this._liveAnnouncer.announce(elementText, this._politeness);
140+
this._previousAnnouncedText = elementText;
141+
}
136142
});
137143
});
138144
}
139145
}
140146
private _politeness: AriaLivePoliteness = 'off';
141147

148+
private _previousAnnouncedText?: string;
142149
private _subscription: Subscription | null;
143150

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

0 commit comments

Comments
 (0)