Como criar um componente de caixa de diálogo

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 meus pensamentos sobre como criar modelos adaptáveis a cores, mini e megamodais responsivos e acessíveis com o elemento <dialog>. Experimente a demonstração e veja o fonte.

Demonstração dos mega e mini diálogos em temas claros e escuros.

Se você preferir o vídeo, aqui está uma versão do YouTube desta postagem:

Visão geral

A <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 de várias ação: talvez porque o formulário seja pequeno ou a única ação necessária do usuário é confirmar ou cancelar.

O elemento <dialog> se tornou estável em vários navegadores:

Compatibilidade com navegadores

  • Chrome: 37.
  • Borda: 79.
  • Firefox: 98.
  • Safari: 15.4.

Origem

Achei que faltavam algumas coisas no elemento. Por isso, nesta GUI Desafio Eu adiciono a experiência do desenvolvedor itens esperados: eventos adicionais, iluminação, animações personalizadas e um mini e megatipo.

Marcação

Os elementos essenciais de um elemento <dialog> são modestos. O elemento vai automaticamente ocultos e têm 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, muitas vezes, os nomes são intercambiáveis. Aqui, tomei a liberdade de usar o elemento de diálogo para tanto pequenas janelas pop-up (mini) quanto caixas de diálogo de página inteira (mega). Eu nomeei em 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>

Captura de tela das caixas de diálogo &quot;Min&quot; e &quot;mega&quot; nos temas claro e escuro.

Nem sempre, mas geralmente os elementos de diálogo são usados para reunir algumas informações de interação. Os formulários dentro de elementos de caixa de diálogo foram adaptados juntas. É uma boa ideia ter um elemento de formulário envolvendo o conteúdo do diálogo para que O JavaScript pode acessar os dados que o usuário inseriu. Além disso, os botões dentro um formulário usando method="dialog" pode 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, bem como destinos de estilo para o apresentação do diálogo. O cabeçalho dá título ao modal e oferece um . 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 in-line onclick. O atributo autofocus receberá focar quando a caixa de diálogo é aberta, e acho que é uma prática recomendada colocar isso o botão de cancelamento, e não o 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ó falta uma <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 podem coletar dados e interação do usuário. Esses fundamentos podem deixar algumas interações interessantes e eficientes no seu site ou aplicativo.

Acessibilidade

O elemento da caixa de diálogo tem uma acessibilidade integrada muito boa. Em vez de adicionar como de costume, muitos já estão lá.

Restaurando o foco

Como fizemos manualmente em Como criar uma navegação lateral componente, é importante que abrir e fechar algo corretamente coloca o foco nas aberturas e nos fechamentos relevantes botões. Quando essa navegação lateral é aberta, o foco é colocado no botão "Fechar". Quando o botão Fechar for pressionado, o foco será restaurado no botão que o abriu.

Com o elemento da caixa de diálogo, este é o comportamento padrão integrado:

Se você quiser animar a caixa de diálogo, essa funcionalidade for perdida. Na seção JavaScript, vou restaurar isso. funcionalidade de armazenamento.

Foco de detecção

O elemento da caixa de diálogo gerencia inert no documento. Antes de inert, o JavaScript era usado para monitorar o foco deixando um elemento, em que ponto ele intercepta e o coloca de volta.

Compatibilidade com navegadores

  • Chrome: 102
  • Borda: 102.
  • Firefox: 112
  • Safari: 15.5.

Origem

Depois de inert, qualquer parte do documento poderá ser "congelada" de forma que estejam não focam mais os alvos ou são interativos com um mouse. Em vez de prender foco, o foco é orientado 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 de 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 é uma prática recomendada para colocar isso no botão de cancelamento, e não no botão de confirmação. Isso garante que a confirmação é deliberada e não acidental.

Fechar com a tecla Esc

É importante facilitar o fechamento desse elemento que pode causar interrupções. Felizmente, o elemento da caixa de diálogo processará a tecla Esc, liberando você do fardo de orquestração.

Estilos

Há um caminho fácil para estilizar o elemento da caixa de diálogo e um caminho difícil. O jeito fácil, caminho é conseguido não alterando a propriedade de exibição da caixa de diálogo e trabalhando com suas limitações. Sigo o difícil para fornecer animações personalizadas para abrindo e fechando 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, escolhi descaradamente trouxe minha biblioteca de variáveis CSS Abrir configurações. Em além das variáveis fornecidas sem custo financeiro, também importo um normalize e alguns buttons, que são "Open Props" fornece como importações opcionais. Essas importações ajudam a me concentrar na personalização e demonstração, sem precisar de muitos estilos para oferecer suporte e fazer com que pareça bom.

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 exibição propriedade de block a none. Infelizmente, isso significa que não pode ser animado dentro e fora, apenas dentro. quero animar o fluxo de entrada e saída, e a primeira etapa é para definir meu próprio display:

dialog {
  display: grid;
}

Mudando e, portanto, sendo proprietário do valor da propriedade de exibição, conforme mostrado nas acima do snippet CSS, uma quantidade considerável de estilos precisa ser gerenciada a fim de para facilitar a experiência adequada do usuário. Primeiro, o estado padrão de uma caixa de diálogo é fechadas. É possível representar esse estado visualmente e impedir que o diálogo seja exibido. recebendo 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 é 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 leitores de tela também não conseguem alcançar a caixa de diálogo oculta.

Aplicar um tema de cor adaptável ao diálogo

Grande caixa de diálogo mostrando os temas claro e escuro, demonstrando as cores da superfície.

Enquanto o color-scheme ativa seu documento em uma opção fornecida pelo navegador tema de cores adaptáveis às preferências claras e escuras do sistema, eu queria personalizar elemento de caixa de diálogo mais do que isso. O Open Props fornece algumas plataformas cores que se adaptam automaticamente preferências claras e escuras do sistema, semelhante ao uso de color-scheme. Esses são ótimos para criar camadas em um design e adoro usar cores para ajudar oferecem suporte visual para essa aparência de superfícies de camada. A cor de fundo é var(--surface-1) para assumir 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 vão ser adicionadas depois para elementos filhos, como o cabeçalho e rodapé. Eu os considero extra para um elemento de diálogo, mas muito importantes para criar um design de diálogo atraente e bem elaborado.

Dimensionamento responsivo de caixas de diálogo

Por padrão, a caixa de diálogo delega seu tamanho ao conteúdo, o que geralmente é ótimo. Meu objetivo aqui é restringir max-inline-size para 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 em uma tela de desktop que é difícil de ler. Em seguida, adiciono max-block-size para que a caixa de diálogo não exceda a altura da página. Isso também significa que vamos precisa especificar onde fica a área rolável da caixa de diálogo, caso ela seja alta elemento da caixa de diálogo.

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, um modelo unidade da janela de visualização. O que eu realmente quero é manter o diálogo dentro do fluxo relativo, para usuários internacionais, então uso a lógica, mais recente e apenas parcialmente suporte à unidade dvb na segunda declaração para quando ela se torna mais estável.

Posicionamento de megacaixas de diálogo

Para ajudar no posicionamento de um elemento de caixa de diálogo, vale a pena dividir seus dois partes: o pano de fundo em tela cheia e o contêiner da caixa de diálogo. O pano de fundo precisa cobrem tudo, fornecendo um efeito de tonalidade para ajudar a apoiar que essa caixa de diálogo seja à frente e o conteúdo atrás fica inacessível. O contêiner da caixa de diálogo é livre para centralizam-se sobre esse pano de fundo e assumem a forma que o conteúdo exige.

Os estilos a seguir fixam o elemento da caixa de diálogo na janela, estendendo-o para cada e usa 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 Defina a margem de baixo como 0, o que coloca o conteúdo da caixa de diálogo na parte de baixo do na janela de visualização. Com alguns ajustes de estilo, posso transformar o diálogo em uma actionsheet, mais próximo 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;
  }
}

