Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MDN React Tutorials #6: Accessibility & Resources #31139

Merged
merged 30 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3fdb558
chore: minor line edits
mxmason Dec 18, 2023
061fa05
chore: moar line edits
mxmason Dec 19, 2023
4d7cafe
chore: add note on focus-visible
mxmason Dec 19, 2023
6b85b6e
chore: yet another line edit
mxmason Dec 19, 2023
2fe0805
chore: update style section
mxmason Dec 19, 2023
ed210b6
chore: clean up whitespace
mxmason Dec 19, 2023
1ff38ed
chore: add useReducer section
mxmason Dec 19, 2023
302b740
chore: clean up client side router section
mxmason Dec 19, 2023
2bc083a
chore: fix typo
mxmason Dec 19, 2023
174c437
chore: update dev tools reference link
mxmason Dec 19, 2023
9da3f89
Merge branch 'main' into ej/react-rewrite-6
chrisdavidmills Dec 19, 2023
e239943
Minor grammar fixes
chrisdavidmills Dec 19, 2023
02d6f76
small tweaks
chrisdavidmills Dec 19, 2023
875d62f
chore: apply suggestions from code review
mxmason Dec 19, 2023
af3270b
chore: add useRef explainer
mxmason Dec 20, 2023
0b3d604
chore: rewrite ref/effect
mxmason Dec 20, 2023
43d94bc
chore: remove mention of create-react-app from testing section
mxmason Dec 20, 2023
d681496
Merge remote-tracking branch 'upstream/main' into ej/react-rewrite-6
mxmason Dec 20, 2023
230e777
chore: update note on focus-visible
mxmason Dec 20, 2023
549533c
chore: add usePrevious explainer
mxmason Dec 20, 2023
5c30c91
chore: dedupe
mxmason Dec 20, 2023
154ebda
chore: more little rewrites
mxmason Dec 20, 2023
946f843
chore: simplify length check
mxmason Dec 20, 2023
0d618ce
Merge branch 'main' into ej/react-rewrite-6
chrisdavidmills Dec 20, 2023
8a5ff92
tweaks to :focus-visible explanation
chrisdavidmills Dec 20, 2023
ed74a5c
minor language tweak
chrisdavidmills Dec 20, 2023
8eec4eb
chore: apply suggestions from code review
mxmason Dec 20, 2023
80f51c2
usePrevious() description update
chrisdavidmills Dec 20, 2023
db7e9e3
Remove "generally"
chrisdavidmills Dec 20, 2023
6485d42
Merge branch 'main' into ej/react-rewrite-6
chrisdavidmills Dec 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ page-type: learn-module-chapter

{{LearnSidebar}}{{PreviousMenuNext("Learn/Tools_and_testing/Client-side_JavaScript_frameworks/React_interactivity_filtering_conditional_rendering","Learn/Tools_and_testing/Client-side_JavaScript_frameworks/React_resources", "Learn/Tools_and_testing/Client-side_JavaScript_frameworks")}}

In our final tutorial article, we'll focus on (pun intended) accessibility, including focus management in React, which can improve usability and reduce confusion for both keyboard-only and screen reader users.
In our final tutorial article, we'll focus on (pun intended) accessibility, including focus management in React, which can improve usability and reduce confusion for both keyboard-only and screen-reader users.

<table>
<tbody>
Expand Down Expand Up @@ -34,7 +34,7 @@ In our final tutorial article, we'll focus on (pun intended) accessibility, incl

## Including keyboard users

At this point, we've accomplished all of the features we set out to implement. A user can add a new task, check and uncheck tasks, delete tasks, or edit task names. Also, they can filter their task list by all, active, or completed tasks.
At this point, we've implemented all the features we set out to implement. Users can add a new task, check and uncheck tasks, delete tasks, or edit task names. Also, they can filter their task list by all, active, or completed tasks.

Or, at least, they can do all of these things with a mouse. Unfortunately, these features are not very accessible to keyboard-only users. Let's explore this now.

