Building an accessible List Component in React JS

Building an accessible List Component in React JS

Featured on Hashnode

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!

Did you find this article valuable?

Support Shubhaw Kumar by becoming a sponsor. Any amount is appreciated!