DEV Community

Cover image for Trap focus using javaScript
Shubham Prakash
Shubham Prakash

Posted on

Trap focus using javaScript

Setting up custom keys to focus previous/next element in the tab index

Introduction

On a web page, we have different focusable elements and they follow a default tab order. We can navigate around and change focus from one focusable element to another using Tab and Shift + Tab keys.

You can easily check this behavior on any website. Just press Tab to move your focus to the next focusable element and press Shift + Tab for the previous one.

We can manipulate this default Tab flow and create our own flow using tabindex but this is not the focus of this article. We want to use a custom key to change the focus just like we use Tab and Shift + Tab keys.

In this article, we will learn how to trap the browser focus using javaScript and assign it to UP and DOWN arrow keys to focus the next and the previous focusable elements (input box in our example)

What we are going to build

Demo page

We are going to create a web page having some input fields. We will create a function that will listen to the keypress event and change the focus of the element on pressing arrow UP and DOWN keys.

Let's begin-

Setup

  1. Creating some input fields on the page for the demo:
<div class="container">
    <h1 class="text-center">Trap focus using javaScript</h1>
    <div class="input-wrapper">
      <input type="text" placeholder="Input 1" value="">
      <input type="text" placeholder="Input 2" value="">
      <input type="text" placeholder="Input 3" value="">
      <input type="text" placeholder="Input 4" value="">
      <input type="text" placeholder="Input 5" value="">
      <button>Submit</button>
    </div>
  </div>
  1. Writing some CSS to make this ugly page a little beautiful because why not 😉

html{
  background: black;
}

.container{
  background: yellow;
  width: 100%;
  max-width: 500px;
  margin: auto;
  border-radius: 10px;
}

h1{
  padding-top: 0.4em;
}

.input-wrapper{
  background: pink;
  padding: 1em;
  display: flex;
  flex-direction: column;
  border-radius: 0 0 10px 10px;
}

.input-wrapper input{
  margin: 0.4em auto;
  padding: 0.4em;
  font-size: 1.4em;
  width: 400px
}

.text-center{
  text-align: center;
}

button{
  width: 100px;
  padding: 0.4em;
  border-radius: 4px;
  margin: auto;
  font-size: 1.2em;
  cursor: pointer;
}

The JavaScript part

We know that the browser fires DOM events on various kinds of events (of course) happening on the page.

We are going to listen to keydown events on the input fields so that whenever the user press UP or DOWN keys we will change the focus on the page to previous or the next element respectively.

Now here's a question, why I chose keydown event for this and not keypress. The answer is compatibility with different browsers. Since I'll be using event.keyCode in this example, I found using keydown or keyup instead of keypress events will work in every major browser.

Alright enough talking, let's get to the coding part-

let's begin with creating a function which we will invoke on keydown event in the browser-

function handleInputFocusTransfer(e){
// we will write code for the functionality here...
}

Now inside this function, we will write the logic for changing the focus on the page.

let's create a variable to store the reference of all the focusable elements we want to use.
In our example, we are manipulating focus for input fields but you can use any elements on the page and select them in the same way-

const focusableInputElements= document.querySelectorAll(`input`);

document.querySelectorAll will return a node list. we will create an array from this node list using spread operator as follow-

  const focusable= [...focusableInputElements]; 

At this point, we have an array focusable of all the focusable elements on the page. The current element which is in focus on the page is also present in this array. So, let's find at which index this guy is sitting-

 //get the index of current item in the "focusable" array
  const index = focusable.indexOf(document.activeElement); 

Here, document.activeElement returns the active node element on the page.

Let's also create a variable to store the index of the next element to be focussed-

  let nextIndex = 0;

I have initialized it with 0 but later we will change it on UP or DOWN arrow key press accordingly.

Now, on keyDown event, the event object has an entry keyCode which stores the ASCII (RFC 20) or Windows 1252 code corresponding to the pressed key.

It is 38 and 40 for the UP and DOWN arrow keys respectively.

