Due to recent surge in online usage, most of us cannot image life without internet. It is now more important than ever to build applications that are accessible. A key ingredient of an accessible web application is making sure screen reader messages are announced correctly.

HTML5 makes it easy for developers to implement screen reader messages using attributes such as aria-label. These attributes go a long way in making content and forms accessible.

However, in today’s complex web applications, we may need to write custom code to announce messages.

Consider scenarios such as the following:

  • Search completes
  • Notification appears on screen
  • Long running task completes

For these advanced cases, Angular framework provides LiveAnnouncer service.

This service can announce screen reader messages from code.

@Component({...}) export class MyComponent { constructor(liveAnnouncer: LiveAnnouncer) { liveAnnouncer.announce("Hey Google"); } }
Code language: TypeScript (typescript)

LiveAnnouncer uses an aria-live region behind the scenes to announce messages. Read more on aria-live regions here.

Unfortunately, it does not support queuing multiple messages.

This means that only the last message is announced, when announce method is invoked simultaneously.

// 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)

In the example above, search and notification components announce messages simultaneously.

It is highly likely that the message from the notification component is not announced.

Extending LiveAnnouncer

Having researched online for a solution, it turns out there is an open issue on GitHub.

I could not wait for another Angular update so I decided to roll my own ScreenReaderService.

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)

As shown in the code above, LiveAnnouncer is internally used to perform announcements.

Each call to LiveAnnouncer is invoked in a setTimeout function, with a delay calculated based on the number of announcements requested. This results in all of the announcements to be heard by the end user.

Here are some test cases I have put together for the ScreenReaderService:

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(301); expect(announcer.announce).toHaveBeenCalledWith("Page loaded", "polite", 200); tick(301); 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(301); expect(announcer.announce).toHaveBeenCalledWith("Page loaded", "polite", 200); tick(301); expect(announcer.announce).toHaveBeenCalledWith("Content loaded", "polite", 200); tick(301); expect(announcer.announce).toHaveBeenCalledWith("Footer loaded", "polite", 200); })); });
Code language: TypeScript (typescript)

Umut Esen

Umut is a certified Microsoft certified developer and has an MSc in Computer Science. He is currently working as a senior software developer in Edinburgh, UK. He is the primary author and the founder of onthecode.

Leave a Reply