Ever since I started programming in 2020, debugging JavaScript was one of the things I had trouble dealing with properly or even understanding debugging tools. I watched video after video on how to work with development tools and such to debug code, and it always seemed so obvious. But when I tried to apply this newly acquired knowledge myself, I failed miserably.
That changed, though, when my problems had to do with accessibility. I think my heart wanted to solve these kinds of problems so bad that it finally 'clicked' in my brain. And on top of that, I also finally managed to understand the React hook useRef
. Neat.
Introduction
Hi. My name is Julia and I'm a 35 yr old self-taught front-end developer and accessibility advocate trying to make the world more accessible for everyone.
To practice my React and accessibility skills at the same time, I thought it would be a good idea to create a simple CRUD application and make it fully accessible. I decided to go with the good old To-Do List because I wanted a safe project to start with, which would then give me the confidence to create something more complicated.
Well, that didn't work out the way I had planned. I was struggling with an accessibility issue, and it took me a while to figure out how to fix it.
CRUD Project
I'm not going to go into detail about how to create a To-Do List but will jump right into the accessibility testing phase. Therefore, the reader should have prior knowledge of React and Hooks. The code is of course linked and can be viewed by anyone.
When creating and testing the app, I made sure that it was operable for keyboard users, perceptible for people with e.g. low vision (font size, color contrast) as well as for screen reader users.
Project setup
For this project, I used the following setup:
Device | System | Browser | Screen Reader |
---|---|---|---|
MacBook Air | macOS Venture v13.1 | Safari v16.2 | VoiceOver |
If you get different results due to a different setup, I'd be happy if you could share them in the comments.
Accessibility integration and first checks
When I looked at the finished app, it was clear to me that I should add some more details to all of the buttons to make the task they perform more obvious to screen reader users while hiding it visually by adding a class .sr-only
to the tags.
Note: It might also be a good idea to keep the detailed information visible, in case users are using such an application for the first time and just don't know what to do, or for users with cognitive disabilities.
/* Example Filter Buttons */
<button
type="button"
aria-pressed={isPressed}
onClick={() => setFilter(name)}
>
<span className="sr-only">Show </span>
<span>{name}</span>
<span className="sr-only"> Tasks</span>
</button>
IDs used here for buttons and tasks must be unique to work correctly. This is ensured by using the nanoid package, which automatically generates unique ids.
Note: The static todos still have self-written IDs, and are for illustrative purposes only.
If you want to add CSS to the whole thing to make the app look good, you should pay attention to the color contrast. This can be tested e.g. with the WebAIM Color Contrast Checker in advance.
I'm wondering if I should create custom checkboxes
to make them larger, and create a hover
/focus
on the item for better visibility.
Now let's move on to the problems.
Accessibility Issue using the tab key
I started testing the application using only the keyboard. I checked if the focus appears correctly at the desired element. And at first glance, everything seemed to work fine.
When adding a new task by pressing the Enter
key, the focus stays in the input
field. When adding a new task by tabbing to the Add
button and press Enter
, the focus stays on the Add
button. Seems logical, exactly what I would expect.
When I interacted with the Edit
widget, I noticed that the focus was lost, and it didn't land on the input
field as expected. When closing edit
mode, regardless of whether I click the Cancel
or Save
button, the focus is lost another time. It would make sense for the focus to be on the Edit
button again. With the Delete
button, the focus is also lost. For me, it would make sense that the focus jumps to the beginning of the list.
However, in both cases, the focus appears in the expected place when you hit the tab
key again. But I had to find a suitable solution for this.
I wasn't able to find a solution to this problem so quickly, and after a bit of research, I thought to write a workaround using tabindex
or something, but nothing was satisfactory.
Solution
After a while, and doing some deeper research on focus and the various pre-built React hooks, the React hook useRef
kept coming up on my radar. And I gave it a try.
What I would like to achieve is that
- when I open the
edit
template, the focus should jump directly to theinput
field - when I close the
edit
view in one of the two possible ways, the focus should return to theEdit
button - when pressing the
Delete
button the focus should jump to the beginning of the list
This is where React's useRef
hook comes into play. This hook creates an object with the property current
. This property becomes a reference to the selected DOM elements.
I have created two variables for reference, one for the edit input
field and one for the Edit
button, with a default value of null
. They get value when I associate them with their respective elements. And to make it all work, useEffect
takes care of that after the app is rendered and checks to see if the user is editing. But in doing so, another problem has arisen. When the app is rendered, the focus jumps directly to the last task item.
So I had to develop logic to ensure that the focus does not jump on the first render, but only if a task has been edited before. To accomplish this, I created a custom hook called usePrevious
that would then check what had happened previously, stored in another variable called wasEditing
.
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
const wasEditing = usePrevious(isEditing);
Now the Edit
and Cancel
buttons work fine, and I can mark my first two tasks as done.
For the third point, jumping to the top of the list after deleting a task, I will use useRef
again. Why do I want to jump to the top of the list? Because when deleting a task from the list (and actually from the DOM), there is nothing to return to. So the beginning of the list, i.e. the heading, seems to make sense to me.
To make a heading focusable, I need to add a tabIndex="-1"
. Another variable for the new reference I will use for focus is also added to the heading. Now the heading looks like this:
<h2 tabIndex="-1" ref={listHeadingRef}>
{headingText}
</h2>
The logic I want to achieve is that the focus should jump to the heading when a task gets deleted.
const listHeadingRef = useRef(null);
function deleteTask(id) {
const remainingTasks = tasks.filter((task) => id !== task.id);
setTasks(remainingTasks);
listHeadingRef.current.focus();
}
By adding the focus()
method to the current reference at the end of the delete function, I can achieve this logic.
Accessibility Issue using a screen reader
When using the screen reader, I have found that when interacting with the list, it does not announce how many items are in the list when it is first reached. However, when deleting an item and thus updating the heading, it does so because the h2
now gets focus
, which also means it is announced by the screen reader.
I wanted the screen reader to announce how many items are on the list right at the start. So I just connected the list to the h2
with an aria-labelledby
.
<h2 id="list-heading">{listHeading}</h2>
<ul aria-labelledby="list-heading">{taskList}</ul>
The screen reader now tells you the number of entries when you first get to the list, and each time the list is refreshed after a deletion.
I think it would also be a good idea to let the user know how many items are in the list when toggling between all, completed and active via screen reader. What do you think of this idea?
Conclusion
In Austria, we have a saying that goes like this:
"Die Not macht erfinderisch."
(Necessity is the mother of invention, translated freely)
Even though I've had a hard time understanding debugging to a certain point and fully understanding React Hooks, it seems to help me find a situation that I care enough about to want to fix problems by all means and thus get a better understanding of new concepts.
If I missed something, please leave me a comment so I can fix it and learn from my mistakes.
Code
As promised, here is the link to the code on CodeSandBox https://codesandbox.io/s/accessible-to-do-list-e0ecgp
References
WebAIM Color Contrast Checker https://webaim.org/resources/contrastchecker/
NMP Nanoid https://www.npmjs.com/package/nanoid
React Hook useRef
https://beta.reactjs.org/reference/react/useRef
React Hook useEffect
https://beta.reactjs.org/reference/react/useEffect