Uma visão geral básica sobre como criar mini e mega modais responsivos, acessíveis e com adaptação de cores 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 confira a
fonte.
Se preferir vídeos, confira a 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 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:
Descobri que o elemento estava faltando algumas coisas. Portanto, 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 ficará
oculto automaticamente e tem estilos integrados para se sobrepor ao conteúdo.
<dialog>
…
</dialog>
Podemos melhorar essa referência.
Tradicionalmente, um elemento de caixa de diálogo tem muitas semelhanças com um modal, e os nomes
geralmente são intercambiáveis. Usei o elemento de caixa de diálogo para
pequenos pop-ups de caixa de diálogo (mini) e caixas de diálogo de página inteira (mega). Chamei
de "mega" e "mini", com as duas caixas de diálogo ligeiramente adaptadas para casos de uso diferentes.
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 em elementos de caixa de diálogo são feitos para serem
usados 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 inseridos pelo usuário. Além disso, os botões em
um formulário que usa 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 Mega
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 e 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
fechamento. 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 gerenciador de eventos inline onclick
. O atributo autofocus
vai receber
o foco quando a caixa de diálogo for aberta, e a prática recomendada é colocá-lo no
botão de cancelamento, não no botão de confirmação. Isso garante que a confirmação seja
deliberada e não acidental.
Minicaixa de diálogo
A caixa de diálogo mini é muito semelhante à caixa de diálogo mega, mas não 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 de diálogo oferece uma base sólida para um elemento de viewport completo que pode coletar dados e interação do usuário. Esses recursos essenciais podem gerar interações muito interessantes e poderosas no seu site ou app.
Acessibilidade
O elemento de caixa de diálogo tem uma acessibilidade integrada muito boa. Em vez de adicionar esses recursos, como de costume, muitos já estão lá.
Restaurar o foco
Como fizemos manualmente em Como criar um componente de barra lateral, é importante que abrir e fechar algo coloque o foco nos botões de abrir e fechar relevantes. Quando o menu lateral é aberto, o foco é colocado no botão de fechar. Quando o botão de fechar é pressionado, o foco é restaurado no botão que o abriu.
Com o elemento de caixa de diálogo, esse é o comportamento padrão integrado:
Se você quiser animar a caixa de diálogo, essa funcionalidade será perdida. Na seção de JavaScript, vou restaurar essa funcionalidade.
Foco de captura
O elemento de diálogo gerencia
inert
para você no documento. Antes do inert
, o JavaScript era usado para detectar o foco
saindo de um elemento, momento em que ele é interceptado e colocado 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 um elemento
Por padrão, o elemento de caixa de diálogo vai 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 por padrão,
use o atributo autofocus
. Como descrito anteriormente, acho que a prática recomendada
é colocar isso no botão de cancelamento, e não no botã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 potencialmente interruptivo. 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. O caminho
fácil é alcançado ao não mudar a propriedade de exibição da caixa de diálogo e trabalhar
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.
Criar estilo com Open Props
Para acelerar as cores adaptáveis e a consistência geral do design, eu coloquei descaradamente minha biblioteca de variáveis CSS Open Props. Além das variáveis gratuitas fornecidas, 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 personalizar a caixa de diálogo e a demonstração sem precisar de muitos estilos para oferecer suporte e melhorar a aparência.
Como estilizar o elemento <dialog>
Possuir a propriedade de exibição
O comportamento padrão de mostrar e ocultar um elemento de diálogo alterna a propriedade
de exibição de block
para none
. Isso significa que não é possível animar
a entrada e a saída, apenas a entrada. 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 adequada do usuário. 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 seguintes estilos:
dialog:not([open]) {
pointer-events: none;
opacity: 0;
}
Agora, a caixa de diálogo fica invisível e não pode ser aberta para interação. Mais tarde,
vou adicionar um pouco de 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 possam 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 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 que se adaptam automaticamente às
preferências do sistema claro e escuro, semelhante ao uso do color-scheme
. Elas
são ótimas para criar camadas em um design, e eu adoro usar cores para ajudar
a mostrar visualmente essa aparência de superfícies de camadas. A cor de plano de fundo é
var(--surface-1)
. Para ficar sobre essa 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 considero isso um extra para um elemento de caixa de diálogo, mas muito importante para criar um design de caixa de diálogo atraente e bem projetado.
Dimensionamento de caixas de diálogo responsivas
A caixa de diálogo delega o tamanho ao conteúdo por padrão, 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 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 seja um elemento de caixa de diálogo 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;
}
Note que max-block-size
aparece 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 dvb
lógica, mais recente e com suporte parcial
na segunda declaração para quando ela se tornar mais estável.
Posicionamento da caixa de diálogo "Mega"
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 plano de fundo precisa cobrir tudo, fornecendo um efeito de sombra para ajudar a garantir que essa caixa de diálogo esteja na frente e que o conteúdo por trás esteja inacessível. O contêiner de diálogo pode ser centralizado sobre esse plano de fundo e ter qualquer forma que o conteúdo exigir.
Os estilos a seguir fixam o elemento 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 caixa de diálogo mega para dispositivos móveis
Em pequenas janelas de visualização, eu estilo esse mega modal de página inteira de forma um pouco diferente. Defino
a margem inferior como 0
, o que traz o conteúdo da caixa de diálogo para a parte inferior 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 caixa de diálogo pequena
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 de JavaScript. Confira a técnica que uso neste link, mas acho que ela está fora do escopo deste artigo. Sem o JavaScript, a mini caixa de diálogo aparece no centro da tela, assim como a caixa de diálogo grande.
Destaque seu canal
Por fim, adicione um pouco de estilo à caixa de diálogo para que ela pareça uma superfície macia acima da página. A suavidade é alcançada arredondando os cantos da caixa de diálogo. A profundidade é alcançada com um dos elementos de sombra criados cuidadosamente pelo Open Props:
dialog {
…
border-radius: var(--radius-3);
box-shadow: var(--shadow-6);
}
Personalizar o pseudoelemento de pano de fundo
Escolhi trabalhar com o plano de fundo de forma muito leve, adicionando apenas um efeito de desfoque com
backdrop-filter
à caixa de diálogo:
dialog[modal-mode="mega"]::backdrop {
backdrop-filter: blur(25px);
}
Também coloquei 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 essa seção de "extras" porque ela tem mais a ver com a demonstração do elemento de caixa de diálogo do que com o elemento de 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 não é o que eu quero:
Normalmente,
overscroll-behavior
seria minha solução usual, mas de acordo com a
especificação,
ela não tem efeito na caixa de diálogo porque não é uma porta de rolagem, ou seja, não é
um controle deslizante, então não há nada a ser impedido. Poderia usar o JavaScript para detectar
os novos eventos deste guia, como "fechado" e "aberto", e alternar
overflow: hidden
no documento ou esperar que :has()
seja 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 definir o cabeçalho, o rodapé e
os elementos do artigo. Com esse layout, pretendo articular o artigo filho como uma
área rolável. Eu consigo 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 tamanho de linha fixos é o que
permite que o elemento do artigo seja limitado e role quando ele transbordar:
dialog > form {
display: grid;
grid-template-rows: auto 1fr auto;
align-items: start;
max-block-size: 80vh;
max-block-size: 80dvb;
}
Como 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 de fechamento 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 que são espaçados nas bordas e alguns paddings e lacunas para dar espaço ao título e aos botões de fechamento:
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);
}
}
Estilo do botão "Fechar" do cabeçalho
Como a demonstração está usando os botões do Open Props, o botão de fechar é personalizado em um botão central com ícone redondo, 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;
}
Como 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 máximos para
ele mesmo, que fornecem restrições para que esse elemento de artigo seja alcançado se 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>
O rodapé tem a função de 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);
}
}
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
de flexbox de união com gap
para fornecer espaço entre os botões. Os elementos do 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 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
animar para dentro e para fora.
O Open Props vem com muitas animações de chave para uso, o que facilita a orquestração e torna o código mais legível. Estas são as metas de animação e a abordagem em camadas que usei:
- O movimento reduzido é a transição padrão, um simples desbotamento de opacidade.
- Se o movimento estiver ok, as animações de deslizar e de escala serão adicionadas.
- O layout responsivo para dispositivos móveis da caixa de diálogo mega é ajustado para deslizar para fora.
Uma transição padrão segura e significativa
Embora o Open Props tenha frames-chave para desbotamento, prefiro essa
abordagem em camadas de transições como padrão com animações de frame-chave como
upgrades em potencial. 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 que você quer:
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 vão deslizar
para cima como entrada e aumentar de tamanho como saída. É possível fazer isso com a
consulta de mídia prefers-reduced-motion
e alguns objetos abertos:
@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;
}
}
Como adaptar a animação de saída para dispositivos móveis
Na seção de estilização, o estilo de caixa de diálogo gigante é adaptado para que dispositivos móveis se pareçam mais com uma página de ação, como se um pequeno pedaço de papel deslizasse da parte de baixo da tela e ainda estivesse preso a ela. A animação de saída de dimensionamento 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á muitas 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 vêm do desejo de dispensar rapidamente (clicando no plano de fundo da caixa de diálogo), da animação e de alguns eventos adicionais para um melhor tempo de recebimento dos dados do formulário.
Como adicionar a dispensação de luz
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 cliques no elemento
de diálogo e aproveitando o evento
bubbling
para avaliar o que foi clicado. Ela só vai
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 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 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 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 uso isso aqui para gerenciar a adição do
atributo inert
na caixa de diálogo fechada. Na demonstração, uso isso para modificar
a lista de avatares se o usuário enviou 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. A partir daqui, defina a caixa de diálogo como
inert
e envie o evento closing
. A próxima tarefa é esperar que as
animações e transições terminem de ser executadas 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
Toast, 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, então,
await
a promessa retornada e avançar 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 de caixa de diálogo integrado não oferece um evento de abertura, como faz com o fechamento. Eu uso um MutationObserver para fornecer insights sobre a mudança de atributos da caixa de diálogo. Neste observador, vou observar as mudanças no atributo aberto e gerenciar os eventos personalizados de acordo com isso.
Assim como fizemos com os eventos de fechamento e de fechamento, crie dois novos eventos
chamados opening
e opened
. Onde antes ouvíamos o evento de fechamento
da caixa de diálogo, desta vez usamos 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, 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 solicite
autofocus
ou o 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 o término das animações
e envie o evento aberto.
Adicionar um evento removido
Em apps 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.
Você pode fazer isso com outro observador de mutação. Dessa vez, em vez de observar atributos em um elemento de caixa de diálogo, vamos observar os filhos do elemento do corpo e observar se os elementos de caixa de diálogo estão sendo 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ção é 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 personalizado removido será enviado.
Como remover o atributo de carregamento
Para evitar que a animação de saída seja reproduzida 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 de caixas de diálogo terminar e remove o atributo. Agora a caixa de diálogo pode ser animada e ocultamos uma animação que distraía o usuário.
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.
Tudo junto
Confira a 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 transmitida a um elemento 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 são atualizadas com descarte 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 diálogo, usei 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.
Aviso dialog.returnValue
: contém a string de encerramento 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 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, envie links para mim e 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 bom
remix que não muda a
propriedade
display
. - @geoffrich_ com Svelte e um bom Svelte FLIP.
Recursos
- Código-fonte no GitHub
- Avatares do Doodle