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

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

<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:

<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

<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:

<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

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:

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:

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:

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

Implementing keyboard navigation with FocusKeyManager

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.

<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 .

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

@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

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

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

Learn more about keyboard navigation techniques:

Here you can find full documentation for Angular CDK ListKeyManager