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.
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:
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>
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.
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
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;
}
}
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:
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;
}
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:
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;
}
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);
}
}
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;
}
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);
}
}
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 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);
}
}
Definir o estilo do menu de rodapé da caixa de diálogo
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;
}
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:
- 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 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
- @GrimLink com um 3 em 1 caixa de diálogo.
- @mikemai2awesome com uma boa
remix que não muda
display
. - @geoffrich_ com Bonito e bom Polimento Svelte FLIP.
Recursos
- Código-fonte no GitHub
- Avatares do Doodle