Vedi tutti i post

Componenti accessibili: Paginazione

Micaela Avigliano

Frontend Engineer & Accessibility Engineer

4 min di lettura

Oggi vedremo come creare una paginazione da zero e renderla accessibile e riutilizzabile. Spero vi sia utile!

Github: https://github.com/micaavigliano/accessible-pagination
Progetto: https://accessible-pagination.netlify.app/

Custom hook per richiedere dati

Blocco di codice

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,
  }
};
  1. Genereremo un hook personalizzato con un generic type. Questo ci permetterà di specificare il tipo di dato atteso quando si usa questo hook.
  2. 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).
  3. Nel nostro stato const [data, setData] = useState<T | null>(null); passiamo il generic type T, 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 un aria-label che la identifichi come una paginazione.
  • Ogni elemento all’interno della paginazione deve includere aria-setsize e aria-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:
    screenshot dell'annuncio del voiceover: elenco di 1859 elementi
    screenshot dell'annuncio del voiceover: elenco di 1859 elementi

    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:
    screenshot del voiceover che annuncia: vai alla pagina 1. Pagina corrente, pulsante, posizione 1 di 1859
    screenshot del voiceover che annuncia: vai alla pagina 1. Pagina corrente, pulsante, posizione 1 di 1859

  • 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:
    voiceover dello screen reader che annuncia: pagina 3 caricata. Mostrando 20 elementi
    voiceover dello screen reader che annuncia: pagina 3 caricata. Mostrando 20 elementi

Per ottenere questo risultato, lo implementeremo nel modo seguente:

Blocco di codice

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.

Blocco di codice

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 stato currentPage nel modo seguente:

Blocco di codice

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 stato currentPage.

Blocco di codice

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:

Blocco di codice

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:

  1. Creare una funzione, in questo caso la chiamerò getPageNumbers.
  2. Creare variabili per il primo e l’ultimo elemento dell’elenco.
  3. Creare una variabile per l’ellissi sul lato sinistro. Per scelta personale, la mia ellissi si collocherà dopo il quarto elemento della lista.
  4. Creare una variabile per l’ellissi sul lato destro. Per scelta personale, la mia ellissi si collocherà prima degli ultimi tre elementi della lista.
  5. 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.

Blocco di codice

const pagesAroundCurrent = [currentPage - 2, currentPage - 1, currentPage, currentPage + 1, currentPage + 2].filter(page => page > firstPage && page < lastPage);
  1. Per l’ultima variabile, creeremo un array che contenga tutte le variabili create in precedenza.
  2. 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:

Blocco di codice

<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="true">…</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