Next, we will write a if-else statement which will change the value of nextIndex according to which key was pressed-

  if (e.keyCode === 38) {
    // up arrow
    e.preventDefault();
    nextIndex= index > 0 ? index-1 : 0;
    focusableInputElements[nextIndex].focus();
  }
  else if (e.keyCode === 40) {
    // down arrow
    e.preventDefault();
    nextIndex= index+1 < focusable.length ? index+1 : index;
    focusableInputElements[nextIndex].focus();
  }

In the if-block above, if the keyCode is 38 (i.e UP arrow key) we are decreasing the value of index by 1 so that just the previous focusable element in the focusableInputElements array can be focussed using focus() method provided by the DOM API.

Similarly, in the else-block, we are increasing the value of index by 1 to focus on the next focusable element.

You'll see I have also taken care of boundary conditions using a ternary operator. This is just to make sure that nextIndex always holds a positive value smaller than the size of the focusableInputElements array to avoid errors.

That's all. Now put these code together inside our handleInputFocusTransfer function and set up an event listener for keydown event on the page.

The whole javascript code now looks like this-


// Adding event listener on the page-
document.addEventListener('keydown',handleInputFocusTransfer);

function handleInputFocusTransfer(e){

  const focusableInputElements= document.querySelectorAll(`input`);

  //Creating an array from the node list
  const focusable= [...focusableInputElements]; 

  //get the index of current item
  const index = focusable.indexOf(document.activeElement); 

  // Create a variable to store the idex of next item to be focussed
  let nextIndex = 0;

  if (e.keyCode === 38) {
    // up arrow
    e.preventDefault();
    nextIndex= index > 0 ? index-1 : 0;
    focusableInputElements[nextIndex].focus();
  }
  else if (e.keyCode === 40) {
    // down arrow
    e.preventDefault();
    nextIndex= index+1 < focusable.length ? index+1 : index;
    focusableInputElements[nextIndex].focus();
  }
}

Now our web page looks like this. Note how the focus is changing on pressing UP and DOWN arrow keys-
Demo page gif

DONE!! you've done it. Check out this codepen to see it live-

https://codepen.io/ishubhamprakash/pen/OJPagqj

This is my first post on dev.to
Hope you'll like it. Smash whatever buttons this platform provides to appreciate the work let me know that you loved this post 😄

More posts coming...

Latest comments (3)

Collapse
 
pulsipherd profile image
Deneb Pulsipher • Edited

I love this, Shubham! You do a good job explaining what's going on, step-by-step, and have moved me towards understanding how to do what I'm trying to do. Thank you! I wonder if you can help me close the gap, though, and get the rest of the way. I've been trying to use your code to make the navigation menu on a site (ideally any site by just modifying the querySelectorAll for the menu) navigable with the left and right arrows. That way menus with long drop-downs could be much more useful when navigating by keyboard than having to tab all the way through them. By targeting the querySelectorAll to the elements in my navigation menu, and changing the keyCodes to 37 and 39 I can navigate the menu fine with left and right arrows, but no matter where my focus is on the page, if I type left or right it jumps me to the menu, presumably because the eventListener is on the document. I tried moving the variables out of the function, and it still works fine, but when I try to move the handleInputFocusTransfer into a conditional statement based on one of the items in the menu array being the activeElement, it doesn't work. Here's what I've got that doesn't work:

const focusableMenuElements= document.querySelectorAll(#menu-sms-main-menu>li>a);
const focusable= [...focusableMenuElements];
if (focusable[0] === document.activeElement) {
document.addEventListener('keydown',handleMenuFocusTransfer);
}
function handleMenuFocusTransfer(e){
const index = focusable.indexOf(document.activeElement);
let nextIndex = 0;
if (e.keyCode === 37) {
// left arrow
e.preventDefault();
nextIndex= index > 0 ? index-1 : 0;
focusableMenuElements[nextIndex].focus();
}
else if (e.keyCode === 39) {
// right arrow
e.preventDefault();
nextIndex= index+1 < focusable.length ? index+1 : index;
focusableMenuElements[nextIndex].focus();
}
}

I'm a noob at JS, so it could be any number of little things making it wrong, but I'd sure appreciate your help!

Collapse
 
taufik_nurrohman profile image
Taufik Nurrohman
Collapse
 
ishubhamprakash profile image
Shubham Prakash

Nice article. Thanks for sharing 😄