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 algunas ideas sobre cómo crear un componente de aviso. Prueba la demostración.

Demostración

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

Descripción general

Los avisos son mensajes cortos 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

Los avisos no son como las notificaciones, las alertas y las mensajes porque no son interactivos y no están destinados a descartarse ni conservar. Las notificaciones contienen información más importante, mensajes síncronos que requieren interacción o mensajes a nivel del sistema (a diferencia del nivel de la página). Los avisos 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 HTML correcto nos brinda una base segura para mejorar con JavaScript y CSS, y habrá mucho JavaScript.

Un aviso

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

Puedes agregar role="status" para ser más inclusivo. Esto proporciona un resguardo en caso de que el navegador no les otorgue 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 avisos

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 avisos 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 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 avisos hace todo el trabajo de diseño para presentar los avisos. Es fixed para el viewport y usa la propiedad lógica inset para especificar en qué bordes fijar, además de 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 y el padding de Herramientas para desarrolladores superpuestos sobre un elemento .gui-toast-container.

Además de posicionarse dentro del viewport, el contenedor de avisos es un contenedor de cuadrícula que puede alinear y distribuir avisos. Los elementos se centran como un grupo con justify-content y se centran de forma individual con justify-items. Agrégale 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 la cuadrícula de CSS en el grupo de avisos, en la que se destaca el espacio y los espacios entre los elementos secundarios de los avisos.

Aviso de la GUI

Un aviso individual tiene algunos padding, algunas esquinas más suaves con border-radius y una función min() para ayudar a ajustar el tamaño de dispositivos móviles y computadoras de escritorio. El tamaño responsivo en el siguiente CSS evita que los avisos se expandan a un valor superior al 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 que se muestran.

Estilos

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

Contenedor de avisos

Los avisos no son interactivos. Presionarlos o deslizarlos no hace nada, pero, por el momento, consumen eventos de puntero. Evita que los avisos roben clics con el siguiente CSS.

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

Aviso de la GUI

Aplica a los avisos un tema adaptable oscuro o claro 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 aviso nuevo con una animación cuando entra en la pantalla. Para adaptar el movimiento reducido, se deben establecer los valores translate en 0 de forma predeterminada, pero se actualiza el valor de movimiento a una longitud en una consulta de medios de preferencia de movimiento . Todo el mundo tiene animación, pero solo algunos usuarios tienen el aviso de recorrer una distancia.

Estos son los fotogramas clave utilizados para la animación del aviso. CSS controlará la entrada, la espera y la salida del aviso, todo en una sola 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 aviso 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 HTML accesible del lector de pantalla, se necesita JavaScript para organizar la creación, adición y destrucción de avisos basados en eventos del usuario. La experiencia del desarrollador con el componente de aviso debe ser mínima y fácil de comenzar, de la siguiente manera:

import Toast from './toast.js'

Toast('My first toast')

Creando el grupo de avisos y los avisos

Cuando el módulo de avisos se carga desde JavaScript, debe crear un contenedor de avisos y agregarlo a la página. Elegí agregar el elemento antes de body. Esto hará que los problemas de apilamiento de z-index sean poco probables, ya que el contenedor está por encima del contenedor para 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 almacena el elemento como Toaster:

const Toaster = init()

La creación del elemento HTML del aviso 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 controlar 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 control de uno o varios avisos. Primero, verifica la cantidad de avisos y si el movimiento está bien. Luego, usa esta información para agregar el aviso o hacer una animación sofisticada para que los otros avisos parezcan "hacer lugar" para el nuevo aviso.

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: animación de entrada, espera 3s y animación de salida. Se llama a flipToast() cuando hay avisos existentes, mediante 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 eso como marcar dónde está la Toaster ahora, dónde va a 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 realiza la elevación del diseño. Cuando se agrega un nuevo aviso, la cuadrícula lo coloca al inicio y lo coloca en un espacio con los demás. Mientras tanto, se usa una animación web para animar el contenedor desde la posición anterior.

Cómo juntar todo el código JavaScript

Cuando se llama a Toast('my first toast'), se crea un aviso y se agrega a la página (tal vez incluso se anima el contenedor para adaptarse al nuevo aviso), se muestra una promesa y se mira 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() 
  })
}

Me pareció que la parte confusa de este código se encuentra en la función Promise.allSettled() y en la asignación toast.getAnimations(). Como usé varias animaciones de fotogramas clave en el aviso, para saber con confianza que todas terminaron, cada una debe solicitarse desde JavaScript y se debe observar cada una de sus promesas finished para completarse. allSettled hace el trabajo 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 de manera confiable y asumir que el aviso completó su ciclo de vida. Por último, llamar a resolve() cumple con la promesa de alto nivel de los avisos, de modo que los desarrolladores puedan realizar una 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 se importen y usen.

Cómo usar el componente de aviso

Para usar el aviso o su experiencia del desarrollador, 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 desea realizar un trabajo de limpieza o lo que sea, después de que se haya mostrado 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 🙂

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

Remixes de la comunidad