Building Accessible Grids 2
Tags: a11y, webdev, html, react
Published: 2020-06-21 00:04:37

---
title: Building Accessible Grids 2
published: true
description: Adding grid-like keyboard navigation to UI components. In depth implementation
tags: #a11y #webdev #html #react
//cover_image: https://direct_url_to_image.jpg
---

Part 1: {% link https://dev.to/tryggvigy/building-accessible-grids-1-216j %} 

---

Here's what we'll build in this post. If you are reading on your phone I'm sorry, this wont work! This time we are focusing (😏) on keyboard support. 

{% codepen https://codepen.io/tryggvigy/pen/rNxyeJV default-tab=result %}

It's a playlist!

Let's quickly recap the first post. We want to build a _composite grid component_ that can be skipped with a single Tab key press and has keyboard support. Pressing an arrow key will navigate to the next interactive element.

Let's build the playlist HTML.

```html
<div class="grid" role="grid" tabindex="0">
  <!-- header row -->
  <div class="grid__header-row" role="row" aria-rowindex="1">
    <div role="columnheader" aria-colindex="1">
      <button tabindex="-1">Title</button>
    </div>
    <div role="columnheader" aria-colindex="2">
      <button tabindex="-1">Album</button>
    </div>
    <div role="columnheader" aria-colindex="3">Duration</div>
  </div>
  <!-- regular row -->
  <div class="grid__row" role="row" aria-rowindex="2">
    <div role="gridcell" aria-colindex="1">
      <div>Black Parade</div>
      <a href="#" tabindex="-1">Beyoncé</a>
    </div>
    <div role="gridcell" aria-colindex="2"></div>
    <div role="gridcell" aria-colindex="3">
      4:41
      <button class="heart" tabindex="-1">
        <span class="sr-only">Add to your liked songs</span>
        ♡
      </button>
    </div>
  </div>
  <!-- more rows ... -->
</div>
```

- 2 out of 3 headers are buttons. Perhaps they are sortable.
- First column TITLE contains the song name and list of artists.
- Second column ALBUM contains the album link if any (songs can be singles)
- Third column DURATION contains the song duration but also a heart button to add the song to your liked songs. The heart should appear when the row is focused/hovered. When invisible it should still be able to receive focus!

The key hierarchy of selectors is this
```scss
div[role="grid"]
└── div[aria-rowindex]
    └── div[aria-colindex]
        └── a, button
```

From any given button or link, we can traverse up its parent chain in the DOM and find which row and column index it belongs to. Let's move focus based on the current focus and the row and column indexes.

The first thing we have to do is remove all the buttons and links from the natural tab order.

```js
const grid = document.querySelector('.grid');

// Remove all buttons/links from the natural tab order
grid
  .querySelectorAll('a:not([tabindex="0"]), button:not([tabindex="0"])')
  .forEach(el => el.setAttribute('tabindex', '-1'));
```

Now the grid can be skipped with a single Tab key instead of the user having to tab through every link and button in the grid.

Next, let's add a keydown handle to the grid and call a function to move the focus in the direction of the arrow keys.

```js
grid.addEventListener('keydown', (e) => {
    // Prevent scrolling
    if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
      e.preventDefault();
    }
    if (e.key === 'ArrowUp') moveFocus(grid, 'up');
    if (e.key === 'ArrowDown') moveFocus(grid, 'down');
    if (e.key === 'ArrowLeft') moveFocus(grid, 'left');
    if (e.key === 'ArrowRight') moveFocus(grid, 'right');
});

function moveFocus(grid, direction) {
  const hasFocusableElement = ensureFocusableElementInGrid(grid)
  if (!hasFocusableElement) return;
  if (direction === 'up') focusUp(grid);
  if (direction === 'down') focusDown(grid);
  if (direction === 'left') focusLeft(grid);
  if (direction === 'right') focusRight(grid);
}

```
The first thing we do before moving the focus is call `ensureFocusableElementInGrid` to make sure that a focusable element is present in the grid.

```js
function ensureFocusableElementInGrid(grid) {
  const firstElem = grid.querySelectorAll('a, button')[0];
  const currentFocusable = grid.querySelector(
    '[tabindex="0"]') || firstElem;

  // Happens if the grid does not contain any interactive elements.
  if (!currentFocusable) {
    return false;
  }
  currentFocusable.setAttribute('tabindex', '0');
  return true;
}
```

Now, let's look at

`focusUp` and `focusDown`:
- find the currently focused element
- find the next cell in above/below row that has interactive elements
- If found, focus the first interactive element in the cell if going down and the last interactive element if going up.


`focusLeft` and `focusRight` require an extra step:
- find the currently focused element
- *extra!* focus the next interactive element in the cell if it exists.
- Otherwise, find the next cell in the current row that has interactive elements
- If found, focus the first interactive element in the cell if going right and the last interactive element if going left.

It's a bit of code involved but it's quite reusable:
{% codepen https://codepen.io/tryggvigy/pen/rNxyeJV default-tab=js %}

Here is a React version
{% codesandbox accessible-grid-inugu %}

## Wrapping up

I hope you can see how this can be extrapolated to lists or different kind of grids or tables. Maybe a spreadsheet for example.

The same basic principles apply.
- Remove all interactive elements within the composite component from the tab order
- On arrow key press, find the next element that should get focus
- Move the focus using [roving tabindex](https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_roving_tabindex) 


**Edit**
It's worth mentioning the [ARIA Grid: Supporting nonvisual layout and keyboard traversal](https://engineering.fb.com/web/aria-grid-supporting-nonvisual-layout-and-keyboard-traversal/) article from Facebook that touches on very similar things.