Skip to content

Instantly share code, notes, and snippets.

@gloriousLoaf
Last active March 16, 2021 22:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gloriousLoaf/9d342135e85f9d9d3e7f2cbe8cbf640d to your computer and use it in GitHub Desktop.
Save gloriousLoaf/9d342135e85f9d9d3e7f2cbe8cbf640d to your computer and use it in GitHub Desktop.
Hoverboard - Keyboard Accessiblity

Hoverboard Keyboard Accessibility

There are obviously a lot of ways to tackle accessibility for any mouse-oriented, visual app like this. Here is a suggestion for how to handle focus states for our little dance floor of lights!

What is the goal?

For this exercise, I set a few basic parameters. The user can tab into the container of squares with the tab key, then they can use the arrow keys to navigate around the grid while creating the same effects as a mouse user.

Another parameter is that the grid should be fixed-size and allow for a "wrap" effect at the edges of the grid, using some simple math and switch case statements. The focus moves from left to right and top to bottom. I chose 400px wide and 500 squares, like the original project.

If you would like the grid to be a different size, then you will need to adjust the width properties in the CSS, the value of const SQUARE, the values to increment and decrement for up and down arrow keys, and the break points in the case evaluations.

CSS

First, we need to address the focus style.

/* Add :focus to the transition, remove outline */
.square:focus {
  outline: none;
}

.square:hover,
.square:focus {
  transition-duration: 0s;
}

Also, for the purposes of making all the math work, we need a fixed grid. Set the .container to a fixed width. This will make more sense later.

.container {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  width: 400px;
}

JS

Next, we need event listeners and ids for focus state.

// inside the for loop, add ids, tabindex & listeners

for (let i = 0; i < SQUARES; i++) {
  const square = document.createElement('div');
  square.classList.add('square');
  
  // unique #id & tabindex of 1
  square.id = i;
  square.tabIndex = 1;
  
  // listeners to set or remove on focus & blur
  square.addEventListener('focus', () => {
    setColor(square);
  });

  square.addEventListener('blur', () => {
    removeColor(square);
  });
  
  // rest of the loop
}

Now every div has a unique id from 0 - 499, our grid is width x 20 squares by height x 25 squares. We can use the event object to check what id is in focus, what key the user pressed and do a little math.

/**
 * @desc    Add keyboard focus with arrow keys,
 *          blurring current focus and setting new
 *          focus based on keyCode. See numReset().
 * @param   event keycode
 */
container.onkeydown = (e) => {
  let currentSq = Number(e.target.id);
  document.getElementById(e.target.id).blur();

  switch (true) {
  
    // left: decrement
    case e.keyCode === 37:
      currentSq--;
      currentSq = numReset(e, currentSq);
      document.getElementById(currentSq).focus();
      break;
      
    // right: increment
    case e.keyCode === 39:
      currentSq++;
      currentSq = numReset(e, currentSq);
      document.getElementById(currentSq).focus();
      break;
      
    // up: +20
    case e.keyCode === 38:
      currentSq = currentSq - 20;
      currentSq = numReset(e, currentSq);
      document.getElementById(currentSq).focus();
      break;
      
    // down: -20
    case e.keyCode === 40:
      currentSq = currentSq + 20;
      currentSq = numReset(e, currentSq);
      document.getElementById(currentSq).focus();
      break;
  }
};

So that event listener immediately gives us a variable currentSq with a value set to the id of the div that is focused. It then blurs focus from that square, so our switch can move the focus to the correct new location.

But what is the numReset() function doing? This is where the fixed grid size comes into play. I set up rules for what should happen for values < 0 and > 499. There are also rules for how the focus should behave at the edges. Pressing right or down from square 499 takes you to 0. Pressing up from square 5 takes you to square 483, which is the last square in the column to the left of square 5.

/**
 * @desc    Fix currentSq focus for numbers
 *          beyond the scope of grid (0 - 499)
 * @params  event, currently focused square id value
 * @return  id of next square to focus, adjusted for grid
 */
const numReset = (e, sq) => {
  switch (true) {
  
    // left or up arrows at 0
    case (sq === -1 && e.keyCode === 37) || sq === -20:
      sq = 499;
      break;
      
    // up arrow anywhere else
    case (sq === -1 && e.keyCode === 38) || (sq < -1 && sq !== -1):
      sq = sq + 499;
      break;
      
    // down arrow anywhere else
    case (sq === 500 && e.keyCode === 40) || sq > 500:
      sq = sq - 499;
      break;
      
    // down or right arrows at 499
    case sq === 500 || sq === 519:
      sq = 0;
      break;
  }
  return sq;
};

Wrap Up

This code can be drier, but I left it verbose to make it readable. Break it out into more discrete functions, if you like. I also reassign the value of variables currentSq and sq a lot, again for readability. Maybe you don't want to overwrite values and would prefer to use some pass-through variables. You may need to throw some console.log()'s in there to see why some of these magic numbers matter, like -1 or 519. Play with it. Make it fun, but make it accessible!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment