Uma visão geral básica de como criar mini e megamodais adaptáveis, responsivos e acessíveis com o elemento <dialog>
.
Nesta postagem, quero compartilhar minhas ideias sobre como criar mini e megamodais adaptáveis, responsivos e acessíveis com o elemento <dialog>
.
Experimente a demonstração e veja a
fonte.
Se preferir vídeo, aqui está uma versão do YouTube desta postagem:
Visão geral
O elemento
<dialog>
é ótimo para ações ou informações contextuais na página. Considere quando a experiência do usuário pode se beneficiar de uma ação na mesma página em vez de uma ação de várias páginas: talvez porque o formulário seja pequeno ou a única ação necessária do usuário seja confirmar ou cancelar.
O elemento <dialog>
se tornou estável em todos os navegadores:
Percebi que faltavam alguns itens no elemento. Por isso, neste desafio da GUI, adiciono os itens esperados da experiência do desenvolvedor: outros eventos, dispensa de luz, animações personalizadas e um mini e megatipo.
Marcação
Os elementos essenciais de um elemento <dialog>
são modestos. O elemento é
oculto automaticamente e tem estilos integrados para sobrepor o conteúdo.
<dialog>
…
</dialog>
Podemos melhorar essa linha de base.
Tradicionalmente, um elemento de caixa de diálogo compartilha muito com um modal e, muitas vezes, os nomes
são intercambiáveis. Tive a liberdade de usar o elemento de caixa de diálogo
para pop-ups pequenos (mini) e de página inteira (mega). Elas foram chamadas de
mega e mini, com ambas as caixas de diálogo ligeiramente adaptadas para diferentes casos de uso.
Adicionei um atributo modal-mode
para permitir que você especifique o tipo:
<dialog id="MegaDialog" modal-mode="mega"></dialog>
<dialog id="MiniDialog" modal-mode="mini"></dialog>
Nem sempre, mas geralmente elementos da caixa de diálogo são usados para coletar algumas
informações de interação. Os formulários dentro de elementos de caixa de diálogo são feitos para ficar
juntos.
É recomendável usar um elemento de formulário para unir o conteúdo da caixa de diálogo para que
o JavaScript possa acessar os dados inseridos pelo usuário. Além disso, os botões dentro
de um formulário que usam method="dialog"
podem fechar uma caixa de diálogo sem JavaScript e transmitir
dados.
<dialog id="MegaDialog" modal-mode="mega">
<form method="dialog">
…
<button value="cancel">Cancel</button>
<button value="confirm">Confirm</button>
</form>
</dialog>
Megacaixa de diálogo
Uma caixa de diálogo mega tem três elementos dentro do formulário:
<header>
,
<article>
e
<footer>
.
Eles servem como contêineres semânticos, bem como destinos de estilo para a apresentação da caixa de diálogo. O cabeçalho intitula o modal e oferece um botão "Fechar". O artigo é para entradas de formulário e informações. O rodapé contém uma
<menu>
de
botões de ação.
<dialog id="MegaDialog" modal-mode="mega">
<form method="dialog">
<header>
<h3>Dialog title</h3>
<button onclick="this.closest('dialog').close('close')"></button>
</header>
<article>...</article>
<footer>
<menu>
<button autofocus type="reset" onclick="this.closest('dialog').close('cancel')">Cancel</button>
<button type="submit" value="confirm">Confirm</button>
</menu>
</footer>
</form>
</dialog>
O primeiro botão de menu tem
autofocus
e um manipulador de eventos inline onclick
. O atributo autofocus
vai receber
foco quando a caixa de diálogo for aberta. A prática recomendada é colocar isso no
botão de cancelamento, não no de confirmação. Isso garante que a confirmação seja
deliberada e não acidental.
Minidiálogo
A minicaixa de diálogo é muito semelhante à caixa de diálogo mega. Só falta um
elemento <header>
. Isso permite que ele seja menor e mais inline.
<dialog id="MiniDialog" modal-mode="mini">
<form method="dialog">
<article>
<p>Are you sure you want to remove this user?</p>
</article>
<footer>
<menu>
<button autofocus type="reset" onclick="this.closest('dialog').close('cancel')">Cancel</button>
<button type="submit" value="confirm">Confirm</button>
</menu>
</footer>
</form>
</dialog>
O elemento da caixa de diálogo oferece uma base sólida para um elemento completo da janela de visualização, que pode coletar dados e interação do usuário. Esses elementos essenciais podem gerar algumas interações muito interessantes e eficientes no seu site ou app.
Acessibilidade
O elemento da caixa de diálogo tem uma acessibilidade integrada muito boa. Em vez de adicionar esses recursos como eu costumo fazer, muitos já estão lá.
Restaurando o foco
Como fizemos manualmente em Como criar um componente de navegação lateral, é importante que abrir e fechar algo corretamente coloque o foco nos botões de abertura e fechamento relevantes. Quando essa navegação lateral é aberta, o foco é colocado no botão "Fechar". Quando o botão "Fechar" é pressionado, o foco é restaurado para o botão que o abriu.
Este é o comportamento padrão integrado com o elemento da caixa de diálogo:
Infelizmente, se você quiser animar a caixa de diálogo, essa funcionalidade será perdida. Na seção JavaScript, vou restaurar essa funcionalidade.
Foco na armadilha
O elemento da caixa de diálogo gerencia
inert
para você no documento. Antes de inert
, o JavaScript era usado para observar se o foco
saía de um elemento, momento em que ele o interceptava e o colocava de volta.
Depois de inert
, qualquer parte do documento pode ser "congelada", de modo que não seja mais alvo de foco ou seja interativa com um mouse. Em vez de prender
o foco, o foco é guiado para a única parte interativa do documento.
Abrir e colocar um elemento em foco automaticamente
Por padrão, o elemento da caixa de diálogo atribui foco ao primeiro elemento focalizável
na marcação da caixa de diálogo. Se esse não for o melhor elemento para o usuário adotar como padrão,
use o atributo autofocus
. Conforme descrito anteriormente, é uma prática recomendada
colocar isso no botão "Cancelar", e não no de "Confirmar". Isso garante que a confirmação seja deliberada e não acidental.
Fechando com a tecla Esc
É importante fechar esse elemento potencialmente interruptivo. O elemento da caixa de diálogo vai processar a tecla de escape para você, liberando do trabalho de orquestração.
Estilos
Há um caminho fácil para estilizar o elemento da caixa de diálogo e um caminho difícil. O caminho mais fácil
é alcançado sem mudar a propriedade de exibição da caixa de diálogo e trabalhando
com as limitações. Eu escolho o caminho mais difícil para fornecer animações personalizadas para
abrir e fechar a caixa de diálogo, assumindo a propriedade display
e muito mais.
Estilizar com objetos abertos
Para acelerar as cores adaptáveis e a consistência geral do design, incluí descaradamente minha biblioteca de variáveis CSS Open Props. Além das variáveis sem custo financeiro fornecidas, também importo um arquivo normalize e alguns botões, ambos fornecidos como importações opcionais. Essas importações me ajudam a me concentrar em personalizar a caixa de diálogo e a demonstração sem precisar de muitos estilos para oferecer suporte e para que ela tenha uma boa aparência.
Como definir o estilo do elemento <dialog>
Como proprietário da propriedade de exibição
O comportamento padrão de mostrar e ocultar de um elemento da caixa de diálogo alterna a propriedade
de exibição de block
para none
. Infelizmente, isso significa que ela não pode ser
animada dentro e fora, somente para dentro. Quero animar para dentro e para fora, e o primeiro passo é definir minha própria propriedade display:
dialog {
display: grid;
}
Ao mudar e, portanto, assumir o valor da propriedade de exibição, como mostrado no snippet de CSS acima, uma quantidade considerável de estilos precisa ser gerenciada para facilitar a experiência do usuário adequada. Primeiro, o estado padrão de uma caixa de diálogo é fechada. É possível representar esse estado visualmente e impedir que a caixa de diálogo receba interações com estes estilos:
dialog:not([open]) {
pointer-events: none;
opacity: 0;
}
Agora a caixa de diálogo é invisível e não é possível interagir com ela quando não está aberta. Mais tarde,
vou adicionar um pouco de JavaScript para gerenciar o atributo inert
na caixa de diálogo, garantindo
que usuários de teclado e leitor de tela também não consigam acessar a caixa de diálogo oculta.
Usar um tema de cor adaptável na caixa de diálogo
Embora o color-scheme
ative seu documento em um tema de cor adaptável fornecido pelo navegador
às preferências claras e escuras do sistema, eu queria personalizar
o elemento da caixa de diálogo mais do que isso. O Open Props oferece algumas cores de
superfície (link em inglês) que se adaptam automaticamente às
preferências claras e escuras do sistema, de forma semelhante ao uso do color-scheme
. Eles
são ótimos para criar camadas em um design, e adoro usar cores para apoiar
visualmente essa aparência das superfícies das camadas. A cor do plano de fundo é
var(--surface-1)
. Para ficar na parte de cima dessa camada, use var(--surface-2)
:
dialog {
…
background: var(--surface-2);
color: var(--text-1);
}
@media (prefers-color-scheme: dark) {
dialog {
border-block-start: var(--border-size-1) solid var(--surface-3);
}
}
Cores mais adaptáveis serão adicionadas posteriormente para elementos filhos, como o cabeçalho e o rodapé. Considero-os extras para um elemento de diálogo, mas muito importantes para criar um design de diálogo atraente e bem projetado.
Dimensionamento responsivo da caixa de diálogo
O padrão da caixa de diálogo é delegar o tamanho ao conteúdo, o que geralmente é
ótimo. Meu objetivo é restringir
max-inline-size
a um tamanho legível (--size-content-3
= 60ch
) ou a 90% da largura da janela de visualização. Isso
garante que a caixa de diálogo não fique de uma borda a outra em um dispositivo móvel e não seja tão
ampla em uma tela de computador que seja difícil de ler. Em seguida, adiciono um
max-block-size
para que a caixa de diálogo não exceda a altura da página. Isso também significa que precisamos
especificar onde está a área de rolagem da caixa de diálogo, caso ela seja um elemento
alto.
dialog {
…
max-inline-size: min(90vw, var(--size-content-3));
max-block-size: min(80vh, 100%);
max-block-size: min(80dvb, 100%);
overflow: hidden;
}
Viu como tenho max-block-size
duas vezes? O primeiro usa 80vh
, uma unidade física
de janela de visualização. O que eu realmente quero é manter a caixa de diálogo no fluxo relativo,
para usuários internacionais. Então, uso a unidade dvb
lógica, mais recente e apenas
com suporte parcial na segunda declaração para quando ela se tornar mais estável.
Megaposicionamento de diálogo
Para ajudar a posicionar um elemento de caixa de diálogo, vale a pena dividir as duas partes dele: o pano de fundo de tela cheia e o contêiner da caixa de diálogo. O pano de fundo precisa cobrir tudo, fornecendo um efeito de sombra para indicar que a caixa de diálogo está à frente e que o conteúdo por trás está inacessível. O contêiner da caixa de diálogo pode ser centralizado sobre esse pano de fundo e assumir a forma necessária para o conteúdo.
Os estilos abaixo corrigem o elemento da caixa de diálogo na janela, estendendo-o até cada
canto, e usam margin: auto
para centralizar o conteúdo:
dialog {
…
margin: auto;
padding: 0;
position: fixed;
inset: 0;
z-index: var(--layer-important);
}
Estilos de megacaixa de diálogo para dispositivos móveis
Em janelas de visualização pequenas, o estilo desse mega modal de página inteira é um pouco diferente. Definimos
a margem inferior como 0
, o que leva o conteúdo da caixa de diálogo para a parte de baixo da
janela de visualização. Com alguns ajustes de estilo, posso transformar a caixa de diálogo em uma
planilha de ações, mais próxima das miniaturas do usuário:
@media (max-width: 768px) {
dialog[modal-mode="mega"] {
margin-block-end: 0;
border-end-end-radius: 0;
border-end-start-radius: 0;
}
}
Posicionamento da minicaixa de diálogo
Ao usar uma janela de visualização maior, como em um computador desktop, optei por posicionar as minicaixas de diálogo sobre o elemento que as chamou. Para fazer isso, preciso do JavaScript. Você pode encontrar a técnica que uso aqui, mas acho que ela está além do escopo deste artigo. Sem o JavaScript, a mini caixa de diálogo aparece no centro da tela, como a caixa de diálogo mega.
Dê destaque
Por fim, dê um toque especial à caixa de diálogo para que ela pareça uma superfície macia bem acima da página. A suavidade é alcançada arredondando os cantos da caixa de diálogo. A profundidade é alcançada com um dos acessórios de sombra cuidadosamente criados da Open Props:
dialog {
…
border-radius: var(--radius-3);
box-shadow: var(--shadow-6);
}
Personalizar o pseudoelemento do pano de fundo
Escolhi trabalhar levemente com o pano de fundo, adicionando apenas um efeito de desfoque com
backdrop-filter
à caixa de diálogo mega:
dialog[modal-mode="mega"]::backdrop {
backdrop-filter: blur(25px);
}
Também optei por colocar uma transição em backdrop-filter
, esperando que os navegadores
permitam a transição do elemento do pano de fundo no futuro:
dialog::backdrop {
transition: backdrop-filter .5s ease;
}
Outros recursos de estilo
Eu chamo essa seção de "extras" porque ela tem mais a ver com a demonstração do elemento da caixa de diálogo do que com o elemento da caixa de diálogo em geral.
Contenção de rolagem
Quando a caixa de diálogo é mostrada, o usuário ainda pode rolar a página por trás dela, o que eu não quero:
Normalmente,
overscroll-behavior
seria minha solução comum, mas de acordo com a
especificação,
ele não afeta a caixa de diálogo porque não é uma porta de rolagem, ou seja, não é
um botão de rolagem, portanto, não há nada a evitar. Eu poderia usar o JavaScript para acompanhar
os novos eventos deste guia, como "fechados" e "abertos", e ativar
overflow: hidden
no documento ou esperar que :has()
fique estável em
todos os navegadores:
html:has(dialog[open][modal-mode="mega"]) {
overflow: hidden;
}
Agora, quando uma mega caixa de diálogo for aberta, o documento html terá overflow: hidden
.
O layout <form>
Além de ser um elemento muito importante para coletar as informações
de interação do usuário, eu o uso aqui para posicionar os elementos de cabeçalho, rodapé e
artigo. Com esse layout, pretendemos articular o elemento filho do artigo como uma
área rolável. Isso pode ser feito com
grid-template-rows
.
O elemento do artigo recebe 1fr
, e o formulário em si tem a mesma altura máxima
que o elemento da caixa de diálogo. Definir essa altura e o tamanho da linha firmes é o que
permite que o elemento do artigo seja restrito e role quando ultrapassar:
dialog > form {
display: grid;
grid-template-rows: auto 1fr auto;
align-items: start;
max-block-size: 80vh;
max-block-size: 80dvb;
}
Estilizar a caixa de diálogo <header>
A função desse elemento é fornecer um título para o conteúdo da caixa de diálogo e oferecer um botão "Fechar" fácil de encontrar. Ele também recebe uma cor de superfície para fazer com que pareça estar por trás do conteúdo do artigo da caixa de diálogo. Esses requisitos resultam em um contêiner flexbox, itens alinhados verticalmente que são espaçados nas bordas e um preenchimento e lacunas para dar espaço aos botões "Título" e "Fechar":
dialog > form > header {
display: flex;
gap: var(--size-3);
justify-content: space-between;
align-items: flex-start;
background: var(--surface-2);
padding-block: var(--size-3);
padding-inline: var(--size-5);
}
@media (prefers-color-scheme: dark) {
dialog > form > header {
background: var(--surface-1);
}
}
Aplicar estilo ao botão "Fechar" do cabeçalho
Como a demonstração usa os botões "Open Props", o botão "Close" é personalizado para um botão redondo, centralizado em um ícone, desta forma:
dialog > form > header > button {
border-radius: var(--radius-round);
padding: .75ch;
aspect-ratio: 1;
flex-shrink: 0;
place-items: center;
stroke: currentColor;
stroke-width: 3px;
}
Estilizar a caixa de diálogo <article>
O elemento do artigo tem uma função especial nessa caixa de diálogo: é um espaço que pode ser rolado no caso de uma caixa de diálogo alta ou longa.
Para fazer isso, o elemento do formulário pai estabeleceu alguns limites máximos que fornecem restrições para esse elemento do artigo alcançar se ficar muito alto. Configure overflow-y: auto
para que as barras de rolagem sejam exibidas apenas quando necessário,
contendo a rolagem dentro delas com overscroll-behavior: contain
e o restante
será estilos de apresentação personalizados:
dialog > form > article {
overflow-y: auto;
max-block-size: 100%; /* safari */
overscroll-behavior-y: contain;
display: grid;
justify-items: flex-start;
gap: var(--size-3);
box-shadow: var(--shadow-2);
z-index: var(--layer-1);
padding-inline: var(--size-5);
padding-block: var(--size-3);
}
@media (prefers-color-scheme: light) {
dialog > form > article {
background: var(--surface-1);
}
}
Estilizar a caixa de diálogo <footer>
A função do rodapé é conter menus de botões de ação. O flexbox é usado para alinhar o conteúdo ao final do eixo inline do rodapé e, em seguida, dar algum espaçamento para dar algum espaço aos botões.
dialog > form > footer {
background: var(--surface-2);
display: flex;
flex-wrap: wrap;
gap: var(--size-3);
justify-content: space-between;
align-items: flex-start;
padding-inline: var(--size-5);
padding-block: var(--size-3);
}
@media (prefers-color-scheme: dark) {
dialog > form > footer {
background: var(--surface-1);
}
}
Definir o estilo do menu do rodapé da caixa de diálogo
O elemento menu
é usado para conter os botões de ação da caixa de diálogo. Ele usa um layout flexbox
com gap
para fornecer espaço entre os botões. Os elementos de menu
têm padding, como <ul>
. Também removo esse estilo, já que não preciso dele.
dialog > form > footer > menu {
display: flex;
flex-wrap: wrap;
gap: var(--size-3);
padding-inline-start: 0;
}
dialog > form > footer > menu:only-child {
margin-inline-start: auto;
}
Animação
Elementos de caixa de diálogo são frequentemente animados porque entram e saem da janela. Oferecer aos diálogos algum movimento de apoio para essa entrada e saída ajuda os usuários a se orientarem no fluxo.
Normalmente, o elemento da caixa de diálogo só pode ser animado para dentro, não para fora. Isso ocorre porque o navegador alterna a propriedade display
no elemento. Anteriormente, o guia
definiu a exibição como grade e nunca a definiu como "nenhuma". Isso desbloqueia a capacidade
de animar para dentro e para fora.
O Open Props vem com muitas animações de frames-chave para uso, o que facilita a orquestração e a leitura. Estas são as metas de animação e a abordagem em camadas que segui:
- O movimento reduzido é a transição padrão, uma simples opacidade que esmaece.
- Se o movimento estiver adequado, animações de deslize e escala serão adicionadas.
- O layout responsivo de dispositivos móveis para a caixa de diálogo mega é ajustado para sair.
Uma transição padrão segura e significativa
Embora o Open Props venha com frames-chave para exibição e exibição, prefiro essa
abordagem em camadas de transições como padrão com animações de frame-chave como
os possíveis upgrades. Anteriormente, já estilizamos a visibilidade da caixa de diálogo com
opacidade, orquestrando 1
ou 0
, dependendo do atributo [open]
. Para
fazer a transição entre 0% e 100%, informe ao navegador a duração e o tipo de
easing desejado:
dialog {
transition: opacity .5s var(--ease-3);
}
Adicionar movimento à transição
Se o usuário aceitar o movimento, as caixas de diálogo mega e mini precisam deslizar
para cima como a entrada e escalonar horizontalmente para a saída. É possível fazer isso com a
consulta de mídia prefers-reduced-motion
e alguns Open Props:
@media (prefers-reduced-motion: no-preference) {
dialog {
animation: var(--animation-scale-down) forwards;
animation-timing-function: var(--ease-squish-3);
}
dialog[open] {
animation: var(--animation-slide-in-up) forwards;
}
}
Adaptação da animação de saída para dispositivos móveis
No início da seção de estilo, o estilo de caixa de diálogo mega é adaptado para dispositivos móveis para que sejam mais como uma folha de ações, como se um pequeno pedaço de papel tivesse escapado da parte inferior da tela e ainda estivesse anexado à parte de baixo. A animação de saída de escalonamento horizontal não se encaixa bem nesse novo design, e podemos adaptá-la com algumas consultas de mídia e alguns Open Props:
@media (prefers-reduced-motion: no-preference) and @media (max-width: 768px) {
dialog[modal-mode="mega"] {
animation: var(--animation-slide-out-down) forwards;
animation-timing-function: var(--ease-squish-2);
}
}
JavaScript
Há algumas coisas a serem adicionadas com JavaScript:
// dialog.js
export default async function (dialog) {
// add light dismiss
// add closing and closed events
// add opening and opened events
// add removed event
// removing loading attribute
}
Essas adições são resultado do desejo de dispensar a luz (clicando no plano de fundo da caixa de diálogo), da animação e de alguns outros eventos para conseguir os dados do formulário de forma mais rápida.
Adicionando iluminação
Essa tarefa é simples e uma ótima adição a um elemento de diálogo que não está
sendo animado. A interação ocorre observando os cliques no elemento da caixa de diálogo
e aproveitando a navegação de
eventos
para avaliar o que foi clicado, e só
close()
se for o elemento superior:
export default async function (dialog) {
dialog.addEventListener('click', lightDismiss)
}
const lightDismiss = ({target:dialog}) => {
if (dialog.nodeName === 'DIALOG')
dialog.close('dismiss')
}
Observe dialog.close('dismiss')
. O evento é chamado e uma string é fornecida.
Essa string pode ser recuperada por outro JavaScript para ter insights sobre como a
caixa de diálogo foi fechada. Você verá que também forneci strings de fechamento toda vez que chamo
a função em vários botões para fornecer contexto ao meu aplicativo sobre
a interação do usuário.
Adicionar eventos de fechamento e fechados
O elemento da caixa de diálogo vem com um evento de fechamento: ele é emitido imediatamente quando a
função close()
da caixa de diálogo é chamada. Como estamos animando esse elemento, é
bom ter eventos para antes e depois da animação, para que uma mudança capture os
dados ou redefina o formulário da caixa de diálogo. Ele é usado aqui para gerenciar a adição do
atributo inert
na caixa de diálogo fechada e, na demonstração, para modificar
a lista de avatares caso o usuário tenha enviado uma nova imagem.
Para isso, crie dois novos eventos chamados closing
e closed
. Em seguida,
detecte o evento de fechamento integrado na caixa de diálogo. Agora, defina a caixa de diálogo como
inert
e envie o evento closing
. A próxima tarefa é aguardar que as
animações e transições terminem de ser executadas na caixa de diálogo e enviar o
evento closed
.
const dialogClosingEvent = new Event('closing')
const dialogClosedEvent = new Event('closed')
export default async function (dialog) {
…
dialog.addEventListener('close', dialogClose)
}
const dialogClose = async ({target:dialog}) => {
dialog.setAttribute('inert', '')
dialog.dispatchEvent(dialogClosingEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogClosedEvent)
}
const animationsComplete = element =>
Promise.allSettled(
element.getAnimations().map(animation =>
animation.finished))
A função animationsComplete
, que também é usada em Como criar um componente
de aviso, retorna uma promessa com base na
conclusão das promessas de animação e transição. É por isso que dialogClose
é uma função
assíncrona.
Ela pode
await
a promessa retornada e avançar com confiança para o evento fechado.
Adicionar eventos abertos e abertos
Esses eventos não são tão fáceis de adicionar, já que o elemento da caixa de diálogo integrada não fornece um evento de abertura como acontece com o fechamento. Eu uso um MutationObserver para fornecer insights sobre as mudanças de atributos da caixa de diálogo. Neste observador, vou observar as mudanças no atributo aberto e gerenciar os eventos personalizados adequadamente.
Assim como iniciamos os eventos de fechamento e fechamento, crie dois novos eventos
chamados opening
e opened
. Onde ouvimos o evento de fechamento da
caixa de diálogo, use um observador de mutação criado para observar os atributos
da caixa de diálogo.
…
const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent = new Event('opened')
export default async function (dialog) {
…
dialogAttrObserver.observe(dialog, {
attributes: true,
})
}
const dialogAttrObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(async mutation => {
if (mutation.attributeName === 'open') {
const dialog = mutation.target
const isOpen = dialog.hasAttribute('open')
if (!isOpen) return
dialog.removeAttribute('inert')
// set focus
const focusTarget = dialog.querySelector('[autofocus]')
focusTarget
? focusTarget.focus()
: dialog.querySelector('button').focus()
dialog.dispatchEvent(dialogOpeningEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogOpenedEvent)
}
})
})
A função de callback do observador de mutações será chamada quando os atributos da caixa de diálogo
forem alterados, fornecendo a lista de alterações como uma matriz. Itere as
mudanças de atributo, procurando que attributeName
esteja aberto. Em seguida, confira
se o elemento tem o atributo ou não: isso informa se a caixa de diálogo
foi aberta ou não. Se ele tiver sido aberto, remova o atributo inert
e defina o foco
para um elemento que solicite
autofocus
ou o primeiro elemento button
encontrado na caixa de diálogo. Por último, de maneira semelhante ao evento de fechamento
e fechado, envie o evento de abertura imediatamente, aguarde a conclusão das animações
e envie o evento aberto.
Como adicionar um evento removido
Em aplicativos de página única, as caixas de diálogo geralmente são adicionadas e removidas com base nas rotas ou em outras necessidades e estados do app. Pode ser útil limpar eventos ou dados quando uma caixa de diálogo é removida.
Você pode conseguir isso com outro observador de mutações. Desta vez, em vez de observar atributos em um elemento de caixa de diálogo, observaremos os filhos do elemento de corpo e observaremos se elementos de caixa de diálogo são removidos.
…
const dialogRemovedEvent = new Event('removed')
export default async function (dialog) {
…
dialogDeleteObserver.observe(document.body, {
attributes: false,
subtree: false,
childList: true,
})
}
const dialogDeleteObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(mutation => {
mutation.removedNodes.forEach(removedNode => {
if (removedNode.nodeName === 'DIALOG') {
removedNode.removeEventListener('click', lightDismiss)
removedNode.removeEventListener('close', dialogClose)
removedNode.dispatchEvent(dialogRemovedEvent)
}
})
})
})
O callback do observador de mutações é chamado sempre que filhos são adicionados ou removidos
do corpo do documento. As mutações específicas que estão sendo monitoradas são para
removedNodes
que tem o
nodeName
de
uma caixa de diálogo. Se uma caixa de diálogo for removida, os eventos de clique e de fechamento serão removidos para
liberar memória, e o evento personalizado removido será enviado.
Como remover o atributo de carregamento
Para evitar que a animação da caixa de diálogo mostre a animação de saída quando adicionada à página ou no carregamento da página, um atributo de carregamento foi adicionado à caixa de diálogo. O script a seguir aguarda a execução das animações da caixa de diálogo e remove o atributo. Agora a caixa de diálogo pode ser animada para dentro e fora da caixa de diálogo, e nós ocultamos uma animação que poderia causar distração.
export default async function (dialog) {
…
await animationsComplete(dialog)
dialog.removeAttribute('loading')
}
Saiba mais sobre o problema de impedir animações de frame-chave no carregamento da página neste link.
Tudo em um só lugar
Confira dialog.js
na íntegra, agora que explicamos cada seção
individualmente:
// custom events to be added to <dialog>
const dialogClosingEvent = new Event('closing')
const dialogClosedEvent = new Event('closed')
const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent = new Event('opened')
const dialogRemovedEvent = new Event('removed')
// track opening
const dialogAttrObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(async mutation => {
if (mutation.attributeName === 'open') {
const dialog = mutation.target
const isOpen = dialog.hasAttribute('open')
if (!isOpen) return
dialog.removeAttribute('inert')
// set focus
const focusTarget = dialog.querySelector('[autofocus]')
focusTarget
? focusTarget.focus()
: dialog.querySelector('button').focus()
dialog.dispatchEvent(dialogOpeningEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogOpenedEvent)
}
})
})
// track deletion
const dialogDeleteObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(mutation => {
mutation.removedNodes.forEach(removedNode => {
if (removedNode.nodeName === 'DIALOG') {
removedNode.removeEventListener('click', lightDismiss)
removedNode.removeEventListener('close', dialogClose)
removedNode.dispatchEvent(dialogRemovedEvent)
}
})
})
})
// wait for all dialog animations to complete their promises
const animationsComplete = element =>
Promise.allSettled(
element.getAnimations().map(animation =>
animation.finished))
// click outside the dialog handler
const lightDismiss = ({target:dialog}) => {
if (dialog.nodeName === 'DIALOG')
dialog.close('dismiss')
}
const dialogClose = async ({target:dialog}) => {
dialog.setAttribute('inert', '')
dialog.dispatchEvent(dialogClosingEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogClosedEvent)
}
// page load dialogs setup
export default async function (dialog) {
dialog.addEventListener('click', lightDismiss)
dialog.addEventListener('close', dialogClose)
dialogAttrObserver.observe(dialog, {
attributes: true,
})
dialogDeleteObserver.observe(document.body, {
attributes: false,
subtree: false,
childList: true,
})
// remove loading attribute
// prevent page load @keyframes playing
await animationsComplete(dialog)
dialog.removeAttribute('loading')
}
Como usar o módulo dialog.js
A função exportada do módulo espera ser chamada e transmitir um elemento de caixa de diálogo que quer adicionar esses novos eventos e funcionalidades:
import GuiDialog from './dialog.js'
const MegaDialog = document.querySelector('#MegaDialog')
const MiniDialog = document.querySelector('#MiniDialog')
GuiDialog(MegaDialog)
GuiDialog(MiniDialog)
Assim, as duas caixas de diálogo foram atualizadas com dispensa de luz, correções de carregamento de animação e mais eventos para trabalhar.
Como ouvir os novos eventos personalizados
Cada elemento atualizado da caixa de diálogo agora pode detectar cinco novos eventos, como este:
MegaDialog.addEventListener('closing', dialogClosing)
MegaDialog.addEventListener('closed', dialogClosed)
MegaDialog.addEventListener('opening', dialogOpening)
MegaDialog.addEventListener('opened', dialogOpened)
MegaDialog.addEventListener('removed', dialogRemoved)
Veja dois exemplos de processamento desses eventos:
const dialogOpening = ({target:dialog}) => {
console.log('Dialog opening', dialog)
}
const dialogClosed = ({target:dialog}) => {
console.log('Dialog closed', dialog)
console.info('Dialog user action:', dialog.returnValue)
if (dialog.returnValue === 'confirm') {
// do stuff with the form values
const dialogFormData = new FormData(dialog.querySelector('form'))
console.info('Dialog form data', Object.fromEntries(dialogFormData.entries()))
// then reset the form
dialog.querySelector('form')?.reset()
}
}
Na demonstração que criei com o elemento da caixa de diálogo, uso esse evento fechado e os dados do formulário para adicionar um novo elemento do avatar à lista. O momento é adequado, porque a caixa de diálogo concluiu a animação de saída e alguns scripts são animados no novo avatar. Graças aos novos eventos, a orquestração da experiência do usuário pode ser mais fácil.
Observe dialog.returnValue
: ele contém a string de fechamento transmitida quando o
evento close()
da caixa de diálogo é chamado. É fundamental no evento dialogClosed
saber se a caixa de diálogo foi fechada, cancelada ou confirmada. Se ele for confirmado, o
script coletará os valores do formulário e o redefinirá. A redefinição é útil para que,
quando a caixa de diálogo for mostrada novamente, fique em branco e pronta para um novo envio.
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
- @GrimLink com uma caixa de diálogo 3 em 1.
- @mikemai2awesome com um bom
remix que não muda a
propriedade
display
. - @geoffrich_ com um acabamento Svelte e Svelte FLIP.
Recursos
- Código-fonte no GitHub
- Avatares do Doodle