How debugging for accessibility helped me finally understand useRef

Julia 👩🏻‍💻 GDE - Mar 22 '23 - - Dev Community

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.

Showing the three phases I encountert, from happy to freaking out: Yay! Doh' Nay!

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>
Enter fullscreen mode Exit fullscreen mode

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.

Add task via keyboard

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.

Focus loss when clicking edit or delete.

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

  1. when I open the edit template, the focus should jump directly to the input field
  2. when I close the edit view in one of the two possible ways, the focus should return to the Edit button
  3. 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);
Enter fullscreen mode Exit fullscreen mode

Now the Edit and Cancel buttons work fine, and I can mark my first two tasks as done.

Focus stays when editing or canceling to edit task

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>
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

By adding the focus() method to the current reference at the end of the delete function, I can achieve this logic.

Focus jumps back to the heading.

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>
Enter fullscreen mode Exit fullscreen mode

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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .