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!
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.
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;
}
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;
};
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!