Captura de tela do DevTools sobrepondo o espaçamento de margem 
  na megacaixa de diálogo para computadores e dispositivos móveis quando aberta.

Posicionamento da minicaixa de diálogo

Ao usar uma janela de visualização maior, como em um computador desktop, optei por posicionar as minicaixas de diálogo sobre do elemento que as chamou. Para fazer isso, preciso do JavaScript. Você encontra 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 ao diálogo para que ele pareça uma superfície macia acima da página. A suavidade é alcançada arredondando os cantos do diálogo. A profundidade é alcançada com uma das sombras cuidadosamente elaboradas de objetos abertos propriedades:

dialog {
  
  border-radius: var(--radius-3);
  box-shadow: var(--shadow-6);
}

Personalizar o pseudoelemento do pano de fundo

Optei por trabalhar bem levemente com o pano de fundo, adicionando apenas um efeito de desfoque com backdrop-filter até a caixa de diálogo mega:

Compatibilidade com navegadores

  • Chrome: 76.
  • Borda: 79.
  • Firefox: 103
  • Safari: 18.

Origem

dialog[modal-mode="mega"]::backdrop {
  backdrop-filter: blur(25px);
}

Também optei por fazer uma transição no backdrop-filter, esperando que os navegadores permitirá a transição do elemento do pano de fundo no futuro:

dialog::backdrop {
  transition: backdrop-filter .5s ease;
}

Captura de tela da mega caixa de diálogo sobrepondo um plano de fundo desfocado com avatares coloridos.

Extras de estilo

Eu chamo esta seção de "extras" porque tem mais a ver com meu elemento de diálogo do que o elemento 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, que eu não quero:

Normalmente, overscroll-behavior seria minha solução habitual, mas de acordo com o especificação, 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 para que não haja nada para evitar. Eu poderia usar o JavaScript para observar os novos eventos deste guia, como "fechados" e "aberto", e alternar overflow: hidden no documento ou posso esperar até que o :has() fique estável em Todos os navegadores:

Compatibilidade com navegadores

  • Chrome: 105
  • Borda: 105.
  • Firefox: 121
  • Safari: 15.4.

Origem

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 a interação informações do usuário, elas são usadas aqui para definir o cabeçalho, o rodapé e elementos de artigo. Com esse layout, pretendo articular o artigo filho como uma na área rolável. Eu consigo isso com grid-template-rows O elemento do artigo recebe o valor 1fr, e o próprio formulário tem o mesmo valor máximo altura do elemento da caixa de diálogo. Definir essa altura e um tamanho de linha firmes é o que permite que o elemento de artigo seja limitado e role quando ultrapassar o limite:

dialog > form {
  display: grid;
  grid-template-rows: auto 1fr auto;
  align-items: start;
  max-block-size: 80vh;
  max-block-size: 80dvb;
}

Captura de tela do DevTools sobrepondo as informações de layout de grade sobre as linhas.

Estilizar a caixa de diálogo <header>

A função desse elemento é fornecer um título para o conteúdo do diálogo e oferecer um botão "Fechar" fácil de encontrar. Ele também recebe uma cor de superfície para fazer parecer estejam por trás do conteúdo do artigo no diálogo. Esses requisitos levam a um flexbox contêineres, itens alinhados verticalmente que são espaçados em suas bordas e alguns padding 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);
  }
}

Captura de tela do Chrome Devtools sobrepondo informações de layout flexbox no cabeçalho da caixa de diálogo.

Definir o estilo do botão "Fechar" do cabeçalho

Como a demonstração usa os botões "Abrir objetos", o botão "Fechar" é personalizado em um botão redondo centralizado, 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;
}

Captura de tela do Chrome Devtools sobrepondo informações de dimensionamento e padding no botão &quot;Fechar&quot; do cabeçalho.

