Skip to content

Modus-Logo-Long-BlackCreated with Sketch.

  • Services
  • Work
  • Blog
  • Resources

    OUR RESOURCES

    Innovation Podcast

    Explore transformative innovation with industry leaders.

    Guides & Playbooks

    Implement leading digital innovation with our strategic guides.

    Practical guide to building an effective AI strategy
  • Who we are

    Our story

    Learn about our values, vision, and commitment to client success.

    Open Source

    Discover how we contribute to and benefit from the global open source ecosystem.

    Careers

    Join our dynamic team and shape the future of digital transformation.

    How we built our unique culture
  • Let's talk
  • EN
  • FR

Originally published on NG-Conf

Programming is fun, especially when you love the technology you’re working on. We at Modus Create love the web and web technologies. One of the frameworks that we work with is Angular.


NEW RESEARCH: LEARN HOW DECISION-MAKERS ARE PRIORITIZING DIGITAL INITIATIVES IN 2024.

Get Report


When you work with Angular on large scale apps, there comes a set of different challenges and problems that require diving deep into Angular. In this article, we’ll go through one such challenge: implementing keyboard navigation to a list component. An example use-case can be an autocomplete dropdown list which may have keyboard navigation.

To implement keyboard navigation entirely from scratch, we could develop a custom implementation, but that would take a bit of  time and perhaps would be re-inventing the wheel – if something is already out there that does the job.

Angular Material has a CDK (Component Dev Kit) which provides a lot of cool components and services for creating custom components & services that we can ship as libraries or use in our applications. Angular Material itself is built on top of the Angular CDK. Angular Material’s `a11y` package provides us a number of services to improve accessibility. One of those services is the `ListKeyManager` service. Which we will be using to implement keyboard navigation into our app.

Let’s dive into the code:

Diving

First, if you don’t have the `@angular/cdk` package installed in your app, run a quick `npm install @angular/cdk --save`.

We have a repo created here which you can clone/fork and work locally on our example app as we go along. We’ll keep things simple for now but there can be more complex use cases when using `ListKeyManager`. We’ll show you how we’ve implemented keyboard navigation in our demo app and you can implement this in a similar way.

Let’s go through what our demo app looks like. First, we’re loading some random users from randomuser.me api. We load them in our `app.component.ts` and then we use our `ListItemComponent` to display each user individually. We also have a search input which will filter the users based on their names.

User List

See the code below for `AppComponent`:

import { Component, OnInit } from "@angular/core";
import { UsersService } from "./core/services/users.service";
import { first } from "rxjs/operators";

@Component({
 selector: "app-root",
 templateUrl: "./app.component.html",
 styleUrls: ["./app.component.scss"]
})
export class AppComponent implements OnInit {
 users: any;
 isLoadingUsers: boolean;
 searchQuery: string;
 constructor(private usersService: UsersService) {}

 ngOnInit() {
   this.isLoadingUsers = true;
   this.usersService
     .getUsers()
     .pipe(first())
     .subscribe(users => {
       this.users = users;
       this.isLoadingUsers = false;
     });
 }
}

In our view (`app.component.html`), we have:

Loading Users

Users List


Notice that we’re looping over `users` using `*ngFor` and passing each user as an `item` in the `app-list-item` component. We’re also filtering the list using the 'filterByName' pipe which uses the value from the input above.

The `app-list-item` component just displays the image, name and email of each user. Here is what the view code looks like:

{{item.name.first}} {{item.name.last}}
{{item.email}}

Notice that the `div` with the class `item` has a conditional class being applied i.e. `item--active`. This would make sure that the active item looks different from the rest since we’re applying different styles on this class. The class `item--active` would be applied when the `isActive` property of the item is `true`. We will use this later.

Moving forward, we’ll now include `ListKeyManager` from the `@angular/cdk/a11y` package in our `app.component.ts` as:

import { ListKeyManager } from '@angular/cdk/a11y';

Then, we have to create a `KeyEventsManager` instance in our `app.component.ts` that we would use to subscribe to keyboard events. We will do it by creating a property in the `AppComponent` class as:

  export class AppComponent implements OnInit {
   users: any;
   isLoadingUsers: boolean;
   keyboardEventsManager: ListKeyManager; // <- add this
   constructor(private usersService: UsersService) {
   }
   ...
}

We have declared the property `keyboardEventsManager` but haven’t initialized it with anything. To do that, we would have to pass a `QueryList` to `ListKeyManager` constructor as it expects a `QueryList` as an argument. The question is, what would this `QueryList` be? The `QueryList` should comprise of the elements on which the navigation would be applied. I.e. the `ListItem` components. So we will first use `@ViewChildren` to create a `QueryList` and access the `ListItem` components which are in `AppComponent`‘s view. Then we will pass that `QueryList` to the `ListKeyManager`. Our AppComponent should look like this now:

import { Component, OnInit, QueryList, ViewChildren } from "@angular/core";
import { UsersService } from "./core/services/users.service";
import { first } from "rxjs/operators";
import { ListKeyManager } from "@angular/cdk/a11y";
import { ListItemComponent } from "./core/components/list-item/list-item.component"; // importing so we can use with `@ViewChildren and QueryList
@Component({
 selector: "app-root",
 templateUrl: "./app.component.html",
 styleUrls: ["./app.component.scss"]
})
export class AppComponent implements OnInit {
 users: any;
 isLoadingUsers: boolean;
 keyboardEventsManager: ListKeyManager;
 @ViewChildren(ListItemComponent) listItems: QueryList; // accessing the ListItemComponent(s) here
 constructor(private usersService: UsersService) {}

