Angular provides LiveAnnouncer service to trigger screen reader announcements programmatically. It works well in simple scenarios but fails to queue messages when used concurrently. In this post, I will show you how to extend LiveAnnouncer to announce simultaneous messages.
Table of contents
Problem demonstration
There are many things to consider when it comes to building accessible web applications. One important consideration is visually impaired users – allowing them to interact by hearing where they cannot see. This includes announcing screen reader messages in a timely manner.
The following example demonstrates how to use LiveAnnouncer service in Angular:
@Component({...})
export class MyComponent {
constructor(liveAnnouncer: LiveAnnouncer) {
liveAnnouncer.announce("Hey Google");
}
}
Code language: TypeScript (typescript)
While the above works fine, the problem surfaces when multiple components use announce
method:
// Search component
liveAnnouncer.announce("Search in progress");
// Notification component
liveAnnouncer.announce("You have a new message");
// Search component
liveAnnouncer.announce("Search complete");
Code language: TypeScript (typescript)
LiveAnnouncer works by updating the text content of a single aria-live
region within the HTML document object model. It always overwrites the text content when your code invokes announce
.
If notification service calls announce
while a search is in progress and completes quickly, a visually impaired user would not hear You have a new message. This is not a great experience as the user would not know about the new message they have just received.
Extending LiveAnnouncer
Let’s create a singleton ScreenReaderService
, which will internally use LiveAnnouncer.
We need to delay each call to LiveAnnouncer using the setTimeout
function.
We also need to keep track of how many times our application wants to announce screen reader messages – with a counter. Extending each delay based on the counter creates a simple queue of announcements.
Putting it all together:
import { LiveAnnouncer } from "@angular/cdk/a11y";
import { Injectable } from "@angular/core";
@Injectable({
providedIn: "root"
})
export class ScreenReaderService {
private counter = 0;
constructor(private announcer: LiveAnnouncer) { }
announce(message: string): void {
setTimeout(() => {
this.announcer.announce(message, "polite", 200);
this.counter--;
}, 300 * this.counter);
this.counter++;
}
}
Code language: TypeScript (typescript)
Waiting a varying period of time between each announcement allows screen reader software to read messages correctly.
The following are some of the test cases that helped me come up with the solution above. I am using the fakeAsync
zone to simulate the passage of time using the tick
method.
import { LiveAnnouncer } from "@angular/cdk/a11y";
import { discardPeriodicTasks, fakeAsync, flush, TestBed, tick } from "@angular/core/testing";
import { ScreenReaderService } from "./screen-reader.service";
describe("ScreenReaderService", () => {
let service: ScreenReaderService;
let announcer: any;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{
provide: LiveAnnouncer,
useValue: jasmine.createSpyObj("LiveAnnouncer", ["announce"])
}
]
});
service = TestBed.inject(ScreenReaderService);
announcer = TestBed.inject(LiveAnnouncer);
});
it("should be created", () => {
expect(service).toBeTruthy();
});
it("should announce message", fakeAsync(() => {
service.announce("Page loaded");
tick(0);
expect(announcer.announce).toHaveBeenCalledWith("Page loaded", "polite", 200);
}));
it("should announce two simultaneous messages correctly", fakeAsync(() => {
service.announce("Page loaded");
service.announce("Content loaded");
tick(300);
expect(announcer.announce).toHaveBeenCalledWith("Page loaded", "polite", 200);
tick(300);
expect(announcer.announce).toHaveBeenCalledWith("Content loaded", "polite", 200);
}));
it("should announce three simultaneous messages correctly", fakeAsync(() => {
service.announce("Page loaded");
service.announce("Content loaded");
service.announce("Footer loaded");
tick(300);
expect(announcer.announce).toHaveBeenCalledWith("Page loaded", "polite", 200);
tick(300);
expect(announcer.announce).toHaveBeenCalledWith("Content loaded", "polite", 200);
tick(300);
expect(announcer.announce).toHaveBeenCalledWith("Footer loaded", "polite", 200);
}));
});
Code language: TypeScript (typescript)
Now that we have a service for screen reader announcements, all we need to do is use it in our components.
This is how I solved the problem of LiveAnnouncer not queueing messages!
Are you considering accessibility when building your Angular applications? Share your thoughts in the comments below!