Expand All @@ -48,34 +48,46 @@ The `<Todo />` component will switch templates, as we designed, and you'll see a

But where did our focus indicator go?

When we switch between templates in our `<Todo />` component, we completely remove the elements that were there before to replace them with something else. That means the element that we were focused on vanishes, and nothing is in focus at all. This could confuse a wide variety of users — particularly users who rely on the keyboard, or users who use a screen reader.
When we switch between templates in our `<Todo />` component, we completely remove the elements from the old template and replace them with the elements from the new template. That means the element that we were focused on no longer exists, so there's no visual cue as to where the browser's focus is. This could confuse a wide variety of users — particularly users who rely on the keyboard, or users who use assistive technology.

To improve the experience for keyboard and screen-reader users, we should manage the browser's focus ourselves.
To improve the experience for keyboard and assistive technology users, we should manage the browser's focus ourselves.

### Aside: a note on our focus indicator
mxmason marked this conversation as resolved.
Show resolved Hide resolved

If you click the "All", "Active", or "Completed" filter buttons with your mouse, you _won't_ see a visible focus indicator. Your code isn't broken!

Our CSS file uses the [`:focus-visible`](/en-US/docs/Web/CSS/:focus-visible) pseudo-class to define the focus indicator we see, and this pseudo-class uses a browser-defined set of rules to determine whether or not to show the focus indicator to the user. Generally, `:focus-visible` _will_ show a focus indicator in response to keyboard input, and _might_ show in response to mouse input. `<button>` elements are an example of an element that does not show a focus indicator in response to mouse input, while `<input>` elements _do_ show a focus indicator in response to mouse input.

The behavior of `:focus-visible` is more selective than the older [`:focus`](/en-US/docs/Web/CSS/:focus) pseudo-class, with which you might be more familiar. `:focus` shows a focus indicator in many more situations, and you can use it instead of or in combination with `:focus-visible` if you prefer.

## Focusing between templates

When a user toggles a `<Todo/>` template from viewing to editing, we should focus on the `<input>` used to rename it; when they toggle back from editing to viewing, we should move focus back to the "Edit" button.
When a user toggles a `<Todo />` template from viewing to editing, we should focus on the `<input>` used to rename it; when they toggle back from editing to viewing, we should move focus back to the "Edit" button.

### Targeting our elements

In order to focus on an element in our DOM, we need to tell React which element we want to focus on and how to find it. React's [`useRef`](https://react.dev/reference/react/useRef) hook creates an object with a single property: `current`. This property can be a reference to anything we want, and that reference can be looked up later. It's particularly useful for referring to DOM elements.
Up to this point, we've been writing JSX and letting React build the DOM from them. Most of the time, we don't need to target specific elements in the DOM because we can use React's state and props to control what gets rendered. To manage focus, however, we _do_ need to be able to target specific elements.
mxmason marked this conversation as resolved.
Show resolved Hide resolved

This is where the `useRef()` hook comes in.

Change the `import` statement at the top of `Todo.js` so that it includes `useRef`:
First, change the `import` statement at the top of `Todo.jsx` so that it includes `useRef`:

```jsx
import React, { useRef, useState } from "react";
import { useRef, useState } from "react";
```

Then, create two new constants beneath the hooks in your `Todo()` function. Each should be a ref – one for the "Edit" button in the view template and one for the edit field in the editing template.
`useRef()` creates an object with a single property: `current`. Refs can store any value we want it to, and we can look up that value later. We can even store references to DOM elements, which is exactly what we're going to do here.
mxmason marked this conversation as resolved.
Show resolved Hide resolved

Next, create two new constants beneath the `useState()` hooks in your `Todo()` function. Each should be a ref – one for the "Edit" button in the view template and one for the edit field in the editing template.

```jsx
const editFieldRef = useRef(null);
const editButtonRef = useRef(null);
```

