Uma visão geral básica de como criar mini e mega modais adaptáveis a cores, responsivos e acessíveis com o elemento <dialog>
.
Nesta postagem, quero compartilhar minha opinião sobre como criar mini e mega modais adaptáveis a cores, responsivos e acessíveis com o elemento <dialog>
.
Teste a demonstração e confira a
origem.
Se você preferir o vídeo, aqui está uma versão do YouTube desta postagem:
Visão geral
O elemento
<dialog>
é ótimo para informações ou ações contextuais in-page. Considere quando a
experiência do usuário pode se beneficiar de uma ação na mesma página em vez da 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 vários navegadores:
Achei que faltavam algumas coisas no elemento. Por isso, neste Desafio de GUI, eu adiciono os itens de experiência do desenvolvedor que eu espero: 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 ficará
oculto automaticamente e tem estilos integrados para se sobrepor ao conteúdo.
<dialog>
…
</dialog>
Podemos melhorar essa linha de base.
Tradicionalmente, um elemento de caixa de diálogo compartilha muito com um modal, e geralmente os nomes
são intercambiáveis. Tomei a liberdade de usar o elemento de caixa de diálogo para
caixas de diálogo pequenas (mini) e de página inteira (mega). Eu as nomeei
"mega" e "mini", com as duas 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 os 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 funcionar
juntos.
É recomendável fazer com que um elemento de formulário envolva o conteúdo da caixa de diálogo para que
o JavaScript possa acessar os dados que o usuário inseriu. Além disso, os botões em
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 megacaixa de diálogo tem três elementos dentro do formulário:
<header>
,
<article>
e
<footer>
.
Eles servem como contêineres semânticos e 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 é sobre entradas de formulário e informações. 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
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.
Minicaixa de diálogo
A minicaixa de diálogo é muito semelhante à megacaixa de diálogo, só tem 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 fornece uma base sólida para um elemento completo da janela de visualização que pode coletar dados e interações do usuário. Esses elementos essenciais podem gerar algumas interações muito interessantes e poderosas 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 de costume, 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 coloque o foco nos botões de abrir e fechar relevantes. Quando essa navegação lateral é aberta, o foco é colocado no botão "Fechar". Quando o botão "Fechar" é pressionado, o foco é restaurado no botão que o abriu.
Com o elemento da caixa de diálogo, este é o comportamento padrão integrado:
Infelizmente, se você quiser animar a caixa de diálogo, essa funcionalidade será perdida. Na seção JavaScript, vou restaurar essa funcionalidade.
Foco de detecção
O elemento da caixa de diálogo gerencia
inert
para você no documento. Antes de inert
, o JavaScript era usado para monitorar o foco
que sai de um elemento e, nesse momento, ele intercepta e o coloca de volta.
Depois de inert
, qualquer parte do documento pode ficar "congelada", desde que
não seja mais um alvo de foco ou seja interativa com um mouse. Em vez de capturar
o foco, ele é guiado para a única parte interativa do documento.
Abrir e focar automaticamente em um elemento
Por padrão, o elemento da caixa de diálogo atribuirá o 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 como padrão,
use o atributo autofocus
. Conforme descrito anteriormente, acho que a prática recomendada
é usar o botão "Cancel", e não o de confirmação. Isso garante que
a confirmação seja deliberada e não acidental.
Fechar com a tecla Esc
É importante facilitar o fechamento desse elemento que pode causar interrupções. Felizmente, o elemento de caixa de diálogo processará a tecla de escape para você, liberando-o da carga de orquestração.
Estilos
Há um caminho fácil para estilizar o elemento da caixa de diálogo e um caminho difícil. Para conseguir o caminho
mais fácil, não há mudanças na propriedade de exibição da caixa de diálogo nem o trabalho
com as limitações. 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.
Como definir o estilo com propriedades abertas
Para acelerar as cores adaptáveis e a consistência geral do design, coloquei de maneira descarada minha biblioteca de variáveis CSS Open Props. Além das variáveis fornecidas sem custo financeiro, também importo um arquivo normalize e alguns botões, ambos fornecidos pelo Open Props como importações opcionais. Essas importações me ajudam a me concentrar na personalização da caixa de diálogo e da demonstração, sem precisar de muitos estilos para oferecer suporte e melhorar a aparência.
Definir o estilo do elemento <dialog>
Possuir a 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 não pode ser animado
para dentro e para fora, apenas dentro. Eu quero animar tanto para dentro quanto para fora, e a primeira etapa é definir minha própria propriedade display:
dialog {
display: grid;
}
Quando você muda e, portanto, tem 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 é fechado. É possível representar esse estado visualmente e impedir que a caixa de diálogo receba interações com os estilos abaixo:
dialog:not([open]) {
pointer-events: none;
opacity: 0;
}
Agora a caixa de diálogo fica invisível, e não é possível interagir quando ela não está aberta. Mais tarde,
vou adicionar JavaScript para gerenciar o atributo inert
na caixa de diálogo, garantindo que
os usuários de teclado e leitor de tela também não consigam acessar a caixa de diálogo oculta.
Aplicar um tema de cor adaptável ao diálogo
Embora o color-scheme
ative seu documento em um tema de cores adaptável
fornecido pelo navegador para as preferências do sistema claro e escuro, eu queria personalizar
o elemento da caixa de diálogo mais do que isso. As propriedades abertas oferecem algumas cores
de superfície que se adaptam automaticamente às
preferências claras e escuras do sistema, semelhante ao uso do color-scheme
. Eles
são ótimos para criar camadas em um design, e eu adoro usar cores para ajudar
a oferecer suporte visual para essa aparência de superfícies de camada. A cor do plano 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 posteriormente para elementos filhos, como cabeçalho e rodapé. Eu os considero como um elemento extra para o diálogo, mas são muito importantes para criar um design atrativo e bem projetado.
Dimensionamento responsivo de caixas de diálogo
Por padrão, a caixa de diálogo delega o tamanho dela 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 a 90% da largura da janela de visualização. Isso
garante que a caixa de diálogo não fique de ponta a ponta em um dispositivo móvel e não seja tão
larga em uma tela de computador a ponto de ser 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 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;
}
Percebeu como eu tenho max-block-size
duas vezes? O primeiro usa 80vh
, uma unidade física
da janela de visualização. O que eu realmente quero é manter a caixa de diálogo dentro do fluxo relativo,
para usuários internacionais. Portanto, uso a unidade dvb
lógica, mais recente e apenas parcialmente
compatível na segunda declaração para quando ela se tornar mais estável.
Posicionamento de megacaixas de diálogo
Para ajudar no posicionamento de um elemento da caixa de diálogo, vale a pena dividir as duas partes: o pano 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 indicar que essa caixa de diálogo está na frente e o conteúdo atrás dela precisa estar inacessível. O contêiner da caixa de diálogo pode se centralizar sobre esse pano de fundo e assumir a forma que o conteúdo exigir.
Os estilos abaixo fixam o elemento da caixa de diálogo na janela, estendendo-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 megacaixa de diálogo para dispositivos móveis
Em pequenas janelas de visualização, estilo esse mega modal de página inteira de forma um pouco diferente. Eu
defino 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
actionsheet 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 minicaixa de diálogo
Ao usar uma janela de visualização maior, como em um computador desktop, optei por posicionar as mini caixas de diálogo sobre o elemento que as chama. Para fazer isso, preciso do JavaScript. Você pode encontrar a técnica que eu uso aqui, mas acho que está além do escopo deste artigo. Sem o JavaScript, a minicaixa de diálogo aparece no centro da tela, como uma megacaixa de diálogo.
Destaque os dados
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 do diálogo. A profundidade é alcançada com um dos acessórios de sombra cuidadosamente elaborados pela Open Props:
dialog {
…
border-radius: var(--radius-3);
box-shadow: var(--shadow-6);
}
Personalizar o pseudoelemento do pano de fundo
Escolhi trabalhar bem 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
, na esperança de que os navegadores
possam permitir a transição do elemento do pano de fundo no futuro:
dialog::backdrop {
transition: backdrop-filter .5s ease;
}
Extras de estilo
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 atrás dela, o que eu não quero:
Normalmente,
overscroll-behavior
seria minha solução habitual, mas de acordo com a
especificação,
ele não tem efeito na caixa de diálogo porque não é uma porta de rolagem, ou seja, não é
um botão de rolagem, por isso não há nada a ser evitado. É possível usar o JavaScript para acompanhar
os novos eventos deste guia, como "fechado" e "aberto", e alternar
overflow: hidden
no documento ou esperar que o :has()
fique estável em
todos os navegadores:
html:has(dialog[open][modal-mode="mega"]) {
overflow: hidden;
}
Agora, quando uma megacaixa 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, ele é usado aqui para posicionar os elementos de cabeçalho, rodapé e artigo. Com esse layout, pretendo articular o artigo filho como uma
área rolável. Faço isso com
grid-template-rows
.
O elemento do artigo recebe a 1fr
, e o próprio formulário tem a mesma altura máxima
do elemento da caixa de diálogo. A definição dessa altura e do tamanho da linha firmes é o que permite que o elemento do artigo seja restrito e rolado quando transbordar:
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>
O papel 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 ficar atrás do conteúdo do artigo na caixa de diálogo. Esses requisitos levam a um contêiner flexbox, itens alinhados verticalmente com espaço às bordas e alguns 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);
}
}
Definir o estilo do botão "Fechar" do cabeçalho
Como a demonstração usa os botões "Open Props", o botão "Fechar" é personalizado como um botão centralizado em um ícone redondo, da seguinte 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 um papel 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 limites para si mesmo, o que fornece restrições para que esse elemento de artigo atinja se ele ficar muito alto. Defina overflow-y: auto
para que as barras de rolagem sejam exibidas apenas quando necessário.
Contenha a rolagem dentro dela 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, um pouco de 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);
}
}
Definir o estilo do 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. Ela usa um layout
flexbox de encapsulamento 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, 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
Os elementos das caixas de diálogo geralmente são animados porque entram e saem da janela. Oferecer às caixas de diálogo 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
define a exibição como grade e nunca a define como nenhuma. Isso desbloqueia a capacidade de
animação para dentro e para fora.
Os recursos abertos vêm com muitas animações de frames-chave para uso, o que torna a orquestração fácil e legível. Aqui estão as metas de animação e a abordagem em camadas que segui:
- O movimento reduzido é a transição padrão, com uma simples opacidade que aparece.
- Se o movimento estiver correto, serão adicionadas animações de escala e deslize.
- O layout responsivo para dispositivos móveis da megacaixa de diálogo é ajustado para deslizar para fora.
Uma transição padrão segura e significativa
Embora as propriedades abertas tenham frames-chave para aparecer e desaparecer, prefiro essa
abordagem de transições em camadas como padrão, com animações de frame-chave como
possíveis upgrades. Anteriormente, já ajustamos 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 que você quer:
dialog {
transition: opacity .5s var(--ease-3);
}
Adicionar movimento à transição
Se o usuário concordar com o movimento, as caixas de diálogo mega e mini precisam deslizar
para cima como entrada e ser expandidas como saída. É possível 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;
}
}
Adaptação da animação de saída para dispositivos móveis
No início da seção de estilo, o estilo da megacaixa de diálogo foi adaptado para que os dispositivos móveis sejam mais como uma folha de ações, como se um pequeno pedaço de papel tivesse passado de baixo para cima na tela e ainda estivesse anexado à parte de baixo. A animação de saída do escalonamento horizontal não se encaixa bem nesse novo design. 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á vários fatores a serem adicionados com o 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 provenientes do desejo de dispensar a iluminação (clicar no plano de fundo da caixa de diálogo), animação e alguns outros eventos para melhorar o tempo ao receber os dados do formulário.
Adicionando luz dispensada
Essa tarefa é simples e é um ótimo complemento para um elemento de caixa de diálogo que não
está sendo animado. A interação é alcançada observando os cliques no elemento
da caixa de diálogo e aproveitando o balanço
de eventos
para avaliar o que foi clicado, e só vai
close()
se for o elemento mais acima:
export default async function (dialog) {
dialog.addEventListener('click', lightDismiss)
}
const lightDismiss = ({target:dialog}) => {
if (dialog.nodeName === 'DIALOG')
dialog.close('dismiss')
}
Atenção: dialog.close('dismiss')
. O evento é chamado e uma string é fornecida.
Essa string pode ser recuperada por outro JavaScript para receber insights sobre como a
caixa de diálogo foi fechada. Você descobrirá que também forneci strings de fechamento toda vez que chamo
a função usando vários botões para fornecer contexto ao app sobre
a interação do usuário.
Adicionando eventos fechados 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 uma mudança que capture os
dados ou redefina o formulário da caixa de diálogo. Eu o uso aqui para gerenciar a adição do
atributo inert
à caixa de diálogo fechada e, na demonstração, uso esse atributo 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,
detecte o evento de fechamento integrado na caixa de diálogo. Aqui, defina a caixa de diálogo como inert
e envie o evento closing
. A próxima tarefa é aguardar a execução das
animações e transições terminar 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 da animação e das promessas de transição. É por isso que dialogClose
é uma função assíncrona. Ele pode então await
retornar a promessa e seguir em frente com confiança para o evento fechado.
Adicionar eventos abertos e de abertura
Esses eventos não são tão fáceis de adicionar, já que o elemento da caixa de diálogo integrado não fornece um evento aberto, como acontece com o fechamento. Uso um MutationObserver para fornecer insights sobre a mudança de atributos da caixa de diálogo. Neste observador, vou monitorar 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 anteriormente o evento de fechamento da caixa de diálogo, desta vez 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 nas
mudanças no atributo, procurando o attributeName
como 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
, defina o foco
para um elemento que solicite
autofocus
ou o primeiro elemento button
encontrado na caixa de diálogo. Por fim, de maneira semelhante ao evento de fechamento
e fechamento, 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 geralmente 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 conseguir isso com outro observador de mutações. Desta vez, em vez de observar atributos em um elemento da caixa de diálogo, vamos observar os filhos do elemento do corpo e observar a remoção desses elementos.
…
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 observadas 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 de remoção personalizado será despachado.
Como remover o atributo de carregamento
Um atributo de carregamento foi adicionado à caixa de diálogo para evitar que a animação da caixa de diálogo reproduza a animação de saída quando adicionada à página ou no carregamento da página. O script a seguir aguarda a execução das animações de caixas de diálogo terminar e remove o atributo. Agora, a caixa de diálogo pode ser animada para dentro e para fora, e ocultamos uma animação que desvia a atenção.
export default async function (dialog) {
…
await animationsComplete(dialog)
dialog.removeAttribute('loading')
}
Saiba mais sobre como impedir animações de frame-chave no carregamento de página aqui.
Tudo junto
Veja 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 precisa ser chamada e transmitir um elemento da caixa de diálogo que quer que esses novos eventos e funcionalidades sejam 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 serão atualizadas com dispensa de luz, correções de carregamento de animação e mais eventos para trabalhar.
Como detectar os novos eventos personalizados
Cada elemento atualizado da caixa de diálogo agora pode detectar cinco novos eventos, como estes:
MegaDialog.addEventListener('closing', dialogClosing)
MegaDialog.addEventListener('closed', dialogClosed)
MegaDialog.addEventListener('opening', dialogOpening)
MegaDialog.addEventListener('opened', dialogOpened)
MegaDialog.addEventListener('removed', dialogRemoved)
Aqui estão dois exemplos de como lidar com 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 da caixa de diálogo, uso esse evento fechado e os dados do formulário para adicionar um novo elemento de avatar à lista. O momento certo é que a caixa de diálogo tenha concluído a animação de saída e, em seguida, alguns scripts sejam animados no novo avatar. Graças aos novos eventos, a orquestração da experiência do usuário pode ser mais suave.
Observe que dialog.returnValue
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 for confirmado, o
script vai capturar os valores do formulário e redefinir o formulário. A redefinição é útil para
que, quando a caixa de diálogo for mostrada novamente, ela 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 (link em inglês) e eu vou adicionar o conteúdo à 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 Svelte e um bom polimento de Svelte FLIP.
Recursos
- Código-fonte no GitHub
- Avatares do Doodle