Cree sus propios paneles de contenido expandibles y contraídos
Publicado: 2022-03-10Los hemos llamado 'panel de apertura y cierre' hasta ahora, pero también se describen como paneles de expansión, o más simplemente, paneles de expansión.
Para aclarar exactamente de qué estamos hablando, diríjase a este ejemplo en CodePen:
Eso es lo que construiremos en este breve tutorial.
Desde el punto de vista de la funcionalidad, hay algunas formas de lograr la apertura y el cierre animados que estamos buscando. Cada enfoque con sus propios beneficios y compensaciones. Voy a compartir los detalles de mi método 'ir a' en detalle en este artículo. Consideremos primero los posibles enfoques.
Enfoques
Hay variaciones en estas técnicas, pero en términos generales, los enfoques se dividen en una de tres categorías:
- Animar/cambiar la
height
o la alturamax-height
del contenido. - Usa
transform: translateY
para mover los elementos a una nueva posición, dando la ilusión de un panel cerrándose y luego vuelve a renderizar el DOM una vez que la transformación esté completa con los elementos en su posición final. - ¡Use una biblioteca que haga alguna combinación/variación de 1 o 2!
Consideraciones de cada enfoque
Desde una perspectiva de rendimiento, usar una transformación es más efectivo que animar o cambiar la altura/altura máxima. Con una transformación, los elementos en movimiento se rasterizan y la GPU los desplaza. Esta es una operación fácil y económica para una GPU, por lo que el rendimiento tiende a ser mucho mejor.
Los pasos básicos cuando se utiliza un enfoque de transformación son:
- Obtenga la altura del contenido que se colapsará.
- Mueva el contenido y todo lo que sigue por la altura del contenido que se colapsará usando
transform: translateY(Xpx)
. Opere la transformación con la transición de su elección para dar un efecto visual agradable. - Use JavaScript para escuchar el evento de final de
transitionend
. Cuando se dispare,display: none
el contenido y elimine la transformación y todo debería estar en el lugar correcto.
No suena tan mal, ¿verdad?
Sin embargo, hay una serie de consideraciones con esta técnica, por lo que tiendo a evitarla para implementaciones casuales, a menos que el rendimiento sea absolutamente crucial.
Por ejemplo, con el enfoque transform: translateY
, debe considerar el z-index
de los elementos. De forma predeterminada, los elementos que se transforman hacia arriba están después del elemento desencadenante en el DOM y, por lo tanto, aparecen encima de las cosas que están delante de ellos cuando se traducen hacia arriba.
También debe considerar cuántas cosas aparecen después del contenido que desea colapsar en el DOM. Si no quiere un gran agujero en su diseño, puede que le resulte más fácil usar JavaScript para envolver todo lo que quiere mover en un elemento contenedor y simplemente moverlo. Manejable, ¡pero acabamos de introducir más complejidad! Este es, sin embargo, el tipo de enfoque que elegí cuando movía a los jugadores hacia arriba y hacia abajo en Entrada/Salida. Puedes ver cómo se hizo eso aquí.
Para necesidades más informales, tiendo a hacer la transición de la max-height
del contenido. Este enfoque no funciona tan bien como una transformación. El motivo es que el navegador está interpolando la altura del elemento colapsado a lo largo de la transición; eso provoca muchos cálculos de diseño que no son tan económicos para la computadora host.
Sin embargo, este enfoque gana desde el punto de vista de la simplicidad. La recompensa de sufrir el impacto computacional mencionado anteriormente es que el reflujo de DOM se encarga de la posición y la geometría de todo. Tenemos muy pocos cálculos para escribir, además el JavaScript necesario para llevarlo a cabo bien es comparativamente simple.
El elefante en la habitación: detalles y elementos resumidos
Aquellos con un conocimiento íntimo de los elementos de HTML sabrán que existe una solución HTML nativa para este problema en forma de details
y elementos de summary
. Aquí hay un ejemplo de marcado:
<details> <summary>Click to open/close</summary> Here is the content that is revealed when clicking the summary... </details>
De forma predeterminada, los navegadores proporcionan un pequeño triángulo desplegable junto al elemento de resumen; haga clic en el resumen y se revelará el contenido debajo del resumen.
Genial, ¿eh? Los detalles incluso admiten el evento de toggle
en JavaScript, por lo que puede hacer este tipo de cosas para realizar diferentes cosas en función de si está abierto o cerrado (no se preocupe si ese tipo de expresión de JavaScript parece extraño; llegaremos a eso en más detalle en breve):
details.addEventListener("toggle", () => { details.open ? thisCoolThing() : thisOtherThing(); })
Bien, voy a detener tu entusiasmo allí mismo. Los detalles y los elementos de resumen no están animados. No de forma predeterminada y actualmente no es posible hacer que se abran y cierren en animación/transición con CSS y JavaScript adicionales.
Si sabes lo contrario, me encantaría que me demuestren que estoy equivocado.
Lamentablemente, como necesitamos una estética de apertura y cierre, tendremos que arremangarnos y hacer el mejor y más accesible trabajo que podamos con las otras herramientas a nuestra disposición.
Bien, con las noticias deprimentes fuera del camino, sigamos haciendo que esto suceda.
Patrón de marcado
El marcado básico se verá así:
<div class="container"> <button type="button" class="trigger">Show/Hide content</button> <div class="content"> All the content here </div> </div>
Tenemos un contenedor exterior para envolver el expansor y el primer elemento es el botón que sirve como disparador de la acción. ¿Observe el atributo de tipo en el botón? Siempre lo incluyo porque, de forma predeterminada, un botón dentro de un formulario realizará un envío. Si pierde un par de horas preguntándose por qué su formulario no funciona y los botones están involucrados en su formulario; ¡asegúrate de verificar el atributo de tipo!
El siguiente elemento después del botón es el propio cajón de contenido; todo lo que quieras esconder y mostrar.
Para dar vida a las cosas, utilizaremos propiedades personalizadas de CSS, transiciones de CSS y un poco de JavaScript.
Lógica básica
La lógica básica es esta:
- Deja que cargue la página, mide la altura del contenido.
- Establezca la altura del contenido en el contenedor como el valor de una propiedad personalizada de CSS.
- Oculte inmediatamente el contenido añadiéndole un atributo
aria-hidden: "true"
. El usoaria-hidden
garantiza que la tecnología de asistencia sepa que el contenido también está oculto. - Conecte el CSS para que la
max-height
de la clase de contenido sea el valor de la propiedad personalizada. - Al presionar nuestro botón de activación, la propiedad aria-hidden cambia de verdadero a falso, lo que a su vez cambia la
max-height
del contenido entre0
y la altura establecida en la propiedad personalizada. Una transición en esa propiedad proporciona el estilo visual: ¡ajústelo al gusto!
Nota: ahora, este sería un caso simple de alternar una clase o atributo si max-height: auto
igualara la altura del contenido. Lamentablemente no es así. Ve y grita sobre eso al W3C aquí.
Veamos cómo se manifiesta ese enfoque en el código. Los comentarios numerados muestran los pasos lógicos equivalentes de arriba en el código.
Aquí está el JavaScript:
// Get the containing element const container = document.querySelector(".container"); // Get content const content = document.querySelector(".content"); // 1. Get height of content you want to show/hide const heightOfContent = content.getBoundingClientRect().height; // Get the trigger element const btn = document.querySelector(".trigger"); // 2. Set a CSS custom property with the height of content container.style.setProperty("--containerHeight", `${heightOfContent}px`); // Once height is read and set setTimeout(e => { document.documentElement.classList.add("height-is-set"); 3. content.setAttribute("aria-hidden", "true"); }, 0); btn.addEventListener("click", function(e) { container.setAttribute("data-drawer-showing", container.getAttribute("data-drawer-showing") === "true" ? "false" : "true"); // 5. Toggle aria-hidden content.setAttribute("aria-hidden", content.getAttribute("aria-hidden") === "true" ? "false" : "true"); })
El CSS:
.content { transition: max-height 0.2s; overflow: hidden; } .content[aria-hidden="true"] { max-height: 0; } // 4. Set height to value of custom property .content[aria-hidden="false"] { max-height: var(--containerHeight, 1000px); }
Puntos de nota
¿Qué pasa con varios cajones?
Cuando tenga varios cajones abiertos y ocultos en una página, deberá recorrerlos todos, ya que probablemente serán de diferentes tamaños.
Para manejar eso, necesitaremos hacer un querySelectorAll
para obtener todos los contenedores y luego volver a ejecutar su configuración de variables personalizadas para cada contenido dentro de un forEach
.
Ese tiempo de espera establecido
Tengo un setTimeout
con 0
de duración antes de configurar el contenedor para que se oculte. Podría decirse que esto no es necesario, pero lo uso como un enfoque de "cinturón y llaves" para garantizar que la página se haya renderizado primero para que las alturas del contenido estén disponibles para ser leídas.
Dispare esto solo cuando la página esté lista
Si tiene otras cosas en marcha, puede elegir envolver su código de cajón en una función que se inicializa en la carga de la página. Por ejemplo, supongamos que la función del cajón se envolvió en una función llamada initDrawers
, podríamos hacer esto:
window.addEventListener("load", initDrawers);
De hecho, lo agregaremos en breve.
Datos adicionales-* atributos en el contenedor
Hay un atributo de datos en el contenedor externo que también se alterna. Esto se agrega en caso de que haya que cambiar algo con el gatillo o el contenedor a medida que se abre/cierra el cajón. Por ejemplo, tal vez queramos cambiar el color de algo o revelar o alternar un ícono.
Valor predeterminado en la propiedad personalizada
Hay un valor predeterminado establecido en la propiedad personalizada en CSS de 1000px
. Ese es el bit después de la coma dentro del valor: var(--containerHeight, 1000px)
. Esto significa que si --containerHeight
se estropea de alguna manera, aún debería tener una transición decente. Obviamente, puede configurarlo en lo que sea adecuado para su caso de uso.
¿Por qué no usar simplemente un valor predeterminado de 100000 px?
Dado que max-height: auto
no hace la transición, es posible que se pregunte por qué no opta por una altura establecida de un valor mayor de lo que necesitaría. Por ejemplo, 10000000px?
El problema con ese enfoque es que siempre hará la transición desde esa altura. Si la duración de su transición se establece en 1 segundo, la transición 'viajará' 10000000px en un segundo. Si su contenido tiene solo 50 px de alto, ¡obtendrá un efecto de apertura/cierre bastante rápido!
Operador ternario para conmutadores
Hemos utilizado un operador ternario un par de veces para alternar atributos. Algunas personas los odian, pero yo, y otros, los amamos. Pueden parecer un poco extraños y un poco de 'código de golf' al principio, pero una vez que te acostumbras a la sintaxis, creo que son una lectura más sencilla que un if/else estándar.
Para los no iniciados, un operador ternario es una forma condensada de if/else. Están escritos de modo que lo que hay que comprobar es primero, luego el ?
separa qué ejecutar si la verificación es verdadera, y luego :
para distinguir qué debe ejecutarse si la verificación es falsa.
isThisTrue ? doYesCode() : doNoCode();
Nuestros conmutadores de atributos funcionan comprobando si un atributo está configurado como "true"
y, de ser así, configúrelo como "false"
, de lo contrario, configúrelo como "true"
.
¿Qué sucede al cambiar el tamaño de la página?
Si un usuario cambia el tamaño de la ventana del navegador, existe una alta probabilidad de que cambien las alturas de nuestro contenido. Por lo tanto, es posible que desee volver a ejecutar la configuración de la altura de los contenedores en ese escenario. Ahora que estamos considerando tales eventualidades, parece un buen momento para refactorizar un poco las cosas.
Podemos hacer una función para establecer las alturas y otra función para manejar las interacciones. Luego agregue dos oyentes en la ventana; uno para cuando se carga el documento, como se mencionó anteriormente, y luego otro para escuchar el evento de cambio de tamaño.
Un poco más de A11Y
Es posible agregar un poco más de consideración para la accesibilidad haciendo uso de los atributos aria-expanded
, aria-controls
y aria-labelledby
. Esto dará una mejor indicación a la tecnología asistida cuando los cajones se hayan abierto/expandido. Agregamos aria-expanded="false"
a nuestro marcado de botón junto con aria-controls="IDofcontent"
, donde IDofcontent
es el valor de una identificación que agregamos al contenedor de contenido.
Luego usamos otro operador ternario para alternar el atributo aria-expanded
al hacer clic en JavaScript.
Todos juntos
Con la carga de la página, múltiples cajones, trabajo adicional de A11Y y manejo de eventos de cambio de tamaño, nuestro código JavaScript se ve así:
var containers; function initDrawers() { // Get the containing elements containers = document.querySelectorAll(".container"); setHeights(); wireUpTriggers(); window.addEventListener("resize", setHeights); } window.addEventListener("load", initDrawers); function setHeights() { containers.forEach(container => { // Get content let content = container.querySelector(".content"); content.removeAttribute("aria-hidden"); // Height of content to show/hide let heightOfContent = content.getBoundingClientRect().height; // Set a CSS custom property with the height of content container.style.setProperty("--containerHeight", `${heightOfContent}px`); // Once height is read and set setTimeout(e => { container.classList.add("height-is-set"); content.setAttribute("aria-hidden", "true"); }, 0); }); } function wireUpTriggers() { containers.forEach(container => { // Get each trigger element let btn = container.querySelector(".trigger"); // Get content let content = container.querySelector(".content"); btn.addEventListener("click", () => { btn.setAttribute("aria-expanded", btn.getAttribute("aria-expanded") === "false" ? "true" : "false"); container.setAttribute( "data-drawer-showing", container.getAttribute("data-drawer-showing") === "true" ? "false" : "true" ); content.setAttribute( "aria-hidden", content.getAttribute("aria-hidden") === "true" ? "false" : "true" ); }); }); }
También puedes jugar con él en CodePen aquí:
Resumen
Es posible seguir refinando y atendiendo más y más situaciones durante algún tiempo, pero la mecánica básica para crear un cajón de apertura y cierre confiable para su contenido ahora debería estar a su alcance. Con suerte, usted también es consciente de algunos de los peligros. El elemento de details
no se puede animar, max-height: auto
no hace lo que esperaba, no puede agregar de manera confiable un valor masivo de altura máxima y esperar que todos los paneles de contenido se abran como se esperaba.
Para reiterar nuestro enfoque aquí: mida el contenedor, almacene su altura como una propiedad personalizada de CSS, oculte el contenido y luego use un interruptor simple para cambiar entre max-height
de 0 y la altura que almacenó en la propiedad personalizada.
Puede que no sea el método de mejor rendimiento absoluto, pero he descubierto que para la mayoría de las situaciones es perfectamente adecuado y se beneficia de ser comparativamente sencillo de implementar.