These refs have a default value of `null` because they will not have value until we attach them to their respective elements. To do that, we'll add an attribute of `ref` to each element, and set their values to the appropriately named `ref` objects.
These refs have a default value of `null` to make it clear that they'll be empty until they're attached to their DOM elements. To attach them to their elements, we'll add the special `ref` attribute to each element's JSX, and set the values of those attributes to the appropriately named `ref` objects.

The textbox `<input>` in your editing template should be updated like this:
Update the `<input>` in your editing template so that it reads like this:

```jsx
<input
Expand All @@ -88,7 +100,7 @@ The textbox `<input>` in your editing template should be updated like this:
/>
```

The "Edit" button in your view template should read like this:
Update the "Edit" button in your view template so that it reads like this:

```jsx
<button
Expand All @@ -100,17 +112,29 @@ The "Edit" button in your view template should read like this:
</button>
```

### Focusing on our refs with useEffect
Doing this will populate our `editFieldRef` and `editButtonRef` with references to the DOM elements they're attached to, but _only_ after React has rendered the component. Test that out for yourself: add the following line somewhere in the body of your `Todo()` function:
mxmason marked this conversation as resolved.
Show resolved Hide resolved

```jsx
console.log(editButtonRef.current);
```

You'll see that the value of `editButtonRef.current` is `null` when the component first renders, but if you click an "Edit" button, it will log the `<input>` element to the console. This is because the ref is populated only after the component renders, and clicking the "Edit" button causes the component to re-render. Be sure to delete this log before moving on.

> **Note:** Your logs will appear 6 times because we have 3 instances of `<Todo />` in our app and React renders our components twice in development.

We're getting closer! To take advantage of our newly referenced elements, we need to use another React hook: `useEffect()`.

