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.
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;
}
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;
}
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;
}
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
}
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
- @_developit com HTML/CSS/JS: demonstração e código
- Joost van der Schee com HTML/CSS/JS: demonstração e código