Estilizar a caixa de diálogo <article>

O elemento do artigo tem um papel especial nesse 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 propriamente dito, que fornecem restrições a esse elemento de artigo se ele for muito alta. Defina overflow-y: auto para que as barras de rolagem sejam mostradas apenas quando necessário. conter rolagem dentro dele com overscroll-behavior: contain, e o restante serão 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);
  }
}

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 em linha do rodapé, seguido de algum 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);
  }
}

Captura de tela do Chrome Devtools sobrepondo informações de layout flexbox no elemento de rodapé.

O menu é usado para conter os botões de ação para a caixa de diálogo. Ele usa um wrapper layout flexbox com gap para fornecer espaço entre os botões. Elementos do menu ter padding, como uma <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;
}

Captura de tela do Chrome Devtools sobrepondo informações de flexbox nos elementos do menu de rodapé.

Animação

Os elementos das caixas de diálogo geralmente são animados porque entram e saem da janela. Fazer aos diálogos um pouco de 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 vai alternar a propriedade display no elemento. Antes, o guia define exibição como grade e nunca a define como nenhuma. Isso desbloqueia a capacidade para dentro e para fora.

As propriedades abertas vêm com muitos frames-chave animações para uso, o que facilita orquestração fácil e legível. Aqui estão as metas de animação e as camadas abordagem que escolhi:

  1. O movimento reduzido é a transição padrão, com uma simples opacidade que aparece.
  2. Se o movimento estiver correto, serão adicionadas animações de escala e deslize.
  3. 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 o Open Props tenha frames-chave para aparecer e desaparecer, prefiro isso de transições em camadas como padrão, com animações de frame-chave como possíveis upgrades. Anteriormente, já estilizamos a visibilidade da caixa de diálogo com opacidade, orquestrando 1 ou 0, dependendo do atributo [open]. Para entre 0% e 100%, diga ao navegador por quanto tempo e que tipo easing gostaria de:

dialog {
  transition: opacity .5s var(--ease-3);
}

Adicionar movimento à transição

Se o usuário concordar com o movimento, tanto as caixas de diálogo mega quanto as mini devem deslizar como entrada e ampliar como saída. Você pode fazer isso com o Consulta de mídia prefers-reduced-motion e alguns cenários 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;
  }
}

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 dispositivos móveis dispositivos para serem mais como uma folha de ação, como se um pequeno pedaço de papel tivesse passado de baixo para cima na tela, ainda está fixado à parte de baixo. A escala a animação de saída não se encaixa bem nesse novo design, e podemos adaptá-lo com algumas consultas de mídia e algumas propostas 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á 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 luz (clicar na caixa de diálogo pano de fundo), animação e alguns eventos adicionais para ajustar o tempo de exposição 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 é que está sendo animado. A interação é alcançada observando-se os cliques na caixa de diálogo. de eventos e aproveitando os eventos borbulhante para avaliar o que foi clicado e só 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 obter informações sobre como o a caixa de diálogo foi fechada. Você verá que também forneci strings de fechamento sempre que chamo a função de vários botões, para fornecer contexto ao meu aplicativo sobre a interação do usuário.

Adicionando eventos fechados e fechados

O elemento da caixa de diálogo é apresentado com um evento de fechamento: ele é emitido imediatamente quando o a função close() da caixa de diálogo é chamada. Como estamos animando esse elemento, ele é é bom ter eventos para antes e depois da animação, para uma mudança capturar o ou redefinir o formulário da caixa de diálogo. Eu o uso aqui para gerenciar a adição do inert na caixa de diálogo fechada. Na demonstração, usamos esse atributo para modificar na lista de avatares se o usuário tiver enviado uma nova imagem.

