Componenti accessibili: Paginazione
Oggi vedremo come creare una paginazione da zero e renderla accessibile e riutilizzabile. Spero vi sia utile!
Github: https://github.com/micaavigliano/accessible-pagination
Proyecto: accessible-pagination.netlify.app/
Custom hook per richiedere dati
const useFetch = <T,>(
url: string,
currentPage: number = 0,
pageSize: number = 20) => {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<boolean>(false);
useEffect(() => {
const fetchData = async() => {
setLoading(true);
setError(false);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error('network response failed')
}
const result: T = await response.json() as T;
setData(result)
} catch (error) {
setError(true)
} finally {
setLoading(false);
}
};
fetchData()
}, [url, currentPage, pageSize]);
return {
data,
loading,
error,
}
};
Genereremo un hook personalizzato con un generic type. Questo ci permetterà di specificare il tipo di dato atteso quando si usa questo hook.
Prevedremo 3 parametri. Uno per l’URL da cui eseguire il fetch dei dati, currentPage che è la pagina in cui ci troviamo e per impostazione predefinita è 0, e pageSize che è il numero di elementi per pagina e per impostazione predefinita è 20 (potete modificare questo valore).
Nel nostro stato
const [data, setData] = useState<T | null>(null);
passiamo il generic typeT
, poiché, man mano che useremo l’hook per richieste di dati diverse, ci aspetteremo tipi di dato differenti.
Paginazione
Per rendere accessibile una paginazione, dobbiamo tenere conto dei seguenti punti:
Il focus deve potersi spostare su tutti gli elementi interattivi della paginazione e avere un indicatore visibile. Per garantire una buona interazione con i lettori di schermo, è necessario usare correttamente regioni, proprietà e stati.
La paginazione deve essere raggruppata all’interno di un tag
<nav>
e contenere unaria-label
che la identifichi come una paginazione.Ogni elemento all’interno della paginazione deve includere
aria-setsize
earia-posinset
. Ora, a cosa servono?aria-setsize
serve per indicare il numero totale di elementi all’interno dell’elenco della paginazione. Il lettore di schermo lo annuncerà nel modo seguente:

aria-posinset
serve per indicare la posizione dell’elemento all’interno del totale degli elementi della paginazione. Il lettore di schermo lo annuncerà nel modo seguente:

Ogni elemento della paginazione deve avere un
aria-label
per identificare a quale pagina si andrà premendo quel pulsante.Prevedere pulsanti per andare alla pagina successiva/precedente e ciascuno di questi pulsanti deve avere il proprio
aria-label
.Se la paginazione contiene un’ellissi (…): deve essere correttamente etichettata con un
aria-label
.Ogni volta che si passa a una nuova pagina, il lettore di schermo deve annunciare in quale pagina ci si trova e quanti nuovi elementi sono presenti, nel modo seguente:

Per ottenere questo risultato, lo implementeremo nel modo seguente:
const [statusMessage, setStatusMessage] = useState<string>("");
useEffect(() => {
window.scrollTo({ top: 0, behavior: 'smooth' });
if (!loading) {
setStatusMessage(`Page ${currentPage} loaded. Displaying ${data?.near_earth_objects.length || 0} items.`);
}
}, [currentPage, loading]);
Quando la pagina avrà terminato di caricarsi, imposteremo un nuovo messaggio con la nostra currentPage
e la lunghezza del nuovo array che stiamo caricando.
Ora sì! Passiamo a vedere com’è strutturato il codice nel file pagination.tsx
.
Il componente richiederà cinque prop.
interface PaginationProps {
currentPage: number;
totalPages: number;
nextPage: () => void;
prevPage: () => void;
goToPage: (page: number) => void;
}
currentPage
si riferirà alla pagina corrente. La gestiremo nel componente in cui desideriamo utilizzare la paginazione nel modo seguente:const [currentPage, setCurrentPage] = useState<number>(1);
totalPages
si riferisce al totale di elementi da mostrare fornito dall’API.nextPage
: questa funzione ci permetterà di passare alla pagina successiva e aggiornare lo statocurrentPage
nel modo seguente:
const handlePageChange = (newPage: number) => {
setCurrentPage(newPage);
};
const nextPage = () => {
if (currentPage < totalPages) {
handlePageChange(currentPage + 1);
}
};
prevPage
: questa funzione ci permetterà di tornare alla pagina precedente rispetto a quella corrente e aggiornare lo statocurrentPage
.
const prevPage = () => {
if (currentPage > 1) {
handlePageChange(currentPage - 1);
}
};
goToPage
: questa funzione richiederà un parametro numerico ed è la funzione che ogni elemento avrà per poter andare alla pagina desiderata. La faremo funzionare nel modo seguente:
const handlePageChange = (newPage: number) => {
setCurrentPage(newPage);
};
Per dare vita alla nostra paginazione manca un ultimo passo: creare l’array che itereremo nel nostro elenco! Per farlo dobbiamo seguire i seguenti passaggi:
Creare una funzione, in questo caso la chiamerò
getPageNumbers
.Creare variabili per il primo e l’ultimo elemento dell’elenco.
Creare una variabile per l’ellissi sul lato sinistro. Per scelta personale, la mia ellissi si collocherà dopo il quarto elemento della lista.
Creare una variabile per l’ellissi sul lato destro. Per scelta personale, la mia ellissi si collocherà prima degli ultimi tre elementi della lista.
Creare una funzione che ci restituisca un array in cui siano sempre centrati 5 elementi: la pagina attuale, due elementi precedenti e due successivi. Se necessario, escluderemo la prima e l’ultima pagina.
const pagesAroundCurrent = [currentPage - 2, currentPage - 1, currentPage, currentPage + 1, currentPage + 2].filter(page => page > firstPage && page < lastPage);
Per l’ultima variabile, creeremo un array che contenga tutte le variabili create in precedenza.
Infine, filtreremo gli elementi
null
e restituiremo l’array.
Questo è l’array che itereremo per ottenere l’elenco degli elementi nella nostra paginazione nel modo seguente:
<ol className="flex gap-3">
{pageNumbers.map((number) => {
const isEllipsis = number === "left-ellipsis" || number === "right-ellipsis"
if (isEllipsis) {
return (
<li
key={number}
className="relative top-5"
aria-label="Ellipsis, more pages between"
>
<span aria-hidden>…</span>
</li>
)
}
const page = Number(number)
const isCurrent = currentPage === page
return (
<li
key={`page-${number}`}
aria-setsize={totalPages}
aria-posinset={typeof number === "number" ? page : undefined}
>
<button
onClick={() => goToPage(page)}
className={isCurrent ? "underline underline-offset-3 border-zinc-300" : undefined}
aria-label={`Go to page ${page}`}
aria-current={isCurrent ? "page" : undefined}
>
{page}
</button>
</li>
)
})}
</ol>
E fin qui come realizzare una paginazione riutilizzabile e accessibile! Personalmente ho imparato a costruire una paginazione da zero a forza di tentativi, perché ho dovuto implementarla durante una sessione di live coding; spero che la mia esperienza vi sia d’aiuto per la vostra carriera e che possiate implementarla e magari perfino migliorarla!
Un saluto, Mica <3