Doing A11y easily with Angular CDK. Keyboard-Navigable Lists

Post Editor

In this article we are going through doing A11y easily with Angular CDK. Keyboard-Navigable Lists.

6 min read
post

Doing A11y easily with Angular CDK. Keyboard-Navigable Lists

In this article we are going through doing A11y easily with Angular CDK. Keyboard-Navigable Lists.

post
post
6 min read

When web applications are properly designed and coded the users of assistive technologies can use them easily. However, we’re not always paying enough attention to the a11y issues. One of the reasons is that it may require significant efforts to make applications accessible.

Luckily, there are tools that can speed up a11y development for our Angular applications. A number of powerful tools are distributed within @angular/cdk package.

One of the issues the Angular CDK can help us with is keyboard navigation in lists. In this article I’m going to describe how to implement keyboard navigation for lists with Angular CDK:

  1. Keyboard list navigation techniques
  2. Angular CDK ListKeyManager
  3. Implementing keyboard navigation with ListKeyManager

Keyboard list navigation techniques
Link to this section

From time to time we have to implement keyboard navigation in menus, tables, trees, and other components. But generally, all those elements are lists. And that’s why we’re going to discuss how to implement keyboard navigation for lists so that you can use these techniques for the other elements.

There are two common techniques that can help us to implement keyboard navigation for lists. Each technique gives us the ability to interact with the component using arrow keys. Also, it allows assistive technologies like screen readers to announce information about the control.

Roving tabindex

As I see it, roving tabindex is the most popular technique. The main idea of the roving tabindex technique is to make component items focus-able by using only arrow keys, and not the tab key. In that case, we’ll be able to tab through a page and when we have the focus on a navigable component the user will be able to press arrow keys and change the focused item.

That technique works perfectly with the majority of assistive technologies because it actually focuses the selected item. Meanwhile, all screen readers always announce newly focused elements on the page.

To implement the roving tabindex technique we have to do the following:

  • Set all elements in the group tabindex="-1" to be not reachable with a keyboard.
  • Set selected element tabindex="0" to be focus-able in the direct order.
  • Then, listen for arrow keys keydown events. When you receive a keydown event, set tabindex="-1" to the currently selected item, then, set tabindex="0" to the newly selected item and call focus on it.

Let’s check an example

<>Copy
<ul> // selected item <li tabindex="0">Apples</li> <li tabindex="-1">Bananas</li> <li tabindex="-1">Cherries</li> <li tabindex="-1">Pineapple</li> </ul>

Here we have a list of elements that can be selected using the roving tabindex technique. After pressing the down arrow key we’ll have the following state:

<>Copy
<ul> <li tabindex="-1">Apples</li> // selected item <li tabindex="0">Bananas</li> <li tabindex="-1">Cherries</li> <li tabindex="-1">Pineapple</li> </ul>

As you notice, after pressing the arrow key we’re moving tabindex="0" to the next element and focusing it. After focusing, the assistive technology will be able to announce Bananas for the user.

aria-activedescendant

The second technique is based on the aria-activedescendant attribute. The aria-activedescendant attribute accepts an element id and announces the appropriate content when the selected element changes. Also, an element with aria-activedescendant attribute has to be focused.

That technique also requires listening to arrow keys and moving the selected element manually. But in that case, you just need to update one parent element.

Let’s have an example here

<>Copy
<ul aria-activedescendant="cherries" tabindex="0"> <li id="apples">Apples</li> <li id="bananas">Bananas</li> // selected item <li id="cherries">Cherries</li> <li id="pineapple">Pineapple</li> </ul>

Here we have a list of elements which can be selected using the aria-activedescendant technique. After pressing the down arrow key we’ll have the following state:

<>Copy
<ul aria-activedescendant="pineapple" tabindex="0"> <li id="apples">Apples</li> <li id="bananas">Bananas</li> <li id="cherries">Cherries</li> // selected item <li id="pineapple">Pineapple</li> </ul>

As you can see here, after pressing arrow key we’re setting the aria-activedescendant attribute of the ul element to be the id of the newly selected item. After focusing the assistive technology will be able to announce pineapple for the user.

Both these techniques allow implementing keyboard navigation for lists which will be completely accessible for assistive technologies. But it is quite resource intensive to implement it each time manually. Here is a place where Angular CDK ListKeyManager comes in hand.

ListKeyManager
Link to this section

ListKeyManager provides the ability to create keyboard-navigable lists easily in a few lines of code. Generally, ListKeyManager just an event handler which calls the appropriate methods on the passed list items depending on a triggered event.

Each component that uses ListKeyManager will generally do three things:

  • Create a @ViewChildren query for the options being managed.
  • Initialize the ListKeyManager, passing in the options list.
  • Forward keyboard events from the managed component to the ListKeyManager.

Meanwhile, each list item should implement the ListKeyManagerOption interface:

<>Copy
interface ListKeyManagerOption { disabled?: boolean; getLabel?(): string; }

As I said previously there are two techniques to manage keyboard navigation for lists: roving tabindex and aria-activedescendant attribute. Based on these techniques, Angular CDK provides two varieties of ListKeyManagerActiveDescendantKeyManager and FocusKeyManager .

ActiveDescendantKeyManager

ActiveDescendantKeyManager is intended to be used with aria-activedescendant attribute. In that case, each list item should implement Highlightable interface:

<>Copy
interface Highlightable extends ListKeyManagerOption { setActiveStyles(): void; setInactiveStyles(): void; }

FocusKeyManager

FocusKeyManager is intended to be used when options will receive the browser focus directly. In that case, each list item has to implement the FocusableOption interface:

<>Copy
interface FocusableOption extends ListKeyManagerOption { focus(): void; }

Implementing keyboard navigation with FocusKeyManager
Link to this section

Content imageContent image

Here we’ve learned what ListKeyManager is and how to use it. Now it’s a time to implement keyboard navigation for lists. As I said previously, keyboard navigation will be useful for a number of different widgets — select, menu, trees, tables and so on. But to keep the article clean I decided to implement keyboard navigation just for lists using FocusKeyManager.

Actually, our list component will consist of two parts: list and list item. And will have the following API.

<>Copy
<my-list> <my-list-item>Apples</my-list-item> <my-list-item>Bananas</my-list-item> <my-list-item>Cherries</my-list-item> </my-list>

Let’s start with implementing ListItemComponent .

<>Copy
import { FocusableOption } from '@angular/cdk/a11y'; @Component({ selector: 'my-list-item', host: { tabindex: '-1', role: 'list-item', }, template: '{{ fruit }}', }) export class ListItemComponent implements FocusableOption { @Input() fruit: string; disabled: boolean; constructor(private element: ElementRef) { } getLabel(): string { return this.fruit; } focus() { this.element.nativeElement.focus(); } }

Here we have a ListItemComponent which implements FocusableOption to become focus-able via FocusKeyManager. FocusKeyManager will call the focus methods for the element that has to be focused next.

Also, as you may notice, I’ve added tabindex="-1" to make list items not accessible via tab key.

Then, we need to implement ListComponent

<>Copy
@Component({ selector: 'my-list', host: { role: 'list' }, template: '<ng-content></ng-content>', }) export class ListComponent implements AfterContentInit { // 1. Query all child elements @ContentChildren(ListItem) items: QueryList<ListItem>; // FocusKeyManager instance private keyManager: FocusKeyManager<ListItem>; ngAfterContentInit() { // 2. Instantiate FocusKeyManager this.keyManager = new FocusKeyManager(this.items) // 3. Enabling wrapping .withWrap(); } }
  1. Here we’re querying all projected ListItemComponent’s via ContentChildren
  2. Instantiating FocusKeyManager via passing all queried ListItemComponent’s.
  3. Enabling Wrapping. Which means, that if you press the down arrow on the last element, ListComponent will select the first element.

The last thing we need to perform here is to proxy keypress events from ListComponent to FocusKeyManager instance

<>Copy
export class ListComponent implements AfterContentInit { @HostListener('keydown', ['$event']) onKeydown(event) { this.keyManager.onKeydown(event); } }

And that’s it! We did it! ? Here is the result:

Keyboard-navigable Fruits list

Recap
Link to this section

We’ve built keyboard navigation for lists using ListKeyManager, which works perfectly with assistive technologies! ? Now you know how to deal with keyboard navigation easily and how to build accessible menus, tables, trees and other components using the Angular CDK.

Now we’re ready for new challenges!

Stay tuned and let me know if you have any particular CDK topics you would like to hear about!

Resources
Link to this section

Learn more about keyboard navigation techniques:

Here you can find full documentation for Angular CDK ListKeyManager

Share

About the author

author_image

I’m a Solution Architect at Akveo, open-source contributor and tech author from Belarus. I passionate about sharing knowledge and digging into the depths of technologies. Google Developer Expert.

author_image

About the author

Nikita Poltoratsky

I’m a Solution Architect at Akveo, open-source contributor and tech author from Belarus. I passionate about sharing knowledge and digging into the depths of technologies. Google Developer Expert.

About the author

author_image

I’m a Solution Architect at Akveo, open-source contributor and tech author from Belarus. I passionate about sharing knowledge and digging into the depths of technologies. Google Developer Expert.

Looking for a JS job?
Job logo
NODE.JS / ANGULAR DEVELOPER(SENIOR)

Interon IT Solutions LLC

Worldwide
Remote
$115k - $125k
Job logo
Sr. Developer-.Net/Angular/Node

PRICE WATERHOUSE COOPERS

Worldwide
Remote
$81k - $103k
Job logo
Angular Developer

Software Technology, Inc.

Worldwide
Remote
$75k - $100k
Job logo
Angular Developer

GenSpark

United States
Remote
$60k - $70k
More jobs

Featured articles

blockchainpost
19 July 202212 min read
An Introduction to Blockchain

Learn the fundamentals of a blockchain starting from first principles. We'll cover hashing, mining, consensus and more. After reading this article, you'll have a solid foundation upon which to explore platforms like Ethereum and Solana.

blockchainpost
19 July 202212 min read
An Introduction to Blockchain

Learn the fundamentals of a blockchain starting from first principles. We'll cover hashing, mining, consensus and more. After reading this article, you'll have a solid foundation upon which to explore platforms like Ethereum and Solana.

Read more
blockchainpostAn Introduction to Blockchain

19 July 2022

12 min read

Learn the fundamentals of a blockchain starting from first principles. We'll cover hashing, mining, consensus and more. After reading this article, you'll have a solid foundation upon which to explore platforms like Ethereum and Solana.

Read more