Why web accessibility?
Web Accessibility is not only necessary but also legal in some parts of the world but that being said you should not just add the accessibility because of legal concerns. There are almost 26% of the users who face issues in accessing a typical website. 26% is a huge number, so you should definitely cater to this audience as well. It should be our moral responsibility to make our website accessible to all.
Why another web accessibility blog?
When you google about accessibility, you'll find a lot of abstract resource saying what web accessibility is, what WCAG guidelines are, etc. But there are only few resources where you actually find well-written and explained accessible React components. This series of blog posts are my attempt to fill that gap. Note that, I am also learning, so it might be possible that I miss few things here and there, so please feel free to provide your inputs in the comment section or reach out to me over my LinkedIn Profile.
What it requires to build an Accessible List component
If you don't want to read the explanation, feel free to scroll down to the code section.
Step 1 - Use semantic elements
While building any accessible web component, the first thing we should keep in our minds is using semantic HTML components. Here, we are building a list component which we can build using plain divs
as well and looping through them, but instead we must use the semantic tags already present in HTML for this very same purpose, i.e., ul
, ol
and li
. You must already know these but just for a refresher, ul
stands for Unordered list, ol
stands for Ordered list and li
is a List item and can be used with both ul
and ol
. Choose ol
between ul
as per your need.
Step 2 - tabIndex
These ul
, ol
and li
are not keyboard accessible by default, i.e., you can't navigate through the list items through up or down arrow keys, or even can't focus using tab.
For any element to gain focus through tab
key, it should have a tabIndex
attribute. tabIndex = 0
means it's in the current line of action and once the control reaches that element, that element can be focussed using the tab
key. tabIndex = -1
means that the element is not reachable via sequential keyboard navigation. According to MDN docs recommendation, while any integer value is possible for the tabIndex
, we should restrict to using only these 2 values (0
or -1
). This is because it would cause more harm than help in terms of accessibility.
Step 3 - Event Handlers to manage focus
Good, so now we can focus our list item through tab keys. But you might have noticed that generally for a list, the preferred way to navigate through the list items is not through tab
key, but through the arrow up
and down
keys and the tab
key is used to bring the focus to the list and move it back to other elements in the page.
So, for this, we would need to write our custom event handler function to map the arrow keys to the up and down navigation actions.
onKeyDown
onKeyDown
is an event handler provided by JavaScript which intercepts keyboard key presses. Through event.key
we would figure out which key was pressed and what function to perform.
onMouseDown
Using onMouseDown
we can intercept mouse click events and perform the functions we want.
But how do we actually move focus to the active list item? For this, we would need a state variable to keep track of the active list item. And on every up
or down
key press or mouse click, we will update the activeIndex
.
So, by using the above Event Handlers we are able to assign the right activeIndex. Now, in the useEffect
, for every change of the activeIndex
we trigger the focus change.
Step 4 - Using tabIndex
properly
As we have discussed before, tabIndex
can be 0 or -1 and -1
means out of sequential keyboard navigation and 0
means in sequence. So, since we want tab
key to bring focus to the list and move it out of the list, and not use in navigating through the list items, we programmatically set the tabIndex
to -1
once the focus is on another list item and only the focussed list item has the tabIndex=0
. So, whenever we press the tab
key, the focus jumps to the next item on the page which has tabIndex=0
and since none of the other list items have tabIndex=0
, it would definitely jump out of the list.
Okay sorry for the long explanation above but I felt that was required. Now, you can take a look at the code below.
The Final Code
import React, { useState, useRef, useEffect } from "react";
export const AccessibleList = (props) => {
const { list } = props;
const [activeIndex, setActiveIndex] = useState(-1);
// Using the listRef to manage the focus
const listRef = useRef(null);
useEffect(() => {
if(listRef.current) {
const activeListItem = listRef.current.children[activeIndex];
activeListItem && activeListItem.focus();
}
}, [activeIndex]); // everytime `activeIndex` changes, focus will change accordingly
const handleKeyDown = (e) => {
switch (e.key) {
case "ArrowDown":
e.stopPropagation();
// using ` % list.length` to ensure the focus loops through the list items
setActiveIndex(prevIndex => (prevIndex + 1) % list.length);
break;
case "ArrowUp":
e.stopPropagation();
setActiveIndex(prevIndex => (prevIndex - 1 + list.length) % list.length);
break;
default: break; // do not alter the behaviour of other keys
}
}
return <ul ref={listRef} role="listbox"> // role of listbox is necessary for screen readers to understand this correctly
{(list || []).map((item, index) => {
return (
<li
key={item.id}
aria-label={item.title} // useful when `li` has nested elements
role="listitem" // this is the default role of `li` elements, but still preferrable to specify
tabIndex={activeIndex === index ? 0 : -1}
onKeyDown={handleKeyDown}
onMouseDown={() => setActiveIndex(index)} // on mouse click, just set the active index to the element which was clicked
>
{item.title}
</li>
)
})}
</ul>
}
That's it! Hope you found it useful. Feel free to add your inputs in the comments and share with your team/colleagues/friends who might need this. Thanks!