To use our refs for their intended purpose, we need to import another React hook: [`useEffect()`](https://react.dev/reference/react/useEffect). `useEffect()` is so named because it runs after React renders a given component, and will run any side-effects that we'd like to add to the render process, which we can't run inside the main function body. `useEffect()` is useful in the current situation because we cannot focus on an element until after the `<Todo />` component renders and React knows where our refs are.
### Implementing `useEffect()`

Change the import statement of `Todo.js` again to add `useEffect`:
[`useEffect()`](https://react.dev/reference/react/useEffect). `useEffect()` is so named because it runs any side-effects that we'd like to add to the render process, and which we can't run inside the main function body. `useEffect()` runs right after a component renders, and this means that meaning the DOM elements we referenced in the previous section will be available for us to use.

Change the import statement of `Todo.jsx` again to add `useEffect`:

```jsx
import React, { useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
```

`useEffect()` takes a function as an argument; this function is executed after the component renders. Let's see this in action; put the following `useEffect()` call just above the `return` statement in the body of `Todo()`, and pass into it a function that logs the words "side effect" to your console:
`useEffect()` takes a function as an argument; this function is executed _after_ the component renders. To illustrate this, put the following `useEffect()` call just above the `return` statement in the body of `Todo()`, and pass a function into it that logs the words "side effect" to your console:
mxmason marked this conversation as resolved.
Show resolved Hide resolved

```jsx
useEffect(() => {
Expand All @@ -124,14 +148,14 @@ To illustrate the difference between the main render process and code run inside
console.log("main render");
```

Now, open the app in your browser. You should see both messages in your console, with each one repeating three times. Note how "main render" logged first, and "side effect" logged second, even though the "side effect" log appears first in the code.
Now, open the app in your browser. You should see both messages in your console, with each one repeating multiple times. Note how "main render" logged first, and "side effect" logged second, even though the "side effect" log appears first in the code.

```plain
main render (3) Todo.js:100
side effect (3) Todo.js:98
main render Todo.jsx
side effect Todo.jsx
```

That's it for our experimentation for now. Delete `console.log("main render")` now, and let's move on to implementing our focus management.
Again, the logs are ordered this way because code inside `useEffect()` runs _after_ the component renders. This takes some getting used to, just keep it in mind as you move forward. For now, delete `console.log("main render")` and we'll move on to implementing our focus management.

### Focusing on our editing field

Expand Down Expand Up @@ -165,28 +189,29 @@ useEffect(() => {
}, [isEditing]);
```

This kind of mostly works. Head back to your browser and you'll see that your focus moves between Edit `<input>` and "Edit" button as you start and end an edit. However, you may have noticed a new problem — the "Edit" button in the final `<Todo />` component is focused immediately on page load, before we even interact with the app!
This kind of works. Head back to your browser and you'll see that your focus moves between Edit `<input>` and "Edit" button as you start and end an edit. However, you may have noticed a new problem — the "Edit" button in the final `<Todo />` component is focused immediately on page load, before we even interact with the app!
chrisdavidmills marked this conversation as resolved.
Show resolved Hide resolved

Our `useEffect()` hook is behaving exactly as we designed it: it runs as soon as the component renders, sees that `isEditing` is `false`, and focuses the "Edit" button. Because there are three instances of `<Todo />`, we see focus on the last "Edit" button.

We need to refactor our approach so that focus changes only when `isEditing` changes from one value to another.

## More robust focus management

In order to meet our refined criteria, we need to know not just the value of `isEditing`, but also _when that value has changed_. In order to do that, we need to be able to read the previous value of the `isEditing` constant. Using pseudocode, our logic should be something like this:
To meet our refined criteria, we need to know not just the value of `isEditing`, but also _when that value has changed_. To do that, we need to be able to read the previous value of the `isEditing` constant. Using pseudocode, our logic should be something like this:

```jsx
if (wasNotEditingBefore && isEditingNow) {
focusOnEditField();
}
if (wasEditingBefore && isNotEditingNow) {
} else if (wasEditingBefore && isNotEditingNow) {
focusOnEditButton();
}
```

The React team had discussed [ways to get a component's previous state](https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state), and has provided an example custom hook we can use for the job.
The React team has discussed [ways to get a component's previous state](https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state), and provided an example hook we can use for the job.

### Enter `usePrevious()`

Paste the following code near the top of `Todo.js`, above your `Todo()` function.
Paste the following code near the top of `Todo.jsx`, above your `Todo()` function.
mxmason marked this conversation as resolved.
Show resolved Hide resolved

```jsx
function usePrevious(value) {
Expand All @@ -198,28 +223,39 @@ function usePrevious(value) {
}
```

Now we'll define a `wasEditing` constant beneath the hooks at the top of `Todo()`. We want this constant to track the previous value of `isEditing`, so we call `usePrevious` with `isEditing` as an argument:
`usePrevious()` is a _custom hook_ that tracks a value across renders. It uses the `useRef()` hook to create a ref, and then updates the `useEffect()` hook. This means the `current` property of the ref will always contain the previous value of the `value` argument. You might think of it as intentionally being one value "behind" – hence the name `usePrevious()`.
mxmason marked this conversation as resolved.
Show resolved Hide resolved

### Using `usePrevious()`

Now we can define a `wasEditing` constant beneath the hooks at the top of `Todo()`. We want this constant to track the previous value of `isEditing`, so we call `usePrevious` with `isEditing` as an argument:
mxmason marked this conversation as resolved.
Show resolved Hide resolved

```jsx
const wasEditing = usePrevious(isEditing);
```

With this constant, we can update our `useEffect()` hook to implement the pseudocode we discussed before — update it as follows:
You can see how `usePrevious()` behaves by adding a console log beneath this line:

```jsx
console.log(wasEditing);
```

In this log, the `current` value of `wasEditing` will always be the previous value of `isEditing`. Click on the "Edit" and "Cancel" button a few times to watch it change, then delete this log when you're ready to move on.

With this `wasEditing` constant, we can update our `useEffect()` hook to implement the pseudocode we discussed before:

```jsx
useEffect(() => {
if (!wasEditing && isEditing) {
editFieldRef.current.focus();
}
if (wasEditing && !isEditing) {
} else if (wasEditing && !isEditing) {
editButtonRef.current.focus();
}
}, [wasEditing, isEditing]);
```

Note that the logic of `useEffect()` now depends on `wasEditing`, so we provide it in the array of dependencies.

Again try using the "Edit" and "Cancel" buttons to toggle between the templates of your `<Todo />` component; you'll see the browser focus indicator move appropriately, without the problem we discussed at the start of this section.
Try using your keyboard to activate the "Edit" and "Cancel" buttons in the `<Todo />` component; you'll see the browser focus indicator move appropriately, without the problem we discussed at the start of this section.

## Focusing when the user deletes a task

Expand All @@ -231,21 +267,21 @@ Sometimes, the place we want to send our focus to is obvious: when we toggled ou

### Creating our ref

Import the `useRef()` and `useEffect()` hooks into `App.js` — you'll need them both below:
Import the `useRef()` and `useEffect()` hooks into `App.jsx` — you'll need them both below:

```jsx
import React, { useState, useRef, useEffect } from "react";
import { useState, useRef, useEffect } from "react";
```

Then declare a new ref inside the `App()` function. Just above the `return` statement is a good place:
Next, declare a new ref inside the `App()` function, just above the `return` statement:

```jsx
const listHeadingRef = useRef(null);
```

### Prepare the heading

Heading elements like our `<h2>` are not usually focusable. This isn't a problem — we can make any element programmatically focusable by adding the attribute [`tabindex="-1"`](/en-US/docs/Web/HTML/Global_attributes/tabindex) to it. This means _only focusable with JavaScript_. You can't press <kbd>Tab</kbd> to focus on an element with a tabindex of `-1` the same way you could do with a [`<button>`](/en-US/docs/Web/HTML/Element/button) or [`<a>`](/en-US/docs/Web/HTML/Element/a) element (this can be done using `tabindex="0"`, but that's not really appropriate in this case).
Heading elements like our `<h2>` are not usually focusable. This isn't a problem — we can make any element programmatically focusable by adding the attribute [`tabindex="-1"`](/en-US/docs/Web/HTML/Global_attributes/tabindex) to it. This means _only focusable with JavaScript_. You can't press <kbd>Tab</kbd> to focus on an element with a tabindex of `-1` the same way you could do with a [`<button>`](/en-US/docs/Web/HTML/Element/button) or [`<a>`](/en-US/docs/Web/HTML/Element/a) element (this can be done using `tabindex="0"`, but that's not appropriate in this case).

Let's add the `tabindex` attribute — written as `tabIndex` in JSX — to the heading above our list of tasks, along with our `headingRef`:

Expand All @@ -259,7 +295,7 @@ Let's add the `tabindex` attribute — written as `tabIndex` in JSX — to the h

### Getting previous state

We want to focus on the element associated with our ref (via the `ref` attribute) only when our user deletes a task from their list. That's going to require the `usePrevious()` hook we already used earlier on. Add it to the top of your `App.js` file, just below the imports:
We want to focus on the element associated with our ref (via the `ref` attribute) only when our user deletes a task from their list. That's going to require the `usePrevious()` hook we used earlier on. Add it to the top of your `App.jsx` file, just below the imports:

```jsx
function usePrevious(value) {
Expand All @@ -277,9 +313,9 @@ Now add the following, above the `return` statement inside the `App()` function:
const prevTaskLength = usePrevious(tasks.length);
```

Here we are invoking `usePrevious()` to track the length of the tasks state, like so:
Here we are invoking `usePrevious()` to track the previous length of the tasks array.

> **Note:** Since we're now utilizing `usePrevious()` in two files, a good efficiency refactor would be to move the `usePrevious()` function into its own file, export it from that file, and import it where you need it. Try doing this as an exercise once you've got to the end.
> **Note:** Since we're now utilizing `usePrevious()` in two files, it might be more efficient to move the `usePrevious()` function into its own file, export it from that file, and import it where you need it. Try doing this as an exercise once you've got to the end.

### Using `useEffect()` to control our heading focus
chrisdavidmills marked this conversation as resolved.
Show resolved Hide resolved

Expand Down
Loading