Skip to content

fix(a11y): aria-live directive announcing the same text multiple times #13467

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/cdk/a11y/live-announcer/live-announcer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,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);
}));

});


Expand Down
11 changes: 9 additions & 2 deletions src/cdk/a11y/live-announcer/live-announcer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about this, I could definitely see times when we would want to announce the same text twice in a row (e.g. if you have a list of some component that announces some text on focus and the user is tabbing through).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You wouldn't need to announce text on focus yourself since screen readers announce whatever focus lands on anyway. Another way we could avoid the issue is to debounce the content changes, or to clear the _previousAnnouncedText after a timeout.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I know that's not how we'd actually implement something like that, but the point is that I do think there are times we might want to announce the same text twice intentionally. Debouncing sounds like the best solution, or if that's too difficult then clearing the variable after a timeout.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting up the debouncing should be pretty easy (we just need to pipe another operator), but thinking through it again, I'm not sure how this would happen in practice. In order for somebody to trigger the callback manually for the same text, they would have to swap out the content with a clone or change an attribute that doesn't affect the text content at all. It would look something like this which seems impractical:

someEl.innerHTML = 'something';

// later on
someEl.innerHTML = 'something';

// or
someEl.setAttribute('something', true);

Also note that these changes are to the CdkAriaLive directive, not the LiveAnnouncer service.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, I missed that it was for the directive only. In that case its probably fine, since the whole point is to announce something when the content changes. Sorry for the confusion.

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,
Expand Down