DEV Community

Cover image for Componentes accesibles: Paginación
Mica
Mica

Posted on

Componentes accesibles: Paginación

Hoy vamos a ver cómo crear una paginación de cero y hacerla accesible y reutilizable. Espero que les sirva y me dejen sus comentarios al final del post!

Github: https://github.com/micaavigliano/accessible-pagination
Proyecto: https://accessible-pagination.vercel.app/

Custom hook to fetch data

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,
  }
};
Enter fullscreen mode Exit fullscreen mode
  1. Vamos a generar un custom hook con un generic type. Esto nos va a permitir especificar el tipo de dato esperado cuando se usa este hook
  2. Vamos a esperar 3 parámetros. Uno para url de donde vamos a fetchear la data, currentPage que es la página donde estamos y por default es 0 y pageSize que es el número de items que vamos a tener por página y por default es 20 (pueden cambiarle este valor).
  3. En nuestro estado const [data, setData] = useState<T | null>(null); le pasamos el generic type T ya que a medida que lo usemos para diferentes peticiones de data vamos a esperar diferentes tipos de datos.

Paginación

Para que una paginación sea accesible debemos tener en cuenta los siguientes puntos:

  • El foco debe moverse por todos los elementos interactivos de la páginación y tener un indicador visible
  • para asegurar una buena interacción con los lectores de pantalla debemos utilizar correctamente las regiones, propiedades y estados de manera correcta
  • La páginación debe estar agrupada dentro de un tag y contener un aria-label que la identifique como una paginación per se.
  • Cada item dentro de la paginación debe contener un aria-setsize y un aria-pointset. Ahora, ¿para qué sirven? Bueno, aria-setsize sirve para calcular el total de items dentro de la lista de la paginación. El lector de pantalla lo anunciará de la siguiente manera:

Captura de pantalla del screen reader voiceover que anuncia: lista de 1859 items

aria-pointset sirve para calcular la posición del item dentro de la totalidad de items en la páginación. El lector de pantalla lo anunciará de la siguiente manera:

Captura de pantalla del screen reader voiceover que anuncia: ir a la página 1. Current page, botón, posición 1 de 1859

  • Cada item debe tener un aria-label para poder identificar a qué página vamos a ir si presionamos sobre ese botón.
  • Tener botones para ir al siguiente/previo elemento y cada uno de estos botones debe tener su aria-label correspondiente
  • Si nuestra paginación contiene una ellipsis, la misma debe tener correctamente marcada con un aria-label
  • Cada vez que vamos a una nueva página, el screen reader debe anunciar en qué página estamos y cuántos items nuevos hay de la siguiente manera.

Captura de pantalla del screen reader voiceover que anuncia: página 3 cargada. Mostrando 20 items

Para poder llegar a esto vamos a codearlo de la siguiente manera:

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

Cuando la página deje de cargar, vamos a setear un nuevo mensaje con nuestra currentPage y la longitud del nuevo array que estamos cargando.

Ahora sí! Pasemos a ver cómo esta estructurado el código en el archivo pagination.tsx

El componente va a requerir de cinco props

interface PaginationProps {
  currentPage: number;
  totalPages: number;
  nextPage: () => void;
  prevPage: () => void;
  goToPage: (page: number) => void;
}
Enter fullscreen mode Exit fullscreen mode
  • currentPage se va a referir a la página actual. Esta misma la vamos a manejar con estamos en el componente donde deseemos utilizar la paginación de la siguiente manera: const [currentPage, setCurrentPage] = useState<number>(1);
  • totalPages se refiere al total de items a mostrar que contiene la API
  • nextPage esta función nos permitirá ir a la siguiente página y actualizar nuestro estado currentPage de la siguiente manera:
const handlePageChange = (newPage: number) => {
    setCurrentPage(newPage); 
  };

  const nextPage = () => {
    if (currentPage < totalPages) {
      handlePageChange(currentPage + 1);
    }
  };
Enter fullscreen mode Exit fullscreen mode
  • prevPage esta función nos permitirá ir a la página previa a nuestra página actual y actualizar nuestro estado currentPage
const prevPage = () => {
    if (currentPage > 1) {
      handlePageChange(currentPage - 1);
    }
  };
Enter fullscreen mode Exit fullscreen mode
  • goToPage esta función va a necesitar un parámetro numérico y es la función que cada item va a tener para poder ir a la página deseada. Vamos a hacerla funcionar de la siguiente manera:
const handlePageChange = (newPage: number) => {
    setCurrentPage(newPage); 
};
Enter fullscreen mode Exit fullscreen mode

Para que nuestra paginación cobre vida nos falta un paso más, ¡crear el array que vamos a iterar en nuestra lista! Para eso debemos seguir los siguientes pasos:

  1. Crear una función, en este caso la llamaré getPageNumbers
  2. Crear variables para el primer y el último item del listado.
  3. Crea una variable para la elipsis del lado izquierdo. Por decisión propia, mi elipsis se va a ubicar luego del cuarto elemento de la lista.
  4. Crear una variable para la elipsis del lado derecho. Por decisión propia, mi elipsis se va a ubicar previo a tres items en la lista.
  5. Crear una función que nos devuelva un array donde estén centrados siempre 5 items, la página actual, dos items previos y dos items subsiguientes. En caso de necesitamos, vamos a excluir a la primera y última página const pagesAroundCurrent = [currentPage - 2, currentPage - 1, currentPage, currentPage + 1, currentPage + 2].filter(page => page > firstPage && page < lastPage);
  6. Para nuestra última variable, vamos a crear un array que contenga todas las variables previamente creadas.
  7. Por último, vamos a filtrar los elementos null y devolver el array.

Este array es el que vamos a recorrer para obtener el listado de items en nuestra paginación de la siguiente manera:

<ol className='flex gap-3'>
          {pageNumbers.map((number) => {
            if (number === 'left-ellipsis' || number === 'right-ellipsis') {
              return (
                <span key={number} className='relative top-5' aria-label='ellipsis'>
                  ...
                </span>
              );
            }
            return (
              <li aria-setsize={totalPages} aria-posinset={typeof number === 'number' ? number : undefined} key={`page-${number}`}>
                <button
                  onClick={() => goToPage(Number(number))}
                  className={currentPage === Number(number) ? 'underline underline-offset-3 border-zinc-300' : ''}
                  aria-label={`go to page ${number}`}
                  aria-current={currentPage === Number(number) && 'page'}
                >
                  {number}
                </button>
              </li>
            );
          })}
        </ol>
Enter fullscreen mode Exit fullscreen mode

Y hasta acá cómo realizar una paginación reutilizable y accesible! Personalmente, aprendí a realizar una páginación de cero a los golpes porque tuve que implementarla en un live coding, espero que mi experiencia le sea de ayuda para su carrera y puedan implementar y ¡hasta mejorarla!

Saludos,
Mica<3

Top comments (0)