
Cómo crear un dropdown (menu desplegable) accesible con reactjs
En otro tutorial ya creamos menu desplegable con html y css en esta ocasión aprenderás a crear pequeño menú desplegable estilo dropdown utilizando reactjs y javascript siguiendo las mejores prácticas de accesibilidad, uno de los puntos en los que fallan la mayoría de menús desplegables que se encuentran en la web.
Los dropdowns suelen estar escritos solo con css y para hacer aparecer la lista de enlaces se usa un efecto hover para este ejemplo usaremos javascript para que los elementos de la lista desplegable aparezcan cuando se haga click sobre el.
Código jsx de menú desplegable
A continuación tienes todo el código más adelante esta explicado paso a paso cada parte de este componente
jsximport React, { useState, useEffect, useRef } from "react";import {dropdown_wrapper,dropdown_activator,dropdown_item_list,active,item_list,} from "./dropdown.module.css";function Dropdown({ items = [], dropdownTitle }) {const activatorRef = useRef(null);const dropdownListRef = useRef(null);const [isOpen, setIsOpen] = useState(false);const clickHandler = () => {setIsOpen(!isOpen);};const keyHandler = event => {// console.log(event);if (event.key === "Escape" && isOpen) {setIsOpen(false);}};const clickOutsideHandler = event => {if (dropdownListRef.current) {if (dropdownListRef.current.contains(event.target) ||activatorRef.current.contains(event.target)) {return;}setIsOpen(false);}};useEffect(() => {if (isOpen) {dropdownListRef.current.querySelector("a").focus();document.addEventListener("mousedown", clickOutsideHandler);} else {document.addEventListener("mousedown", clickOutsideHandler);}}, [isOpen]);return (<div className={dropdown_wrapper} onKeyUp={keyHandler}><buttonclassName={dropdown_activator}aria-haspopup="true"aria-controls={dropdownTitle}onClick={clickHandler}ref={activatorRef}>{dropdownTitle}{" "}{isOpen ? (<svgheight="24"fill="rgb(70,70,70)"viewBox="0 0 24 24"width="24"xmlns="http://www.w3.org/2000/svg"><path d="m0 0h24v24h-24z" fill="none" /><path d="m7.41 15.41 4.59-4.58 4.59 4.58 1.41-1.41-6-6-6 6z" /></svg>) : (<svgheight="24"fill="rgb(70,70,70)"viewBox="0 0 24 24"width="24"xmlns="http://www.w3.org/2000/svg"><path d="m0 0h24v24h-24z" fill="none" /><path d="m7.41 8.59 4.59 4.58 4.59-4.58 1.41 1.41-6 6-6-6z" /></svg>)}</button><ulref={dropdownListRef}className={`${dropdown_item_list} ${isOpen ? active : ""} `}>{items.map((item, index) => {return (<li className={item_list} key={index}><a href={item.slug}>{item.anchor}</a></li>);})}</ul></div>);}export default Dropdown;
Css necesario para el funcionamiento del dropdown
Este código css esta escrito usando css modules funciona igual pero el archivo debe tener el formato nombre_de_estilos.module.css.
css.dropdown_wrapper {position: relative;display: inline-block;}.dropdown_activator {align-items: center;background-color: inherit;border: none;height: 100%;color: gray;font-weight: 500;letter-spacing: 1.1px;display: flex;align-items: center;font-size: inherit;padding: 1rem 0.6rem;cursor: pointer;width: 100%;}.dropdown_activator:hover {border-bottom: 1px solid silver;border-image: linear-gradient(to right,transparent 20%,#1a1b1b,transparent 80%) 30;}.dropdown_item_list {background: white;display: none;margin: 0;z-index: 1000;position: absolute;box-shadow: 0 0 2px 0 gray;border-radius: 5px;padding: 0;}.dropdown_item_list.active {display: grid;grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));}.item_list {list-style: none;}.item_list:last-child a {border-bottom: none;}.item_list a,.item_list a:link {display: flex;/* gap: 0.8rem; */color: #666666;font-weight: 600;text-decoration: none;padding: 0.8rem;transition: all 0.1s linear;}.item_list a:hover {background-color: gray;color: white;}
Explicación paso a paso del código del dropdown
Para crear este componente necesitaremos usar los ganchos useState, useEffect y useRef así que asegurate de importarlos en tu componente.
Props necesarias para nuestro componente
jsxfunction Dropdown({ items = [], dropdownTitle }) {/*Resto del código*/}
El Componente resivirá dos props:
items: los elementos que que conforman al menú, para este caso necesitamos un array de obejetos en el cual cada obejeto debe tener los valores slug y anchor, slug hacia donde apunte el enlace y anchor será el texto del enlace .
dropdownTitle: Esta propiedad será el texto visible en el botón que activará el desplegable
Hooks necesarios para el componente
jsx/* ...Resto del código */const activatorRef = useRef(null);const dropdownListRef = useRef(null);const [isOpen, setIsOpen] = useState(false);/* ...Resto del código */
activatorRef y dropdownListRef: necesitaremos acceder a los nodos del dom tanto del botón que despliega el dropdown, como de la lista de enlaces.
También un típico estado toggle para controlar el comportamiento del desplegable, debe estar inicializado en falso ya que en un primer momento el dropdown estará cerrado.
Funciones para modificar el estado del dropdown
jsxconst clickHandler = () => {setIsOpen(!isOpen);};const keyHandler = event => {// console.log(event);if (event.key === "Escape" && isOpen) {setIsOpen(false);}};const clickOutsideHandler = event => {if (dropdownListRef.current) {if (dropdownListRef.current.contains(event.target) ||activatorRef.current.contains(event.target)) {return;}setIsOpen(false);}};
clickHandler: esta función lo unico que hará es cambiar el estado de false a true viceversa siempre que se haga click sobre el activatorRef.
keyHandler: Este dropdown debe funcionar sin usar el ratón para hacerlo usable y accesible para todos los usuarios, así que con esta función escuchamos el teclado y en caso de que la tecla pulsada sea Escape cambiamos el estado para cerrar el dropdown.
La función keyHandler la ejecutaremos en el evento onKeyUp del elemento que envuelve todo el componente.
- clickOutsideHandler: Con esta función detectaremos cada vez que ocurra un click fuera del dropdown y en caso de haberlo echo cambiaremos el estado a falso. Por otro lado en caso que el click sea dentro de alguna de las dos referencias no se debe hacer nada por eso la primera condicional.
js// Condicional para verificar que la referencia del dropdwon existeif (dropdownListRef.current) {//Condicional para ver si el click es dentro del dropdown, si es el caso no hacer nada y si no cambiar el estado a falseif (dropdownListRef.current.contains(event.target) ||activatorRef.current.contains(event.target)) {return;}setIsOpen(false);}}
La función clickOutsideHandler la ejecutaremos dentro de un useEffect ya que necesitamos detectar los eventos de mouse de todo el documento.
Hook useEffect para detectar los clicks fuera el desplegable
jsuseEffect(() => {if (isOpen) {dropdownListRef.current.querySelector("a").focus();document.addEventListener("mousedown", clickOutsideHandler);} else {document.addEventListener("mousedown", clickOutsideHandler);}}, [isOpen]);
En este gancho ejecutaremos la función clickOutsideHandler sobre todo el documento usando el evento mousedown.
En el caso que el desplegable este abierto enfocaremos el enlace dentro del dropdown para continuar con el flujo correcto de elementos clicables para los usuarios que recorren el documento con el tabulador del teclado.
Este efecto se debe ejecutar cada vez que cambie el estado isOpen así que se lo pasamos como dependencia al useEffect.
Jsx y renderizado del dropdown
jsx<div className={dropdown_wrapper} onKeyUp={keyHandler}></div>
Elemento contenedor le pasamos la clase dropdown_wrapper que definimos en nuestro css y en el evento onKeyUp la función keyHandler
jsx<buttonclassName={dropdown_activator}aria-haspopup="true"aria-controls={dropdownTitle}onClick={clickHandler}ref={activatorRef}>{dropdownTitle}{" "}{isOpen ? (<svgheight="24"fill="rgb(70,70,70)"viewBox="0 0 24 24"width="24"xmlns="http://www.w3.org/2000/svg"><path d="m0 0h24v24h-24z" fill="none" /><path d="m7.41 15.41 4.59-4.58 4.59 4.58 1.41-1.41-6-6-6 6z" /></svg>) : (<svgheight="24"fill="rgb(70,70,70)"viewBox="0 0 24 24"width="24"xmlns="http://www.w3.org/2000/svg"><path d="m0 0h24v24h-24z" fill="none" /><path d="m7.41 8.59 4.59 4.58 4.59-4.58 1.41 1.41-6 6-6-6z" /></svg>)}</button>
Al botón que actuará como activador le pasamos la clase dropdown_activator, ejecutamos la función clickHandler en el evento Onclick y le pasamos la referencia activatorRef previamente definida con el gancho useRef.
el atributo aria-haspopup lo establecemos en true para indicar que es un elemento que aparecerá de forma interactiva y al atributo aria-controls le pasamos la prop dropdownTitle para identificarlo.
Aria es un conjunto de atributos que definen formas de hacer que el contenido web sea más accesible para usuarios con discapacidades aprende más en este enlace
Dentro del botón renderizamos el dropdownTitle y dependiendo del estado isOpen mostramos un svg con una flecha para arriba o para abajo
jsx<ulref={dropdownListRef}className={`${dropdown_item_list} ${isOpen ? active : ""} `}>{items.map((item, index) => {return (<li className={item_list} key={index}><a href={item.slug}>{item.anchor}</a></li>);})}</ul>
Este elemento renderizará nuestra lista de enlaces así que le pasamos la referencia dropdownListRef previamente definida.
Agregamos la clase dropdown_item_list para darle algo de estilos y también usamos un operador ternario para agregar la clase active dependiendo si el estado isOpen es verdadero o falso. La clase active es la encargada de hacer que la lista se muestre o se oculte.
Por último recorremos el arreglo items y renderizamos un li con un enlace en el que el atributo href es item.slug y el texto del enlace item.anchor
js// Estrutura del arreglo itemsconst items = [{slug: "/link1/",anchor: "Link 1",},{slug: "/link2/",anchor: "Link 2",},{slug: "/link3/",anchor: "Link 3",},];
No voy a explicar paso a paso el código css de este ejemplo, creo que es muy fácil de entender y estoy seguro que tú puedes hacerlo mucho mejor que yo, espero haberte ayudado muchas gracias por visitar mi web.