DEV Community

Mica
Mica

Posted on

Focus Management: how to improve the accessibility and usability of our components

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;

Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

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

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:

Picture of an HTML structure with its respective head element and body element. Inside the body tag, there is a div element with an id "root," and within it, the first child is another div element with the class "container."

  • Example of how the main hierarchy looks with the modal:

Picture of an HTML structure with its respective head element and body element. Inside the body tag, there is a div element with an id "root," and within it, there are two child elements: a div element with the class "container" and a dialog element corresponding to 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]);
Enter fullscreen mode Exit fullscreen mode

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!!!🫰

Top comments (0)