A little bit tired of always seeing the typical introductory courses about accessibility, I decided to start by the end. So this is why I am going to talk about Focus Management.
Before starting to talk about Focus Management and accessibility we should focus on our product’s usability. One of the WCAG 2.2 main principles is that the application should be operable. This principle allows us to think about the user experience of those keyboard’s users. Some examples of navigation in general could be: navigation using a screen reader (NVDA, Jaws, VoiceOver, etc). The focus and the screen readers use the Accessibility API, which can be found in all operative systems and in the browsers (DOM). Why is this important? Basically, because the assistive technologies are going to navigate the same way as the focus does. A practical example of that is when a modal is opened and there is no focus trap, the focus will get lost and this might lead to frustrations and confusions. If you want to know more about the Accessibility API you can click on this link: Core Accessibility API Mappings 1.1.
So, for this post I came up with the idea of creating a typical but not so covered case of what happens when we open and close a modal. I am going to develop it using React with Typescript and explain step by step what I am doing. You can check out the code here Github repository and the explanation here Project .
Let’s do it!
useDataFetching hook
First of all, let's create out fetching data's hook ti display the users (I really love this hook and I hope it would be useful for you all)
import { useState, useEffect, useCallback } from "react";
const useDataFetching = (url: string) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<boolean>(false);
const fetchData = useCallback(async () => {
setLoading(true);
setError(false);
try {
const response = await fetch(url);
const result = await response.json();
setData(result);
} catch (err) {
setError(true);
}
setLoading(false);
}, []);
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, loading, error };
};
export default useDataFetching;
CardContainer.tsx
import React from "react";
import useFetch from "../hooks/useFetch";
import Card from "../component/Card";
const CardContainer = () => {
const { data, loading } = useFetch(
"https://63bedcf7f5cfc0949b634fc8.mockapi.io/users"
);
if (loading)
return (
<p role="status">
Loading...
</p>
);
return (
<div className="container">
<Card item={data} />
</div>
);
};
export default CardContainer;
In this component, we are going to use the useFetch()
hook so we can retrieve the data. An important part of our loading
component is that we are going to pass it two attributes: role
and aria-live
.
What is this for?
The role="status"
in this case we have to use it to announce to the Accessibility API that that zone of our application is a "live region", which means that the state will change and that change will have to be announced to the user. By default the role="status"
will have the attribute aria-live="polite"
. This attribute announces to assistive technology that, in a non-intrusive way and as soon as there is a spot, it SHOULD announce the content that is inside the live region. If you want to know more about the live region roles you can go to the follow link: https://www.w3.org/TR/wai-aria-1.1/#dfn-live-region
Component Card.tsx
Here is where we are going to place the logic to open the modal and manage the focus when we close it.
const handleClick = (id: number) => {
setItemId(id);
setModalOpen(true);
};
The handleClick
function will allow is to pass it an id
and set it on the setItemId
and also open the modal. What is what we search by storing the id of every item in an state? Well, it will let us validate that the id of the item and the stored id matched, and if they match the open is opened. This is necessary because we can validate that every item is unique so we can inject the data in every item in a unique way.
const closeModal = () => {
setModalOpen(false);
};
The closeModal
function is for closing the modal by pressing the esc
key or clicking over the close button
<button
onClick={() => handleClick(data.id!)}
data-item-id={data.id}
aria-label={`more info about ${data.name}`}
className="more-info"
>
More info
</button>
{modalOpen && itemId === data.id && (
<Modal
isOpen={modalOpen}
title={data.name}
onClose={closeModal}
>
<>
<div className="modal-content">
<p>
Website: <a href={data.website}>{data.website}</a>
</p>
</div>
<div className="desc-container">
<p>{data.description}</p>
</div>
</>
</Modal>
)}
Now, let's go to the useEffect
:
useEffect(() => {
if (!modalOpen) {
const buttonToFocus = document.querySelector(
`button[data-item-id="${itemId}"]`
);
if (buttonToFocus instanceof HTMLElement) {
buttonToFocus.focus();
}
}
}, [modalOpen, itemId]);
Here we are going to check if the modal it is visible or not. If it's closed, we are going to grab the button according to its specific attribute button[data-item-id="${itemId}"]
. Finally, we check if the button is an element with buttonToFocus instanceof HTMLElement
so we can call to the focus()
method.
The interaction with the focus should be: when the modal is opened, the focus should placed in the dialog
. When we close it, the focus should go back to the more information
button.
Componente Modal.tsx
const onKeyDown = (event: React.KeyboardEvent) => {
if (event.key === "Escape" && onClose) {
onClose();
}
};
With the onKeyDown()
function, we listen for which key is pressed. If the pressed key is "Escape" and the onClose function exists, we will close the modal. This function allows us to comply with the G21 technique, which emphasizes the importance of ensuring that users do not get trapped in any part of our app and providing them with an escape mechanism. If you want to read more about this technique and criterion 2.1.2 (no keyboard trap), you can do so at the following link: https://www.w3.org/TR/WCAG20-TECHS/G21.html
Now, let's continue with our Modal.tsx component. We are going to create the modal within a createPortal(), my favorite method provided by React.
What is createPortal() used for?
As I mentioned earlier, createPortal()
is a method provided by React that is used to create portals. Portals are used to render child elements into a DOM node that exists outside the main component hierarchy. This method is particularly useful for creating modals, tooltips, or any other component that doesn't need to be constantly rendered within the main component structure.
createPortal()
takes two arguments: the element to render and the DOM element where you want to inject it.
- Example of how the main hierarchy looks without the modal:
- Example of how the main hierarchy looks with the modal:
To conclude, createPortal() enhances the accessibility of our applications by preventing assistive technologies from announcing hidden content or unnecessary navigations in our DOM.
Let's continue with the logic in our Modal component.
useEffect(() => {
if (isOpen) {
const interactiveElement =
modalRef.current?.querySelector("[tabindex='-1']");
if (interactiveElement) {
if (interactiveElement instanceof HTMLElement) {
interactiveElement.focus();
}
}
}
}, [isOpen]);
This hook will help us focus on the modal as soon as it opens. In this case, the first element to receive focus will always be our dialog, as it is necessary for assistive technologies to announce its role and title. Announcing the title is achieved by establishing a relationship between the aria-labelledby and the id we pass to our title.
It is crucial to have control over the focus in all interactive elements to ensure that the interaction and usability of our application are accessible to everyone, regardless of their method of navigating through it. This also ensures free navigation on the web in general.
With this final explanation, I conclude my first post in this series on accessible components. My goal is to raise awareness about accessibility and demonstrate that we, as frontend developers, can enhance the experience for many people without it being an extra effort. We just need the tools and sensitivity to learn and incorporate them.
I hope this has been helpful, and any doubts, questions, or suggestions for improvement in the future are welcome. I'll leave my social media links where you can reach out to me for anything!
Linkedin: https://www.linkedin.com/in/micaelaavigliano/
Github: https://github.com/micaavigliano
Twitter: https://twitter.com/messycatx
Thanks for reading this far!!!🫰