Focus Management: come migliorare l’accessibilità e l’usabilità dei nostri componenti
Un po’ stanco di vedere sempre i tipici corsi introduttivi sull’accessibilità, ho deciso di partire dalla fine. Ecco perché parlerò della Gestione del Focus.
Per iniziare a parlare di Focus Management e accessibilità dobbiamo concentrarci sulla usabilità del nostro prodotto. Uno dei quattro principi che ci propone la WCAG 2.2 affinché la nostra applicazione sia accessibile è che sia operabile. Questo principio ci permette di riflettere sull’esperienza di usabilità delle persone che, per vari motivi, non utilizzano il mouse per navigare sul web. Alcuni esempi di modalità di navigazione possono essere: navigare utilizzando la tastiera oppure navigare tramite uno screen reader o lettore di schermo (NVDA, Jaws, VoiceOver, tra gli altri).
Gli screen reader e il focus funzionano grazie alla API di Accessibilità presenti in tutti i sistemi operativi e nei browser attraverso il Document Object Model (DOM). Perché questo è importante? Perché fondamentalmente vengono gestiti tramite il focus e la tecnologia assistiva risponderà nello stesso modo in cui risponde il focus nella nostra web. Alcuni esempi pratici possono essere quando si apre una modale e non viene creato un focus trap: in questo caso l’utente perde il focus e ciò può portare a confusione e frustrazione. Se vuoi leggere di più sulla API di Accessibilità puoi visitare questo link: https://www.w3.org/TR/core-aam-1.1/#intro_aapi
Per questo post mi è venuto in mente un caso tipico ma poco trattato: vedere che cosa succede al focus quando si apre e si chiude una modale. Lo implementerò in React con TypeScript e spiegherò passo dopo passo che cosa sto facendo. Potete dare un’occhiata al codice qui https://github.com/micaavigliano/focus-management e all’applicazione qui https://focus-management.netlify.app/
Mettiamoci all’opera!
useDataFetching hook
Per prima cosa creeremo il nostro hook per ottenere i dati dei nostri utenti (mi piace usare questo hook e spero che sia utile anche per i vostri progetti 🩷).
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;
useDataFetching hook
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;
Qui andremo a importare il nostro hook useFetch()
per poter effettuare la richiesta. Un punto importante nel nostro elemento Loading è che gli passeremo due attributi: role
e aria-live
.
A cosa servono?
Il role="status"
in questo caso serve per informare l’API di Accessibilità che quella zona della nostra applicazione è una “zona viva”, cioè che cambierà a seconda dello stato. Questo permette alle tecnologie assistive di essere aggiornate su questi cambiamenti e di annunciarli.
Di default verrà applicato l’attributo aria-live="polite"
. Questo attributo comunica alla tecnologia assistiva che, in maniera non intrusiva e non appena ci sia spazio, DEVE annunciare il contenuto che si trova all’interno della regione viva.
Se volete leggere di più sui diversi ruoli che compongono le live regions, potete farlo al seguente link: https://www.w3.org/TR/wai-aria-1.1/#dfn-live-region
Component Card.tsx
In Card andremo a collocare tutta la nostra logica per aprire la modale e gestire il focus quando la chiudiamo.
const handleClick = (id: number) => {
setItemId(id);
setModalOpen(true);
};
La funzione handleClick
ci permetterà di passare un id, impostarlo in setItemId
e anche visualizzare la modale. Cosa cerchiamo di ottenere salvando l’id di ogni item in uno stato? Ci permette di impostare l’id nel nostro stato e questo ci serve per validare che l’id dell’item e l’id dello stato siano gli stessi: se coincidono, si apre la modale. Questo serve a garantire che ogni item abbia la propria modale unica, in modo da poter iniettare in maniera specifica le informazioni dell’item.
Provate a non inserire questa validazione: vedrete che ogni modale che aprirete conterrà solo le informazioni dell’ultimo item nell’array.
const closeModal = () => {
setModalOpen(false);
};
La funzione closeModal
servirà semplicemente per passarla alla Modale e poterla chiudere sia premendo Esc sia cliccando sul pulsante per chiudere la modale.
<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>
)}
In questo useEffect
andremo a verificare se la modale è visibile o meno. Se è chiusa, quello che accadrà è che selezioneremo il button in base al suo attributo specifico e al fatto che il valore di quell’attributo coincida con l’id che abbiamo salvato nel nostro stato button[data-item-id="${itemId}"]
. Infine, controlleremo se effettivamente si tratta di un button con la validazione buttonToFocus instanceof HTMLElement
, per poter richiamare il metodo focus()
.
L’interazione del focus dovrebbe essere la seguente: quando la modale si apre, il focus deve posizionarsi sul dialog; quando la modale si chiude, il focus deve tornare al pulsante iniziale con cui abbiamo aperto la modale.
Componente Modal.tsx
const onKeyDown = (event: React.KeyboardEvent) => {
if (event.key === "Escape" && onClose) {
onClose();
}
};
Con la funzione onKeyDown()
ascoltiamo quale tasto viene premuto. Se il tasto in questione è “Escape” e la funzione onClose
esiste, chiuderemo la modale. Questa funzione ci permette di essere conformi alla tecnica G21, che sottolinea l’importanza di non lasciare gli utenti intrappolati in qualche punto della nostra app e di fornire un meccanismo di uscita. Se volete leggere di più su questa tecnica e sul criterio 2.1.2 (No Keyboard Trap), potete farlo al seguente link: https://www.w3.org/TR/WCAG20-TECHS/G21.html.
Bene, continuiamo con il nostro componente Modal.tsx
. La modale la creeremo dentro a un createPortal()
, il mio metodo preferito fornito da React, jijij.
Esempio di come appare la gerarchia principale senza la modale:
Immagine di una struttura HTML con il rispettivo elemento head ed elemento body. All’interno del tag body, c’è un elemento div con un id. Esempio di come appare la gerarchia principale con la modale:
Immagine di una struttura HTML con il rispettivo elemento head e l’elemento body. All’interno del tag body, c’è un elemento div con un id
Per concludere, createPortal()
migliora l’accessibilità delle nostre applicazioni perché evita che le tecnologie assistive debbano annunciare contenuti nascosti o effettuare navigazioni inutili nel nostro DOM.
Continuiamo con la logica nel nostro componente Modal
.
useEffect(() => {
if (isOpen) {
const interactiveElement =
modalRef.current?.querySelector("[tabindex='-1']");
if (interactiveElement) {
if (interactiveElement instanceof HTMLElement) {
interactiveElement.focus();
}
}
}
}, [isOpen]);
Questo hook ci aiuterà a posizionare il focus non appena si apre la nostra modale. In questo caso, il primo elemento a ricevere il focus sarà sempre il nostro dialog, poiché è necessario affinché le tecnologie assistive possano annunciarne il ruolo e il titolo. L’annuncio del titolo si ottiene creando una relazione tra aria-labelledby
e l’id che assegniamo al nostro titolo.
È estremamente importante avere il controllo del focus su tutti gli elementi interattivi, in modo che l’interazione e l’usabilità della nostra applicazione possano essere sfruttate da tutte le persone, indipendentemente dal modo in cui navigano, e anche per garantire la libera navigazione sul web in generale.
Con quest’ultima spiegazione concludo il mio primo post di questa serie dedicata ai componenti accessibili. Il mio obiettivo è quello di dare visibilità all’accessibilità e dimostrare che noi, come sviluppatori frontend, possiamo migliorare l’esperienza di molte persone senza che ciò rappresenti uno sforzo eccessivo: basta avere gli strumenti e la sensibilità per imparare e integrare.
Spero che vi sia stato utile e qualsiasi dubbio, domanda o suggerimento per migliorare in futuro è il benvenuto. Vi lascio i miei social dove potete contattarmi per qualsiasi cosa!