Componenti accessibili: Tooltip
Questa volta voglio condividere con voi come creare un tooltip dinamico accessibile e come utilizzare la Clipboard API in JavaScript. Cominciamo subito, perché le cose si fanno interessanti!
Repository del progetto: https://github.com/micaavigliano/accessible-tooltip
Link del progetto: https://accessible-tooltip.vercel.app/
Prima di tutto, parliamo un po’ di ciò che vogliamo ottenere per rendere accessibile il nostro pulsante di copia negli appunti. Abbiamo bisogno che:
Il pulsante possa ricevere il focus e abbia un nome accessibile.
Il pulsante di copia abbia un tooltip che appaia quando l’utente ci passa sopra con il mouse o quando riceve il focus.
Il contenuto del tooltip sia dinamico e venga annunciato dal lettore di schermo ogni volta che cambia.
Il contenuto che vogliamo copiare venga effettivamente copiato negli appunti.
Il componente di copia sia riutilizzabile e possa essere sia testo statico che un input.
Il tooltip si chiuda quando l’utente preme il tasto Esc.
Le nostre specifiche sono chiare e semplici. Iniziamo a scrivere il codice!
Tooltip.tsx
interface TooltipProps {
text: string;
children: ReactNode;
direction: "top" | "bottom" | "left" | "right";
id: string;
}
Text: sarà il testo che il nostro tooltip conterrà.
Children: l’elemento che conterrà il tooltip.
Direction: la direzione in cui vogliamo che il nostro tooltip appaia.
ID: un ID per collegarlo all’attributo aria-describedby che avranno i children.
Cominciamo ad analizzare il componente:
Dobbiamo impostare uno state per gestire la visibilità del tooltip:
const [showTooltip, setShowTooltip] = useState<boolean>(false);
Successivamente, creiamo due funzioni per gestire questo stato: tooltipOn e tooltipOff:
const tooltipOn = () => { setShowTooltip(true); }; const tooltipOff = () => { setShowTooltip(false); };
Queste funzioni saranno passate al nostro elemento contenitore per mostrare il tooltip quando si passa sopra con il mouse utilizzando onMouseEnter, nascondere il tooltip quando il mouse lascia l’elemento con onMouseLeave, mostrarlo di nuovo quando l’elemento riceve il focus con onFocus e nasconderlo quando il focus lascia l’elemento con onBlur. Inoltre, per assicurarci che il nostro tooltip sia al 100% funzionale e accessibile, creeremo una funzione per farlo scomparire quando l’utente preme il tasto Esc:
const closeTooltip = (ev: KeyboardEvent) => {
if (ev.key === "Escape") {
setShowTooltip(false);
}
};
e gestiremo l’evento dentro un useEffect:
useEffect(() => {
document.addEventListener("keydown", closeTooltip);
return () => {
document.removeEventListener("keydown", closeTooltip);
};
}, []);
Vediamo ora come apparirà il nostro componente:
<div
className="relative inline-block justify-center text-center"
onMouseEnter={tooltipOn}
onMouseLeave={tooltipOff}
onFocus={tooltipOn}
onBlur={tooltipOff}
>
{showTooltip && (
<div
className={`
bg-black text-white text-center rounded p-3 absolute z-10
transition-opacity duration-300 ease-in-out w-fit
outline outline-offset-0
${
direction === "top"
? "bottom-[calc(100%+1px)] left-10 transform translate-x-[-60%] mb-2"
: ""
}
${
direction === "bottom"
? "top-[calc(100%+1px)] left-10 transform translate-x-[-60%] mt-2"
: ""
}
${
direction === "left"
? "-left-100 top-1/2 transform -translate-y-1/2 mr-2"
: ""
}
${
direction === "right"
? "-right-100 top-1/2 transform -translate-y-1/2 ml-2"
: ""
}
`}
data-placement={direction}
role="tooltip"
id={id}
style={getTooltipStyle()}
>
{text}
</div>
)}
{children}
</div>
Per rendere il nostro Tooltip accessibile, dobbiamo passargli una serie di attributi:
role="tooltip": Sebbene semanticamente non rappresenti un grande cambiamento, in termini di riferimento aiuta i lettori di schermo a identificare e associare il tooltip con il relativo elemento. Cosa intendo con questo? Qualsiasi elemento che contiene role="tooltip" deve essere collegato a un altro elemento che contiene aria-labelledby (in questo caso, i children dovrebbero averlo). Questo perché il tooltip fornisce informazioni aggiuntive sull’elemento.
id: L’ID dell’elemento da collegare tramite aria-describedby.
data-placement: Riceverà la proprietà direction, che sarà la direzione del nostro tooltip.
CopyToClipboard.tsx
Ora passiamo a uno dei componenti più piacevoli che abbia avuto il piacere di creare. Non so perché, ma mi ci sono affezionato molto.
Cominciamo con le sue proprietà; in questo caso, ne abbiamo una facoltativa e una obbligatoria:
interface ICopyToClipboard {
text?: string;
type: "text" | "input";
}
Text: il testo che il nostro componente riceverà nel caso in cui sia di tipo text.
Type: può assumere solo due valori, text oppure input. Text sarà un valore statico, mentre input sarà un valore dinamico.
In questo caso, il nostro pulsante di copia sarà racchiuso all’interno del componente Tooltip
. Il pulsante avrà un attributo onClick che riceverà la funzione handleCopyText
, di cui parleremo tra poco, e l’attributo aria-labelledby per collegarlo al nostro tooltip.
<Tooltip text={copyText} direction={"bottom"} id={"copyid"}>
<button onClick={handleCopyText} aria-labelledby="copyid">
<ContentCopy />
</button>
</Tooltip>
Passiamo ora alla funzione handleCopyText.
Dobbiamo creare uno state per gestire il testo:
const [copyText, setCopyText] = useState<string>("Copy to clipboard");
Per continuare con la funzione handleCopyText, dobbiamo capire cos’è la Clipboard API. La Clipboard API ci permette di interagire con gli appunti di un sistema operativo. Contiene metodi e funzioni per accedere e manipolare le informazioni memorizzate negli appunti (copia, incolla e taglia). In questo caso, utilizzeremo il metodo
writeText()
. Questo metodo accetta un parametro di testo obbligatorio e restituisce una promise. Se l’operazione ha successo, verrà eseguito il metodo then(), che modificherà il valore del nostro statocopyText
. Dopo 5 secondi, il testo tornerà a"Copy to clipboard"
. In questo caso, il parametro del metodo writeText riceveràtext
oppureinputValue
, a seconda di quale non sia nullo. Questo perché, se passiamotype="input"
al nostro componente, non forniremo un testo statico predefinito.
const handleCopyText = () => {
navigator.clipboard.writeText(text || inputValue).then(() => {
setCopyText("Copiado");
setTimeout(() => {
setCopyText("Copiar al portapapeles");
}, 5000);
});
};
E... eccolo lì! Semplice come sembra, ora abbiamo il nostro pulsante accessibile per copiare il testo con un tooltip altrettanto accessibile. Infine, vorrei mostrarvi come il lettore di schermo annuncia il contenuto del nostro tooltip:
Stato iniziale:
Immagine del lettore di schermo VoiceOver che annuncia: "Copia negli appunti, pulsante" Clic sul pulsante di copia:
Immagine del lettore di schermo VoiceOver che annuncia: "Copiato" Torna allo stato iniziale dopo 5 secondi:
Immagine del lettore di schermo VoiceOver che annuncia: "Copia negli appunti"
Ora, spero che questo componente vi sia piaciuto tanto quanto è piaciuto a me, e vi invito a condividere se vi è mai capitato di incontrare un tooltip e pensare che potesse essere reso accessibile. Infine, ecco l’elenco delle risorse che ho utilizzato per raccogliere le informazioni: