Doing A11y easily with Angular CDK. Keyboard-Navigable Lists
AngularInDepth is moving away from Medium. This article, its updates and more recent articles are hosted on the new platform inDepth.dev
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:
- Keyboard list navigation techniques
- Angular CDK
ListKeyManager
- 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, settabindex="0"
to the newly selected item and callfocus
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 ListKeyManager
— ActiveDescendantKeyManager
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();
}}
- Here we’re querying all projected
ListItemComponent
’s viaContentChildren
- Instantiating
FocusKeyManager
via passing all queriedListItemComponent
’s. - 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:
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:
- Roving tabindex — https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets
- Aria-activedescendant — https://www.w3.org/WAI/GL/wiki/Using_aria-activedescendant_to_allow_changes_in_focus_within_widgets_to_be_communicated_to_Assistive_Technology
Here you can find full documentation for Angular CDK ListKeyManager
- Angular CDK
ListKeyManager
— https://material.angular.io/cdk/a11y/overview#basic-usage