 ngOnInit() {
   this.isLoadingUsers = true;
   this.usersService
     .getUsers()
     .pipe(first())
     .subscribe(users => {
       this.users = users;
       this.isLoadingUsers = false;
       this.keyboardEventsManager = new ListKeyManager(this.listItems); // initializing the event manager here
     });
 }

Now that we have created `keyboardEventsManager`, we can initiate the keyboard events handler using a method named `handleKeyUp` on the search input as we would press `Up` and `Down` arrow keys and navigate through the list observing the active item.

...
import { ListKeyManager } from '@angular/cdk/a11y';
import { ListItemComponent } from './core/components/list-item/list-item.component';
import { UP_ARROW, DOWN_ARROW, ENTER } from '@angular/cdk/keycodes';

...
export class AppComponent implements OnInit {
 ...
 keyboardEventsManager: ListKeyManager;
 searchQuery: string;
 @ViewChildren(ListItemComponent) listItems: QueryList;
 constructor(private usersService: UsersService) {
 }

 ...
  /**
  * @author Ahsan Ayaz
  * @desc Triggered when a key is pressed while the input is focused
  */
 handleKeyUp(event: KeyboardEvent) {
   event.stopImmediatePropagation();
   if (this.keyboardEventsManager) {
     if (event.keyCode === DOWN_ARROW || event.keyCode === UP_ARROW) {
       // passing the event to key manager so we get a change fired
       this.keyboardEventsManager.onKeydown(event);
       return false;
     } else if (event.keyCode === ENTER) {
       // when we hit enter, the keyboardManager should call the selectItem method of the `ListItemComponent`
       this.keyboardEventsManager.activeItem.selectItem();
       return false;
     }
   }
 }
}

We will connect the `handleKeyUp` method to the search input in `app.component.html` on the `(keyup)` event as:

 

If you debug the functions now, they would be triggered when up/down or enter key is pressed. But this doesn’t do anything right now. The reason is that as we discussed, the active item is distinguished when the `isActive` property inside the `ListItemComponent` is `true` and the `item--active` class is therefore applied. To do that, we will keep track of the active item in the `KeyboardEventsManager` by subscribing to `keyboardEventsManager.change`. We will get the active index of the current item in navigation each time the active item changes. We just have to set the `isActive` of our `ListItemComponent` to reflect those changes in view. To do that, we will create a method `initKeyManagerHandlers` and will call it right after we initialize the `keyboardEventsManager`.

Let’s see what our app looks like now:

Loading

Keyboard Nav using UP & DOWN arrow keys

BOOM! Our list now has keyboard navigation enabled and works with the `UP` and `DOWN` arrow keys. The only thing remaining is to show the selected item on `ENTER` key press.

Notice that in our `app.component.html`, the `app-list-item` has an `@Output` emitter as:

(itemSelected)="showUserInfo($event)"

This is what the `ListItemComponent` looks like:

import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core';

@Component({
 selector: 'app-list-item',
 templateUrl: './list-item.component.html',
 styleUrls: ['./list-item.component.scss']
})
export class ListItemComponent implements OnInit {
 @Input() item;
 @Output() itemSelected = new EventEmitter();
 isActive: boolean;
 constructor() { }

 ngOnInit() {
   this.isActive = false;
 }

 setActive(val) {
   this.isActive = val;
 }

 selectItem() {
   this.itemSelected.emit(this.item);
 }

}

If you recall, in our `handleKeyUp` method inside `AppComponent`, we execute the below statement on `ENTER` key press:

 this.keyboardEventsManager.activeItem.selectItem();

The above statement is calling `ListItemComponent`‘s `selectItem` method which emits `itemSelected` to the parent component. The emitted event in the parent calls the `showUserInfo($event)` which finally alerts the message with the user name.

Let’s see how the completed app looks now:

Finished App

Selecting active item using ENTER key

Conclusion

Angular CDK provides a lot of tools and as we’re working on complex projects, we’re continuously finding out great ways to create intuitive experiences that are easy to write and maintain. If you’re interested in building your own component libraries like Angular Material, do dive into Angular CDK and paste in the comments whatever cool stuff you come up with.

Happy coding – check out our Github repo for more.

Need efficient and scalable software solutions? Learn more about our software development expertise.

Posted in Application Development
Share this

Ahsan Ayaz

Ahsan Ayaz is a Google Developer Expert in Angular. During his time at Modus, Ahsan worked as a Software Architect. Apart from building web and hybird-mobile apps based on Angular, Ionic & MEAN Stack, Ahsan contributes to open-source projects, speaks at events, writes articles and makes video tutorials.
Follow

Related Posts

  • Drag and Drop Angular CDK 7
    Angular CDK brings Drag & Drop in Beta 7

    In this article we'll go through one of the most anticipated features that all Angular…

  • Angular + React Become ReAngular
    Angular + React Become ReAngular

    Angular and React developers have revealed this morning that they are planning to merge to…

Want more insights to fuel your innovation efforts?

Sign up to receive our monthly newsletter and exclusive content about digital transformation and product development.

What we do

Our services
AI and data
Product development
Design and UX
IT modernization
Platform and MLOps
Developer experience
Security

Our partners
Atlassian
AWS
GitHub
Other partners

Who we are

Our story
Careers
Open source

Our work

Our case studies

Our resources

Blog
Innovation podcast
Guides & playbooks

Connect with us

Get monthly insights on AI adoption

© 2025 Modus Create, LLC

Privacy PolicySitemap
Scroll To Top
  • Services
  • Work
  • Blog
  • Resources
    • Innovation Podcast
    • Guides & Playbooks
  • Who we are
    • Careers
  • Let’s talk
  • EN
  • FR