How I approached keyboard accessibility on my site navigation
Tags: a11y, javascript
Published: 2020-07-01 13:55:18

Cover photo by [Jay Zhang](https://unsplash.com/@jay_zhang?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText) on [Unsplash](https://unsplash.com/s/photos/keyboards?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText)

A couple of disclaimers before I start:
- This is not a how-to. I'm not suggesting this is how keyboard accessibility _should_ be handled, this is just an article on how _I_ approached it.
- I am not an expert in accessibility and there will most certainly be things I've done here that could be better.

###Contents:###
- [The Basic Markup](#the-markup)
- [Improving the Markup with Attributes](#adding-attributes)
- [Adding Some Javascript](#adding-javascript)

TL;DR
[Check out my Codepen example](https://codepen.io/colette-wilson/full/Vweryrr)
[Check out a real world example](https://watershedlrs.com/)

### The Basic Markup <a name="the-markup"></a>
Let's take a look at some basic markup as a starting point. Typically, I might start off with something that looks like this;

```html
<header class="header">
  <nav class="nav">
    <ul class="nav__list">
      <li class="nav__item has-dropdown">
        <span>Item 1</span>
        <ul class="nav__dropdown">
          <li><a href="">Sub item 1</a></li>
          <li><a href="">Sub item 2</a></li>
          <li><a href="">Sub item 3</a></li>
        </ul>
      </li>
      <li class="nav__item has-dropdown">
        <span>Item 2</span>
        <ul class="nav__dropdown">
          <li><a href="">Sub item 1</a></li>
          <li><a href="">Sub item 2</a></li>
          <li><a href="">Sub item 3</a></li>
        </ul>
      </li>
      <li class="nav__item has-dropdown">
        <span>Item 3</span>
        <ul class="nav__dropdown">
          <li><a href="">Sub item 1</a></li>
          <li><a href="">Sub item 2</a></li>
          <li><a href="">Sub item 3</a></li>
        </ul>
      </li>
      <li class="nav__item"><a href="">Item 4</a></li>
    </ul>
  </nav>
</header>
```
As you can see I have a list of navigation items some of which have sub items which I would like to be presented as dropdowns.

In my accessibility ignorant past I was satisfied with handling the display of those dropdowns purely through CSS simply by declaring `display: none;` on the dropdown then targeting the parent `<li>` on hover and switching to `display: block;`.

As I now know this approach renders those dropdowns completely inaccessible to keyboard users. How do I fix this?

### Improving the Markup with Attributes<a name="adding-attributes"></a>
Currently there are a number of problems with the markup as it is. First off, the list items with dropdowns are not keyboard navigable. This is because some HTML elements come with built in roles, examples of these are buttons or anchor tags. Since my list items contain a span, an element that doesn't come with a role, there is no reason for the keyboard to tab to that item. Secondly, even if I could tab to these items the dropdown is only shown on hover so at the moment I can only access those dropdowns as a mouse user. 

The first step in making my navigation more keyboard friendly is to add some additional markup in the form of attributes. 

Firstly, let's give the `nav` element a label. I might have more than one type of nav element across the website, for example, the footer might contain a nav element or I might have some pagination within a nav element. It would be nice to announce what the navigation is for. To do this I'm going to add `aria-label="Main"`.

Secondly, although `<li>` is an example of an element that comes with a built in role I want to specify a role that is more accurate of what these elements actually are so I'm going to add `role="menuitem"` to my `<li>`s.

Thirdly, I need to be able to tab to my `<li>`s. For this I'm going to add `tabindex="0"` to the items that contain a dropdown. Giving the attribute a value of zero allows that element to become focusable sequentially in keyboard navigation.

Lastly, I want to add a couple more attributes that it make clear that this item has a popup which is currently not expanded so I also want to add `aria-haspopup="true" aria-expanded="false"` to my list items containing a dropdown.

The markup for my list items now looks like this;
```html
<li class="nav__item has-dropdown" role="menuitem" aria-haspopup="true" aria-expanded="false" tabIndex="0">
  <span>Item 1</span>
  <ul class="nav__dropdown">
    <li><a href="">Sub item 1</a></li>
    <li><a href="">Sub item 2</a></li>
    <li><a href="">Sub item 3</a></li>
  </ul>
</li>
```
Great, now I'm able to tab through my main navigation items but how do I get to the dropdowns?

### Adding Some Javascript<a name="adding-javascript"></a>

At this point I should say that there is a CSS solution to this problem. By using the `:focus-within` pseudo-class I could add `display: block;` to the `<li>` should one of the links within that item receive focus. This may be appropriate for smaller nav structures but my project featured a structure that I felt was a little too complex and I decided it was best to employ an alternative solution.

Firstly, I want to target all the list items that have a dropdown. I could do this with `const navItems = document.querySelectorAll(".nav__item.has-dropdown")`, however, I prefer to separate my css from my js and to make it clear that there is javascript attached to these items I'm going to add a js specific class of `js-nav-item`. Now that I have them stored as a variable I can loop over them and add a keydown event listener which will invoke a function called `handleKeydown`. My `handleKeydown` function will look like this;

```js
const handleKeydown = (evt) => {
  if (evt.keyCode === 13) {
    evt.preventDefault()
    evt.currentTarget.setAttribute("aria-expanded", "true")
    evt.currentTarget.querySelector("a").focus()
  }

  if (evt.keyCode === 27) {
    evt.currentTarget.setAttribute("aria-expanded", "false")
    evt.currentTarget.focus()
  }
}
```
This function listens for a key press and if the key code matches the code for the Enter key will change the value of the `aria-expanded` attribute to `true`. It will also find the first `<a>` within the list item and give it focus. If the key code matches the code for the Escape key it will change the value of the `aria-expanded` attribute to `false` and will return the focus to the navigation item. 

Great but at this point my dropdown still won't actually display. I need to update my CSS to target the `aria-expanded` attribute and set `display: block;` when true. Like so, 

```CSS
.nav__item.has-dropdown[aria-expanded="true"] .nav__dropdown {
  display: block;
}
```

This is good progress. I can tab through my nav items, I can open the dropdown by pressing Enter, and I can close the dropdown by pressing Escape. However, although I can tab through the links in the dropdown on Chrome, Firefox will just tab to the next top-level nav item -- I need to be able to cycle through the dropdown links somehow. Also, if I tab to next top-level item without first closing the dropdown it will remain open. I also want the dropdown to close when the focus is not on it's parent. 

Let's tackle the dropdown links. It would be nice if, when the dropdown is open, I could cycle through the links using the arrow keys. To do this I'm going to expand my `handleKeydown` function. I want to be able to select the dropdown in my js so, as before, I want to add a js specific class of `js-nav-dropdown` to all the dropdown elements. Now, I can get all the links within the dropdown and add a keydown event listener which will invoke a `handleDropdownKeydown` function. My updated `handleKeydown` function now looks like this;

```js
const handleKeydown = (evt) => {
  if (evt.keyCode === 13) {
    evt.preventDefault()
    evt.currentTarget.setAttribute("aria-expanded", "true")
    evt.currentTarget.querySelector("a").focus()
    
    // Target dropdown and call function to cycle through dropdown links
    let dropdown = evt.currentTarget.querySelector(".js-nav-dropdown")
    handleDropdownKeydown(dropdown)
  }

  if (evt.keyCode === 27) {
    evt.currentTarget.setAttribute("aria-expanded", "false")
    evt.currentTarget.focus()
  }
}
```

And my `handleDropdownKeydown` function looks like this;

```js
const handleDropdownKeydown = (parent) =>  {
  const links = parent.querySelectorAll("a")
  links.forEach((el, i) => {
    el.addEventListener("keydown", (evt) => {
      if (evt.keyCode === 40 || evt.keyCode === 39) {
        let next = links[i + 1] || null
        evt.preventDefault()
        next !== null && next.focus()
      }
      if (evt.keyCode === 38 || evt.keyCode === 37) {
        let prev = links[i - 1] || null
        evt.preventDefault()
        prev !== null && prev.focus()
      }
      if (evt.keyCode === 13) {
        evt.stopPropagation()
      }
    })
  })
}
```

In this function I am selecting all the links within the dropdown and looping over each of them to add a keydown event listener. If the keyCode for the event is 40 (up arrow) or 39 (right arrow) I want to add focus to the next link. If the keyCode is  38 (down arrow) or 37 (left arrow) I'd like to add focus to the previous link. If the keyCode is 13 (Enter key) I want the link to take me to it's destination, however, in my `handleKeydown` function I have prevented the default action. Because of event bubbling this means that pressing Enter when focused on a dropdown link won't do anything so I need to invoke the `stopPropogation()` method. Excellent, now I can cycle through the dropdown links and they will function as expected. 

The very last thing I want to do is to close the dropdown if the focus moves on to another top-level nav item. To do this I want to loop over my nav items and add a focus event listener which will call a `handleFocus` function. `handleFocus` will loop over all the items and update the `aria-expanded` attribute to `false` which will close any open dropdowns. 

That's pretty much it.