Para isso, crie dois novos eventos chamados closing e closed. Depois, detectar o evento integrado de fechamento na caixa de diálogo. Aqui, defina a caixa de diálogo como inert e envie o evento closing. A próxima tarefa é aguardar animações e transições para que a execução seja concluída na caixa de diálogo e, em seguida, envie o 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 no módulo Como criar um aviso componente, retorna uma promessa com base na da animação e das promessas de transição. É por isso que dialogClose é um assíncrono function ele pode await a promessa retornada 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 forneça um evento aberto, como acontece com o fechamento. Eu uso um MutationObserver para fornecer insights sobre as mudanças de atributos na caixa de diálogo. Neste observador, Vou acompanhar as mudanças no atributo de abertura e gerenciar os eventos personalizados. de maneira adequada.

Assim como iniciamos os eventos de encerramento e fechamento, crie dois novos eventos. chamados opening e opened. Onde ouvimos anteriormente o fechamento da caixa de diálogo , desta vez use um observador de mutação criado para observar o evento atributos.


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 a caixa de diálogo atributos forem alterados, fornecendo a lista de alterações como uma matriz. Iterar ao longo o atributo muda, procurando o attributeName como aberto. Depois, verifique se o elemento tiver o atributo ou não: isso informa se a caixa de diálogo se tornou aberto. Se ela tiver sido aberta, remova o atributo inert, defina o foco a um elemento que solicita autofocus ou o primeiro elemento button encontrado na caixa de diálogo. Por último, semelhante à seção de encerramento e fechado, envie o evento de abertura imediatamente, aguarde as animações para finalizar e envie o evento aberto.

Adicionar um evento removido

Em aplicativos de página única, as caixas de diálogo costumam ser adicionadas e removidas com base em rotas ou outras necessidades e estados do aplicativo. Pode ser útil limpar eventos ou quando uma caixa de diálogo é removida.

É possível conseguir isso com outro observador de mutações. Desta vez, em vez de ao observar atributos em um elemento de diálogo, vamos observar os filhos do corpo e observar a remoção de elementos da 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ções é chamado sempre que filhos são adicionados ou removidos. no corpo do documento. As mutações específicas que estão sendo observadas são para removedNodes que tenham o nodeName de em uma caixa de diálogo. Se uma caixa de diálogo foi removida, os eventos de clique e fechamento são removidos para liberar memória, e o evento personalizado removido será enviado.

Como remover o atributo de carregamento

Para evitar que a animação da caixa de diálogo seja de saída quando adicionada ao na página ou no carregamento da página, um atributo de carregamento foi adicionado à caixa de diálogo. A O script a seguir espera que as animações da caixa de diálogo terminem de ser executadas, depois remove o atributo. Agora, a caixa de diálogo pode ser animada para dentro e para fora, e nós escondeu uma animação que não causaria distração.

export default async function (dialog) {
  
  await animationsComplete(dialog)
  dialog.removeAttribute('loading')
}

Saiba mais sobre o problema de impedir animações de frame-chave no carregamento da página aqui.

Tudo junto

Veja o 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 uma 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 são atualizadas com a dispensa de luz, animações carregar correções 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 é bom que a caixa de diálogo tenha concluído a animação de saída, e alguns scripts serão animados no novo avatar. Graças aos novos eventos, orquestrar a experiência do usuário pode ser mais suave.

Observe dialog.returnValue: contém a string de fechamento transmitida quando o evento da caixa de diálogo close() é chamado. No evento dialogClosed, é fundamental saber se a caixa de diálogo foi fechada, cancelada ou confirmada. Se estiver confirmado, o em seguida, pega os valores do formulário e redefine o formulário. A redefinição é útil, então quando a caixa de diálogo for mostrada novamente, ela estará em branco e pronta para um novo envio.

Conclusão

Agora que você sabe como eu fiz isso, o que você faria‽ 🙂

Vamos diversificar nossas abordagens e aprender todas as maneiras de criar na Web.

Crie uma demonstração, envie um tweet para mim e adicione os links acesse a seção "Remixes da comunidade" abaixo.

Remixes da comunidade

Recursos