Cómo compilar un componente de aviso

Una descripción general fundamental de cómo compilar un componente de aviso adaptable y accesible.

En esta publicación, quiero compartir mis ideas sobre cómo compilar un componente de notificación. Prueba la demostración.

Demostración

Si prefieres ver un video, aquí tienes una versión de esta publicación en YouTube:

Descripción general

Los avisos son mensajes breves no interactivos, pasivos y asíncronos para los usuarios. Por lo general, se usan como patrón de comentarios de la interfaz para informar al usuario sobre los resultados de una acción.

Interacciones

A diferencia de las notificaciones, las alertas y las instrucciones, los avisos no son interactivos y no están diseñados para descartarse ni persistir. Las notificaciones son para información más importante, mensajes síncronos que requieren interacción o mensajes a nivel del sistema (en lugar de a nivel de la página). Los avisos emergentes son más pasivos que otras estrategias de avisos.

Marca

El elemento <output> es una buena opción para el aviso porque se anuncia a los lectores de pantalla. El código HTML correcto proporciona una base segura para mejorar con JavaScript y CSS, y habrá mucho JavaScript.

Un brindis

<output class="gui-toast">Item added to cart</output>

Para que sea más inclusiva, agrega role="status". Esto proporciona un resguardo si el navegador no les otorga a los elementos <output> la función implícita según la especificación.

<output role="status" class="gui-toast">Item added to cart</output>

Un contenedor de notificaciones

Se puede mostrar más de un aviso a la vez. Para organizar varios avisos, se usa un contenedor. Este contenedor también controla la posición de los mensajes en la pantalla.

<section class="gui-toast-group">
  <output role="status">Wizard Rose added to cart</output>
  <output role="status">Self Watering Pot added to cart</output>
</section>

Diseños

Elegí fijar los avisos en el inset-block-end del viewport y, si se agregan más avisos, se apilan desde ese borde de la pantalla.

Contenedor de la GUI

El contenedor de notificaciones hace todo el trabajo de diseño para presentarlas. Es fixed para el viewport y usa la propiedad lógica inset para especificar a qué bordes fijar los datos, más un poco de padding del mismo borde block-end.

.gui-toast-group {
  position: fixed;
  z-index: 1;
  inset-block-end: 0;
  inset-inline: 0;
  padding-block-end: 5vh;
}

Captura de pantalla con el tamaño del cuadro y el padding de Herramientas para desarrolladores superpuestos sobre un elemento .gui-toast-container.

Además de posicionarse dentro del viewport, el contenedor de notificaciones emergentes es un contenedor de cuadrícula que puede alinear y distribuir notificaciones emergentes. Los elementos se centran como un grupo con justify-content y se centran de forma individual con justify-items. Agrega un poco de gap para que los avisos no se toquen.

.gui-toast-group {
  display: grid;
  justify-items: center;
  justify-content: center;
  gap: 1vh;
}

Captura de pantalla con la superposición de cuadrícula de CSS en el grupo de notificaciones, esta vez, destacando el espacio y los espacios entre los elementos secundarios de la notificación.

Notificación de la GUI

Una notificación individual tiene algunos padding, algunas esquinas más suaves con border-radius y una función min() para ayudar con el tamaño de los dispositivos móviles y de escritorio. El tamaño responsivo en el siguiente CSS evita que los avisos emergentes sean más anchos que el 90% del viewport o 25ch.

.gui-toast {
  max-inline-size: min(25ch, 90vw);
  padding-block: .5ch;
  padding-inline: 1ch;
  border-radius: 3px;
  font-size: 1rem;
}

Captura de pantalla de un solo elemento .gui-toast, con el padding y el radio del borde.

Estilos

Con el diseño y el posicionamiento establecidos, agrega CSS que ayude a adaptarse a la configuración y las interacciones del usuario.

Contenedor de tostadas

Los avisos no son interactivos, por lo que no sucede nada si se presionan o se desliza el dedo sobre ellos, pero actualmente consumen eventos de puntero. Evita que los avisos emergentes roben clics con el siguiente CSS.

.gui-toast-group {
  pointer-events: none;
}

Aviso de GUI

Dales a los avisos emergentes un tema adaptable claro o oscuro con propiedades personalizadas, HSL y una consulta de medios de preferencia.

.gui-toast {
  --_bg-lightness: 90%;

  color: black;
  background: hsl(0 0% var(--_bg-lightness) / 90%);
}

@media (prefers-color-scheme: dark) {
  .gui-toast {
    color: white;
    --_bg-lightness: 20%;
  }
}

Animación

Se debería mostrar un nuevo aviso con una animación a medida que entra en la pantalla. Para adaptarse al movimiento reducido, se establecen los valores de translate en 0 de forma predeterminada, pero se actualiza el valor de movimiento a una longitud en una consulta de contenido multimedia de preferencia de movimiento. Todos obtienen una animación, pero solo algunos usuarios tienen el aviso que se desplaza.

Estos son los fotogramas clave que se usan para la animación del aviso. CSS controlará la entrada, la espera y la salida del aviso, todo en una animación.

@keyframes fade-in {
  from { opacity: 0 }
}

@keyframes fade-out {
  to { opacity: 0 }
}

@keyframes slide-in {
  from { transform: translateY(var(--_travel-distance, 10px)) }
}

Luego, el elemento de tostada configura las variables y organiza los fotogramas clave.

.gui-toast {
  --_duration: 3s;
  --_travel-distance: 0;

  will-change: transform;
  animation: 
    fade-in .3s ease,
    slide-in .3s ease,
    fade-out .3s ease var(--_duration);
}

@media (prefers-reduced-motion: no-preference) {
  .gui-toast {
    --_travel-distance: 5vh;
  }
}

