Uma visão geral básica de como criar modais pequenos e grandes adaptáveis a cores, responsivos e acessíveis com o elemento <dialog>
.
Nesta postagem, quero compartilhar minhas ideias sobre como criar mini e mega modais adaptáveis a cores, responsivos e acessíveis com o elemento <dialog>
.
Teste a demonstração e veja a
fonte.
Se preferir vídeo, confira uma versão desta postagem no YouTube:
Visão geral
O elemento
<dialog>
é ótimo para informações ou açõ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 em 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>
recentemente se tornou estável em todos os navegadores:
Descobri que o elemento estava faltando algumas coisas. Por isso, neste desafio de GUI, adicionei os itens de experiência do desenvolvedor que eu esperava: eventos adicionais, dispensa leve, animações personalizadas e um tipo mini e mega.
Marcação
Os elementos essenciais de um elemento <dialog>
são modestos. O elemento será ocultado automaticamente e terá estilos integrados para sobrepor seu conteúdo.
<dialog>
…
</dialog>
Podemos melhorar esse valor de referência.
Tradicionalmente, um elemento de caixa de diálogo compartilha muito com um modal, e muitas vezes os nomes são intercambiáveis. Usei o elemento de caixa de diálogo para
pop-ups pequenos (mini) e caixas de diálogo de página inteira (mega). Eu os chamei de mega e mini, com os dois diálogos ligeiramente adaptados 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 os elementos de caixa de diálogo são usados para coletar algumas informações de interação. Os formulários dentro dos elementos de caixa de diálogo são feitos para
funcionar juntos.
É uma boa ideia ter um elemento de formulário que envolva 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 usando 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>
Caixa de diálogo grande
Um mega diálogo tem três elementos dentro do formulário:
<header>
,
<article>
,
e
<footer>
.
Eles servem como contêineres semânticos e como destinos de estilo para a
apresentação da caixa de diálogo. O cabeçalho dá um título ao modal e oferece um botão de
fechar. O artigo é sobre entradas e informações de formulários. O rodapé contém um
<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
o foco quando a caixa de diálogo for aberta, e acho que é uma prática recomendada colocar isso no
botão "Cancelar", não no botão "Confirmar". Isso garante que a confirmação seja
deliberada e não acidental.
Minicaixa de diálogo
A caixa de diálogo mini é muito parecida com a mega, só que sem 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 de caixa de diálogo oferece uma base sólida para um elemento de janela de visualização completa que pode coletar dados e interação do usuário. Esses elementos essenciais podem gerar interações muito interessantes e eficientes no seu site ou app.
Acessibilidade
O elemento de caixa de diálogo tem acessibilidade integrada muito boa. Em vez de adicionar esses recursos como de costume, muitos já estão lá.
Restaurar o foco
Assim como fizemos manualmente em Como criar um componente de barra lateral, é importante que a abertura e o fechamento adequados de algo foquem nos botões relevantes de abrir e fechar. Quando a barra lateral é aberta, o foco é colocado no botão de fechar. Quando o botão de fechar é pressionado, o foco é restaurado para o botão que o abriu.
Com o elemento de caixa de diálogo, esse é o comportamento padrão integrado:
Infelizmente, se você quiser animar a caixa de diálogo para dentro e para fora, essa funcionalidade será perdida. Na seção JavaScript, vou restaurar essa funcionalidade.
Bloqueio de foco
O elemento de caixa de diálogo gerencia
inert
para você no documento. Antes do inert
, o JavaScript era usado para monitorar o foco
deixando um elemento, momento em que ele intercepta e o coloca de volta.
Depois de inert
, qualquer parte do documento pode ser "congelada" de modo que não seja mais um alvo de foco ou interativa com um mouse. Em vez de prender o foco, ele é direcionado para a única parte interativa do documento.
Abrir e focar automaticamente um elemento
Por padrão, o elemento de 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 usar por padrão,
use o atributo autofocus
. Como descrito anteriormente, considero uma prática recomendada
colocar isso no botão "Cancelar" e não no botão "Confirmar". Isso garante que a confirmação seja proposital e não acidental.
Fechar com a tecla Esc
É importante facilitar o fechamento desse elemento potencialmente interruptivo. Felizmente, o elemento de caixa de diálogo processa a tecla "Esc" para você, liberando você do fardo da orquestração.
Estilos
Há um caminho fácil e um difícil para estilizar o elemento de caixa de diálogo. O caminho
fácil é alcançado não mudando a propriedade de exibição da caixa de diálogo e trabalhando
com as limitações dela. Eu sigo o caminho difícil para fornecer animações personalizadas para
abrir e fechar a caixa de diálogo, assumindo a propriedade display
e muito mais.
Estilo com Open Props
Para acelerar as cores adaptáveis e a consistência geral do design, usei sem pudor a 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, que o Open Props oferece como importações opcionais. Essas importações me ajudam a focar na personalização da caixa de diálogo e da demonstração sem precisar de muitos estilos para oferecer suporte e fazer com que pareça bom.
Estilizar o elemento <dialog>
Ser proprietário da propriedade de exibição
O comportamento padrão de mostrar e ocultar de um elemento de caixa de diálogo alterna a propriedade
de exibição de block
para none
. Infelizmente, isso significa que ele não pode ser animado
para dentro e para fora, apenas para dentro. Quero animar a entrada e a saída. A primeira etapa é definir minha própria propriedade display:
dialog {
display: grid;
}
Ao mudar, e portanto possuir, o valor da propriedade de exibição, conforme mostrado no snippet de CSS acima, uma quantidade considerável de estilos precisa ser gerenciada para facilitar a experiência adequada do usuário. Primeiro, o estado padrão de uma caixa de diálogo é fechado. Você pode representar esse estado visualmente e impedir que a caixa de diálogo receba interações com os seguintes estilos:
dialog:not([open]) {
pointer-events: none;
opacity: 0;
}
Agora, a caixa de diálogo fica invisível e não pode ser usada 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 leitores de tela também não consigam acessar a caixa de diálogo oculta.
Como dar à caixa de diálogo um tema de cores adaptativo
Embora color-scheme
ative um tema de cores adaptável fornecido pelo navegador para preferências de sistema claras e escuras, eu queria personalizar o elemento de caixa de diálogo mais do que isso. O Open Props oferece algumas cores de superfície que se adaptam automaticamente às preferências do sistema claro e escuro, de maneira semelhante ao uso do color-scheme
. Eles são ótimos para criar camadas em um design, e eu adoro usar cores para ajudar a apoiar visualmente essa aparência de superfícies em camadas. A cor de fundo é
var(--surface-1)
. Para ficar em 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 depois para elementos filhos, como o cabeçalho e o rodapé. Considero esses elementos extras para um elemento de caixa de diálogo, mas muito importantes para criar um design de caixa de diálogo atraente e bem projetado.
Dimensionamento responsivo da caixa de diálogo
Por padrão, a caixa de diálogo delega o tamanho ao conteúdo, o que geralmente é ótimo. Meu objetivo aqui é restringir o
max-inline-size
a um tamanho legível (--size-content-3
= 60ch
) ou 90% da largura da janela de visualização. Isso garante que a caixa de diálogo não vá de ponta a ponta em um dispositivo móvel e não fique tão larga em uma tela de computador que dificulte a leitura. 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 fica a área rolável 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;
}
Percebe como eu tenho max-block-size
duas vezes? O primeiro usa 80vh
, uma unidade de viewport física. O que eu realmente quero é manter a caixa de diálogo dentro do fluxo relativo para usuários internacionais. Por isso, uso a unidade lógica, mais recente e parcialmente compatível dvb
na segunda declaração para quando ela se tornar mais estável.
Posicionamento da caixa de diálogo grande
Para ajudar a posicionar um elemento de caixa de diálogo, vale a pena dividir as duas partes dele: o plano de fundo em tela cheia e o contêiner da caixa de diálogo. O pano de fundo precisa cobrir tudo, fornecendo um efeito de sombra para ajudar a mostrar que a caixa de diálogo está na frente e o conteúdo atrás está inacessível. O contêiner de diálogo pode se centralizar sobre esse plano de fundo e assumir qualquer forma que o conteúdo exija.
Os estilos a seguir fixam o elemento de caixa de diálogo na janela, esticando-o para 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 mega caixa de diálogo para dispositivos móveis
Em janelas de visualização pequenas, eu estilizo esse mega modal de página inteira de maneira um pouco diferente. Defini a margem inferior como 0
, o que traz 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
folha de ações, mais próxima dos polegares 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 caixa de diálogo pequena
Ao usar uma janela de visualização maior, como em um computador desktop, posicionei as mini caixas de diálogo sobre o elemento que as chamou. Para fazer isso, preciso de JavaScript. Você pode encontrar a técnica que uso aqui, mas acho que ela não faz parte do escopo deste artigo. Sem o JavaScript, a minicaixa de diálogo aparece no centro da tela, assim como a megacaixa de diálogo.
Cause impacto
Por fim, adicione um toque especial à caixa de diálogo para que ela pareça uma superfície macia muito acima da página. A suavidade é alcançada arredondando os cantos da caixa de diálogo. A profundidade é alcançada com uma das propriedades de sombra cuidadosamente criadas do Open Props:
dialog {
…
border-radius: var(--radius-3);
box-shadow: var(--shadow-6);
}
Como personalizar o pseudoelemento de pano de fundo
Optei por trabalhar de forma muito leve com o plano de fundo, adicionando apenas um efeito de desfoque com
backdrop-filter
ao mega diálogo:
dialog[modal-mode="mega"]::backdrop {
backdrop-filter: blur(25px);
}
Também escolhi colocar uma transição em backdrop-filter
, na esperança de que os navegadores
permitam a transição do elemento de plano de fundo no futuro:
dialog::backdrop {
transition: backdrop-filter .5s ease;
}
Extras de estilo
Chamo esta seção de "extras" porque ela tem mais a ver com minha demonstração do elemento de caixa de diálogo do que com o elemento de caixa de diálogo em geral.
Restriçã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 usual, mas de acordo com a especificação, não tem efeito na caixa de diálogo porque não é uma porta de rolagem, ou seja, não é um scroller, então não há nada a impedir. Posso usar JavaScript para observar os novos eventos deste guia, como "closed" e "opened", e alternar 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 caixa de diálogo grande está aberta, o documento HTML tem 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 apresentar os elementos de cabeçalho, rodapé e artigo. Com esse layout, pretendo articular o filho do artigo como uma
área rolável. Faço isso com
grid-template-rows
.
O elemento de artigo recebe 1fr
, e o formulário tem a mesma altura máxima do elemento de caixa de diálogo. Definir essa altura e esse tamanho de linha fixos é o que permite que o elemento do artigo seja restrito e role quando transborda:
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 parecer estar atrás do conteúdo do artigo da caixa de diálogo. Esses requisitos levam a um contêiner flexbox, itens alinhados verticalmente e espaçados até as bordas, além de algum padding e lacunas para dar espaço ao título e aos botões de 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);
}
}
Estilizar o botão de fechar do cabeçalho
Como a demonstração está usando os botões Open Props, o botão de fechar é personalizado em um botão redondo centralizado com ícone, assim:
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 "article" tem uma função especial nessa caixa de diálogo: é um espaço destinado a ser rolado no caso de uma caixa de diálogo alta ou longa.
Para isso, o elemento de formulário pai estabeleceu alguns máximos para si mesmo, que fornecem restrições para que o elemento de artigo alcance se ficar muito alto. Defina overflow-y: auto
para que as barras de rolagem só apareçam quando necessário, contenha a rolagem dentro dele 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, adicionar um espaçamento para dar 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);
}
}
Estilizar o menu de 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 de quebra
com gap
para fornecer espaço entre os botões. Os elementos de menu
têm padding, como um <ul>
. Também removo esse estilo porque 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
Os elementos de caixa de diálogo geralmente são animados porque entram e saem da janela. Dar aos diálogos um movimento de suporte para essa entrada e saída ajuda os usuários a se orientarem no fluxo.
Normalmente, o elemento de 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. Antes, o guia
definiria a exibição como grade e nunca como "nenhuma". Isso permite animar a entrada e a saída.
O Open Props vem com muitas animações de keyframe para uso, o que facilita a orquestração e a torna legível. Confira as metas de animação e a abordagem em camadas que usei:
- O movimento reduzido é a transição padrão, um simples fade-in e fade-out de opacidade.
- Se o movimento estiver ok, as animações de deslizar e dimensionar serão adicionadas.
- O layout responsivo para dispositivos móveis da caixa de diálogo grande é ajustado para deslizar para fora.
Uma transição padrão segura e significativa
Embora o Open Props venha com frames-chave para aparecer e desaparecer, prefiro essa abordagem em camadas de transições como padrão, com animações de frames-chave como possíveis upgrades. Antes, já definimos o estilo da 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%, diga ao navegador quanto tempo e que tipo de
suavização você quer:
dialog {
transition: opacity .5s var(--ease-3);
}
Adicionar movimento à transição
Se o usuário não tiver problemas com movimento, as caixas de diálogo grande e pequena vão deslizar para cima ao entrar e diminuir ao sair. Você pode fazer isso com a
consulta de mídia prefers-reduced-motion
e algumas propriedades abertas:
@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;
}
}
Adaptar a animação de saída para dispositivos móveis
Na seção de estilização anterior, o estilo de caixa de diálogo grande foi adaptado para dispositivos móveis e ficou mais parecido com uma folha de ações, como se um pequeno pedaço de papel tivesse deslizado para cima da parte de baixo da tela e ainda estivesse anexado a ela. A animação de saída de expansão não se encaixa bem nesse novo design, e podemos adaptá-la com algumas consultas de mídia e algumas propriedades abertas:
@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 decorrem do desejo de dispensar a ação com um clique (clicando no plano de fundo da caixa de diálogo), animação e alguns eventos adicionais para melhorar o tempo de obtenção dos dados do formulário.
Como adicionar a dispensa leve
Essa tarefa é simples e uma ótima adição a um elemento de caixa de diálogo que não está sendo animado. A interação é alcançada observando os cliques no elemento
de diálogo e aproveitando o encapsulamento
de eventos
para avaliar o que foi clicado, e só
close()
se for o elemento mais alto:
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 gerar insights sobre como a
caixa de diálogo foi fechada. Você vai notar que também forneci strings próximas sempre que chamo
a função de vários botões para dar contexto ao meu aplicativo sobre
a interação do usuário.
Adicionar eventos de encerramento e encerrados
O elemento de 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 antes e depois da animação para que uma mudança pegue os dados ou redefina o formulário de diálogo. Uso esse recurso aqui para gerenciar a adição do
atributo inert
na caixa de diálogo fechada. Na demonstração, uso esses recursos para modificar
a lista de avatares se o usuário tiver enviado uma nova imagem.
Para isso, crie dois novos eventos chamados closing
e closed
. Em seguida, ouça o evento de fechamento integrado na caixa de diálogo. Defina a caixa de diálogo como
inert
e envie o evento closing
. A próxima tarefa é aguardar a conclusão das
animações e transições na caixa de diálogo e, em seguida, 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 toast, retorna uma promessa com base na
conclusão das promessas de animação e transição. Por isso, dialogClose
é uma função
async;
ela pode então
await
a promessa retornada e avançar com confiança para o evento fechado.
Adicionar eventos de abertura e abertos
Esses eventos não são tão fáceis de adicionar porque o elemento de caixa de diálogo integrado não fornece um evento de abertura como faz com o fechamento. Uso um MutationObserver para fornecer insights sobre a mudança de atributos da caixa de diálogo. Neste observador, vou monitorar mudanças no atributo aberto e gerenciar os eventos personalizados de acordo.
Assim como iniciamos os eventos "closing" e "closed", crie dois novos eventos
chamados opening
e opened
. Desta vez, em vez de ficar aguardando o evento de fechamento da caixa de diálogo, use um observador de mutação criado para monitorar os atributos dela.
…
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ção será chamada quando os atributos da caixa de diálogo forem alterados, fornecendo a lista de mudanças como uma matriz. Itere as mudanças de atributo, procurando o attributeName
a ser aberto. Em seguida, verifique 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
em um elemento que solicita
autofocus
ou no primeiro elemento button
encontrado na caixa de diálogo. Por fim, 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.
Adicionar um evento removido
Em aplicativos de página única, as caixas de diálogo são adicionadas e removidas com base em rotas ou outras necessidades e estados do aplicativo. Pode ser útil limpar eventos ou dados quando uma caixa de diálogo é removida.
É possível fazer isso com outro observador de mutação. Desta vez, em vez de observar atributos em um elemento de caixa de diálogo, vamos observar os filhos do elemento body e monitorar a remoção de elementos de caixa de diálogo.
…
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ção é chamado sempre que filhos são adicionados ou removidos
do corpo do documento. As mutações específicas monitoradas são para
removedNodes
que têm o
nodeName
de
uma caixa de diálogo. Se uma caixa de diálogo for removida, os eventos de clique e fechamento serão removidos para liberar memória, e o evento personalizado de remoção será enviado.
Remover o atributo de carregamento
Para evitar que a animação da caixa de diálogo seja reproduzida ao ser adicionada à página ou no carregamento dela, um atributo de carregamento foi adicionado à caixa de diálogo. O script a seguir aguarda a conclusão das animações da caixa de diálogo e remove o atributo. Agora, a caixa de diálogo pode animar a entrada e a saída, e escondemos uma animação que poderia distrair.
export default async function (dialog) {
…
await animationsComplete(dialog)
dialog.removeAttribute('loading')
}
Saiba mais sobre o problema de impedir animações de keyframe no carregamento da página aqui.
Todos juntos
Aqui está 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 receber um elemento de caixa de diálogo que quer ter esses novos eventos e funcionalidades adicionados:
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 são atualizadas com rejeição leve, correções de carregamento de animação e mais eventos para trabalhar.
Como detectar os novos eventos personalizados
Cada elemento de caixa de diálogo atualizado 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)
Confira dois exemplos de como processar esses 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 de caixa de diálogo, uso esse evento fechado e os dados do formulário para adicionar um novo elemento de avatar à lista. O tempo é bom, já que a caixa de diálogo concluiu a animação de saída, e alguns scripts animam o novo avatar. Graças aos novos eventos, a orquestração da experiência do usuário pode ser mais tranquila.
Observe dialog.returnValue
: ele contém a string de fechamento transmitida quando o
evento de caixa de diálogo close()
é chamado. É fundamental no evento dialogClosed
saber se a caixa de diálogo foi fechada, cancelada ou confirmada. Se for confirmada, o
script vai extrair os valores do formulário e redefini-lo. A redefinição é útil para que, quando a caixa de diálogo for mostrada novamente, ela esteja em branco e pronta para um novo envio.
Conclusão
Agora que você sabe como eu fiz, como você faria? 🙂
Vamos diversificar nossas abordagens e aprender todas as maneiras de criar na Web.
Crie uma demonstração, me envie um tweet com o link, e eu vou adicionar à seção de remixes da comunidade abaixo.
Remixes da comunidade
- @GrimLink com uma caixa de diálogo 3 em 1.
- @mikemai2awesome com um remix
legal que não muda a
propriedade
display
. - @geoffrich_ com Svelte e um bom acabamento Svelte FLIP.
Recursos
- Código-fonte no GitHub
- Avatares do Doodle