Como criar um componente de aviso

Visão geral básica de como criar um componente de aviso adaptável e acessível.

Nesta postagem, quero compartilhar ideias sobre como criar um componente de aviso. Teste a demonstração.

Demonstração

Se preferir vídeo, aqui está uma versão do YouTube desta postagem:

Visão geral

Os avisos são mensagens curtas não interativas, passivas e assíncronas para os usuários. Em geral, eles são usados como um padrão de feedback de interface para informar o usuário sobre os resultados de uma ação.

Interações

Os avisos são diferentes de notificações, alertas e solicitações porque não são interativos e não podem ser dispensados ou persistir. As notificações são destinadas a informações mais importantes, mensagens síncronas que exigem interação ou mensagens no nível do sistema (e não da página). Toasts são mais passivos do que outras estratégias de notificação.

Marcação

O elemento <output> é uma boa escolha para o aviso, porque ele é anunciado aos leitores de tela. O HTML correto fornece uma base segura para aprimorarmos com JavaScript e CSS, e haverá muito JavaScript.

Um brinde

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

Ela pode ser mais inclusiva (link em inglês) adicionando role="status". Isso vai fornecer um substituto se o navegador não fornecer aos elementos <output> o papel implícito de acordo com a especificação.

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

Um contêiner de avisos

Mais de um aviso pode ser mostrado por vez. Para orquestrar vários avisos, é usado um contêiner. Esse contêiner também processa a posição dos avisos na tela.

<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>

Layouts

Escolhi fixar avisos no inset-block-end da janela de visualização e, se mais avisos forem adicionados, eles serão empilhados a partir da borda da tela.

Contêiner da GUI

O contêiner de avisos faz todo o trabalho de layout para apresentar avisos. O valor é fixed na janela de visualização e usa a propriedade lógica inset para especificar quais bordas serão fixadas, além de um pouco de padding da mesma borda block-end.

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

Captura de tela com o tamanho da caixa e o padding do DevTools sobrepostos em um elemento .gui-toast-container.

Além de se posicionar dentro da janela de visualização, o contêiner de avisos é um contêiner de grade que pode alinhar e distribuir avisos. Os itens são centralizados como um grupo com justify-content e centralizados individualmente em justify-items. Insira um pouco de gap para que os avisos não toquem.

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

Captura de tela com a sobreposição de grade CSS no grupo de avisos, desta vez
destacando o espaço e as lacunas entre os elementos filhos de notificação.

Aviso da GUI

Uma notificação de aviso individual tem padding, alguns cantos mais macios com border-radius e uma função min() para ajudar no dimensionamento de dispositivos móveis e computadores. O tamanho responsivo no CSS a seguir impede que os avisos se expandam mais de 90% da janela de visualização ou de 25ch.

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

Captura de tela de um único elemento .gui-toast, com o padding e o raio
da borda mostrados.

Estilos

Com o layout e o posicionamento definidos, adicione um CSS que ajude na adaptação às configurações e interações do usuário.

Contêiner de avisos

Os avisos não são interativos. Tocar ou deslizar neles não faz nada, mas atualmente consome eventos de ponteiro. Evite que os avisos roubem cliques com o CSS a seguir.

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

Aviso da GUI

Dê aos avisos um tema adaptável claro ou escuro com propriedades personalizadas, HSL e uma consulta de mídia de preferência.

.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%;
  }
}

Animação

Um novo aviso será mostrado com uma animação ao entrar na tela. Acomodação do movimento reduzido é feita definindo valores translate como 0 por padrão, mas atualizando o valor do movimento para um comprimento em uma consulta de mídia de preferência de movimento . Todos recebem algumas animações, mas apenas alguns usuários conseguem acompanhar a viagem por uma distância.

Estes são os frames-chave usados na animação de aviso. O CSS controlará a entrada, a espera e a saída do aviso, tudo em uma única animação.

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

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

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

Em seguida, o elemento de aviso configura as variáveis e orquestra os frames-chave.

.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

Com os estilos e o HTML acessível do leitor de tela pronto, o JavaScript é necessário para orquestrar a criação, adição e destruição de avisos com base nos eventos do usuário. A experiência do desenvolvedor do componente de aviso precisa ser mínima e fácil de começar, da seguinte forma:

import Toast from './toast.js'

Toast('My first toast')

Como criar o grupo de avisos e os avisos

Quando o módulo de aviso é carregado usando o JavaScript, ele precisa criar um contêiner de notificação e adicioná-lo à página. Escolhi adicionar o elemento antes de body. Isso impossibilita os problemas de empilhamento da z-index, já que o contêiner fica acima do contêiner em todos os elementos do corpo.

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

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

Captura de tela do grupo de avisos entre as tags &quot;head&quot; e &quot;body&quot;.

A função init() é chamada internamente para o módulo, armazenando o elemento como Toaster:

const Toaster = init()

A criação do elemento HTML de aviso é feita com a função createToast(). A função exige texto para o aviso, cria um elemento <output>, o adorna com algumas classes e atributos, define o texto e retorna o nó.

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

  return node
}

Como gerenciar um ou mais avisos

Agora, o JavaScript adiciona um contêiner ao documento para conter avisos e está pronto para adicionar avisos criados. A função addToast() orquestra o gerenciamento de um ou mais avisos. Primeiro, confira o número de avisos e se o movimento está adequado. Depois, use essas informações para anexar o aviso ou fazer uma animação bonita para que os outros avisos pareçam "abrir espaço" para o novo aviso.

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

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

Ao adicionar o primeiro aviso, Toaster.appendChild(toast) adiciona um aviso à página que aciona as animações CSS: animar, aguardar 3s e sair. O flipToast() é chamado quando há avisos, empregando uma técnica chamada FLIP de Paul Lewis (links em inglês). A ideia é calcular a diferença nas posições do contêiner, antes e depois da adição do novo aviso. Pense nisso como marcar onde a torradeira está agora, onde vai estar e, em seguida, anima de onde estava para onde 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',
  })
}

A grade CSS faz o levantamento do layout. Quando um novo aviso é adicionado, a grade o coloca no início e o espaça com os outros. Enquanto isso, uma animação da Web é usada para animar o contêiner da posição antiga.

Como reunir todo o JavaScript

Quando Toast('my first toast') é chamado, um aviso é criado, adicionado à página (talvez até mesmo o contêiner seja animado para acomodar o novo aviso), uma promessa é retornada e o aviso criado é observado para conclusão da animação CSS (as três animações de frames-chave) para resolução de promessas.

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() 
  })
}

Achei que a parte confusa desse código está na função Promise.allSettled() e no mapeamento toast.getAnimations(). Como usei várias animações de frame-chave para o aviso, para saber com segurança que todas elas foram concluídas, cada uma precisa ser solicitada ao JavaScript e cada uma das promessas finished observadas para conclusão. O allSettled faz isso funciona para nós, resolvendo-se como completo quando todas as promessas foram cumpridas. Usar await Promise.allSettled() significa que a próxima linha de código pode remover o elemento com confiança e presumir que o aviso concluiu o ciclo de vida. Por fim, chamar resolve() cumpre a promessa de aviso de alto nível para que os desenvolvedores possam limpar ou fazer outro trabalho depois que o aviso for mostrado.

export default Toast

Por fim, a função Toast é exportada do módulo, para que outros scripts importem e usem.

Como usar o componente Toast

Para usar o aviso, ou a experiência do desenvolvedor dele, é preciso importar a função Toast e chamá-la com uma string de mensagem.

import Toast from './toast.js'

Toast('Wizard Rose added to cart')

Se o desenvolvedor quiser fazer um trabalho de limpeza ou qualquer outra coisa, depois que o aviso for mostrado, ele pode usar async e await.

import Toast from './toast.js'

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

Conclusão

Agora que você sabe como eu fiz isso, o que você faria ‽ 🙂

Vamos diversificar nossas abordagens e aprender todas as maneiras de criar na Web. Crie uma demonstração, envie um tweet para mim e os adicionarei à seção de remixes da comunidade abaixo.

Remixes da comunidade