JavaScript

Con los estilos y el lector de pantalla aptos para HTML accesible, se necesita JavaScript para organizar la creación, adición y destrucción de avisos basados en eventos del usuario. La experiencia del desarrollador del componente de aviso debe ser mínima y fácil de comenzar a usar, como esta:

import Toast from './toast.js'

Toast('My first toast')

Crea el grupo y los avisos

Cuando el módulo de aviso se carga desde JavaScript, debe crear un contenedor de aviso y agregarlo a la página. Elegí agregar el elemento antes de body, lo que hará que sea poco probable que se produzcan problemas de apilamiento de z-index, ya que el contenedor está por encima del contenedor de todos los elementos del cuerpo.

const init = () => {
  const node = document.createElement('section')
  node.classList.add('gui-toast-group')

  document.firstElementChild.insertBefore(node, document.body)
  return node
}

Captura de pantalla del grupo de avisos entre las etiquetas de la cabeza y el cuerpo.

Se llama a la función init() de forma interna al módulo, y se oculta el elemento como Toaster:

const Toaster = init()

La creación de elementos HTML de Toast se realiza con la función createToast(). La función requiere texto para el aviso, crea un elemento <output>, lo adorna con algunas clases y atributos, establece el texto y muestra el nodo.

const createToast = text => {
  const node = document.createElement('output')
  
  node.innerText = text
  node.classList.add('gui-toast')
  node.setAttribute('role', 'status')

  return node
}

Cómo administrar uno o varios avisos

JavaScript ahora agrega un contenedor al documento para contener los avisos y está listo para agregar los avisos creados. La función addToast() organiza el manejo de uno o varios avisos. Primero, verifica la cantidad de notificaciones y si el movimiento está bien. Luego, usa esta información para agregar la notificación o hacer una animación elegante para que las otras notificaciones parezcan “hacer espacio” para la nueva.

const addToast = toast => {
  const { matches:motionOK } = window.matchMedia(
    '(prefers-reduced-motion: no-preference)'
  )

  Toaster.children.length && motionOK
    ? flipToast(toast)
    : Toaster.appendChild(toast)
}

Cuando se agrega el primer aviso, Toaster.appendChild(toast) agrega un aviso a la página que activa las animaciones de CSS: animar para entrar, esperar 3s, animar fuera. Se llama a flipToast() cuando hay notificaciones existentes, y se emplea una técnica llamada FLIP de Paul Lewis. La idea es calcular la diferencia en las posiciones del contenedor, antes y después de agregar el nuevo aviso. Piensa en ello como marcar dónde está la tostadora ahora, dónde estará y, luego, animar desde donde estaba hasta donde está.

const flipToast = toast => {
  // FIRST
  const first = Toaster.offsetHeight

  // add new child to change container size
  Toaster.appendChild(toast)

  // LAST
  const last = Toaster.offsetHeight

  // INVERT
  const invert = last - first

  // PLAY
  const animation = Toaster.animate([
    { transform: `translateY(${invert}px)` },
    { transform: 'translateY(0)' }
  ], {
    duration: 150,
    easing: 'ease-out',
  })
}

La cuadrícula de CSS levanta el diseño. Cuando se agrega un nuevo aviso, la cuadrícula lo coloca al principio y lo separa de los demás. Mientras tanto, se usa una animación web para animar el contenedor desde la posición anterior.

Cómo combinar todo el código JavaScript

Cuando se llama a Toast('my first toast'), se crea un aviso, se agrega a la página (tal vez incluso el contenedor esté animado para adaptarse al nuevo aviso), se muestra una promesa y se supervisa el aviso creado para completar la animación de CSS (las tres animaciones de fotogramas clave) para la resolución de la promesa.

const Toast = text => {
  let toast = createToast(text)
  addToast(toast)

  return new Promise(async (resolve, reject) => {
    await Promise.allSettled(
      toast.getAnimations().map(animation => 
        animation.finished
      )
    )
    Toaster.removeChild(toast)
    resolve() 
  })
}

Creo que la parte confusa de este código está en la función Promise.allSettled() y la asignación toast.getAnimations(). Como usé varias animaciones de fotogramas clave para el aviso, para saber con seguridad que todas terminaron, cada una debe solicitarse desde JavaScript y cada una de sus promesas de finished se debe observar para su finalización. allSettled hace eso por nosotros y se resuelve como completo una vez que se cumplen todas sus promesas. El uso de await Promise.allSettled() significa que la siguiente línea de código puede quitar el elemento con confianza y suponer que el aviso de notificación completó su ciclo de vida. Por último, llamar a resolve() cumple la promesa de Toast de alto nivel para que los desarrolladores puedan realizar tareas de limpieza o realizar otro trabajo una vez que se muestre el aviso.

export default Toast

Por último, la función Toast se exporta desde el módulo para que otras secuencias de comandos la importen y usen.

Cómo usar el componente de aviso

Para usar el aviso o la experiencia del desarrollador del aviso, importa la función Toast y llámala con una cadena de mensaje.

import Toast from './toast.js'

Toast('Wizard Rose added to cart')

Si el desarrollador quiere realizar tareas de limpieza o cualquier otra tarea después de que se muestre el aviso, puede usar async y await.

import Toast from './toast.js'

async function example() {
  await Toast('Wizard Rose added to cart')
  console.log('toast finished')
}

Conclusión

Ahora que sabes cómo lo hice, ¿cómo lo harías tú? 🙂

Diversifiquemos nuestros enfoques y aprendamos todas las formas de desarrollar en la Web. Crea una demo, twittea los vínculos y los agregaré a la sección de remixes de la comunidad a continuación.

Remixes de la comunidad