// html · Web Platform Advent #18
O elemento HTML dialog: modais reais sem biblioteca
O elemento HTML dialog oferece um modal de verdade: camada superior, página inerte, backdrop e tratamento do Esc, apenas com marcação simples. Veja showModal vs show, form method=dialog, returnValue, o atributo closedby e o hábito a evitar.
Todos os programadores front-end já construíram um modal à mão: uma div com posição fixa, uma guerra de z-index, um listener de clique no exterior, um tratador do Esc e alguns malabarismos com o foco para que o teclado não se perca por trás da sobreposição. O elemento <dialog> substitui tudo isso por uma etiqueta e uma chamada de método. Eis o que ele faz na realidade, e os dois ou três detalhes que decidem se a sua caixa de diálogo se comporta como deve.
A caixa de diálogo funcional mais pequena
Uma caixa de diálogo é marcação inerte até que a abra a partir do JavaScript:
<dialog id="confirm">
<p>Eliminar este ficheiro?</p>
<button autofocus>Cancelar</button>
</dialog>
<button id="open">Eliminar</button> const dialog = document.getElementById("confirm");
document.getElementById("open")
.addEventListener("click", () => dialog.showModal()); Essa única chamada a showModal() faz consideravelmente mais do que tornar um elemento visível.
showModal() versus show(): a diferença que conta
Estes dois métodos não são variações sobre o mesmo tema. Produzem coisas genuinamente diferentes.
O showModal() abre uma caixa de diálogo modal. O navegador coloca-a na camada superior, por isso ela é desenhada acima de toda a página sem envolver qualquer z-index. Desenha um pseudo-elemento ::backdrop por trás dela. Torna todo o resto inerte, o que significa que o resto do documento não pode ser clicado nem alcançado com o tabulador. E define implicitamente aria-modal="true".
O show() abre uma caixa de diálogo não modal. A página continua interativa, não há camada superior, não há backdrop, e aria-modal é false.
A inércia é a parte que as pessoas subestimam. Tornar o fundo não clicável é fácil; torná-lo inacessível ao tabulador é a parte que os modais feitos à mão quase sempre erram, e aqui o navegador trata disso por si.
O hábito a abandonar: o atributo open
Um <dialog> tem também um atributo booleano open, e recorrer a ele é o erro mais comum com este elemento.
<!-- funciona, mas isto NÃO é um modal -->
<dialog open>Sou não modal, independentemente do que esperava.</dialog> Uma caixa de diálogo mostrada através do atributo open é sempre não modal. Sem camada superior, sem backdrop, sem página inerte. A MDN recomenda usar show() ou showModal() em vez do atributo, e descreve alternar open à mão como não sendo a prática recomendada. Se o seu modal parece correto mas a página por trás dele continua a responder aos cliques, é quase de certeza por isto.
form method="dialog": a funcionalidade sem equivalente
Esta é a parte do <dialog> que nada mais replica, e é a razão pela qual uma caixa de diálogo de confirmação pode não precisar de JavaScript algum para além do que a abre.
<dialog id="confirm">
<form method="dialog">
<p>Eliminar este ficheiro?</p>
<button value="cancel" autofocus>Cancelar</button>
<button value="delete">Eliminar</button>
</form>
</dialog> Quando um formulário dentro de uma caixa de diálogo é submetido com method="dialog", a caixa fecha-se em vez de enviar um pedido. Os estados dos controlos do formulário são guardados mas não submetidos, e o returnValue da caixa de diálogo passa a ter o valor do botão que foi ativado.
Depois basta ler a resposta:
dialog.addEventListener("close", () => {
if (dialog.returnValue === "delete") {
// o utilizador confirmou
}
}); Dois botões, um atributo, e fica a saber qual foi carregado. Sem tratadores de clique, sem invólucro de promessa, sem variável de estado.
A armadilha dos campos obrigatórios
Um comportamento surpreende as pessoas à primeira vez e vale a pena conhecê-lo antes que lhe custe uma tarde.
Se a caixa de diálogo contiver um formulário com campos obrigatórios, a submissão continua a executar a validação. Um botão simples não consegue fechar a caixa enquanto esses campos forem inválidos, o que significa que o seu botão Cancelar deixa de funcionar precisamente quando o utilizador mais precisa dele. As respostas documentadas são adicionar formnovalidate ao botão que dispensa a caixa, ou chamar dialog.close() a partir do seu próprio código:
<button value="cancel" formnovalidate>Cancelar</button> O Esc e o atributo closedby
A dispensa pelo teclado não é uniforme, e a diferença segue a divisão entre modal e não modal.
Uma caixa de diálogo aberta com showModal() pode ser dispensada com Esc por predefinição. Uma caixa de diálogo não modal não se dispensa com Esc por predefinição.
O atributo closedby torna isto explícito em vez de implícito:
<dialog closedby="any"> <!-- Esc, clique no exterior, ou o seu código -->
<dialog closedby="closerequest"> <!-- Esc ou o seu código -->
<dialog closedby="none"> <!-- apenas o seu código --> O closedby="any" é o que lhe dá o light-dismiss, o comportamento de clique no exterior que normalmente se escreve à mão. O closedby="none" destina-se à rara caixa de diálogo que não pode ser dispensada acidentalmente, e traz consigo uma obrigação: tem de fornecer uma saída visível.
Acertar na acessibilidade
O elemento entrega-lhe a maior parte disto, mas três pontos ficam do seu lado.
Use o autofocus de forma deliberada. Com showModal(), o foco recai sobre o primeiro elemento focável aninhado. Muitas vezes não é aquele que quer. Coloque autofocus no elemento com que o utilizador deve interagir de imediato, ou no botão de fechar se nada mais for mais urgente.
Forneça sempre um botão de fechar. A forma mais robusta de garantir que todos os utilizadores conseguem sair da caixa de diálogo é um botão explícito, não apenas o Esc ou um clique no exterior.
Não coloque tabindex na própria caixa de diálogo. Ela não é interativa e não recebe o foco. O seu conteúdo é que recebe.
Estilizar o backdrop
A camada escurecida por trás de um modal é um pseudo-elemento real que pode estilizar:
dialog::backdrop {
background: rgb(0 0 0 / 0.5);
backdrop-filter: blur(2px);
} Só existe para caixas de diálogo abertas com showModal(), o que é outra forma de verificar de relance se está mesmo em modo modal.
dialog ou popover?
Ambos são desenhados na camada superior, por isso a questão surge constantemente. A distinção tem a ver com a interrupção.
Use <dialog> com showModal() quando o utilizador tem de resolver algo antes de continuar: uma confirmação, uma escolha obrigatória, um formulário que bloqueia o fluxo. Bloquear a página é precisamente o objetivo.
Use a Popover API para interfaces de sobreposição transitórias que não devem interromper: menus, tooltips, cartões, notificações. Aí a página deve continuar utilizável.
Um teste útil: se fosse errado o utilizador ignorá-lo e seguir em frente, é uma caixa de diálogo. Se ignorá-lo for uma resposta perfeitamente razoável, é um popover.
Em resumo
O elemento <dialog> transforma um pedaço genuinamente difícil de interface em marcação. Abra-o com showModal() e obtém a camada superior, um backdrop, uma página inerte e o tratamento do Esc sem escrever nada disso. Use form method="dialog" e returnValue e obtém a resposta do utilizador sem um único tratador de clique. Evite o atributo open, lembre-se do formnovalidate no seu botão de cancelar, e coloque o autofocus você mesmo.
O que fica é uma caixa de diálogo de confirmação em cerca de quinze linhas de HTML que se comporta corretamente para utilizadores de teclado e de leitores de ecrã, o que é mais do que a maioria das que foram construídas à mão e que ela substitui.
Perguntas frequentes
- Qual é a diferença entre show() e showModal()?
- O showModal() abre uma caixa de diálogo modal: o navegador coloca-a na camada superior, desenha um ::backdrop por trás dela, torna o resto da página inerte para que nada no exterior possa ser clicado ou alcançado com o tabulador, e define implicitamente aria-modal="true". O show() abre uma caixa de diálogo não modal: o resto da página continua interativo, não há camada superior nem backdrop, e aria-modal é false. Se quer um modal a sério, showModal() é o método certo.
- Devo abrir uma caixa de diálogo com o atributo open?
- Não. A MDN afirma claramente que é recomendado usar o método show() ou showModal() em vez do atributo open. Uma caixa de diálogo mostrada através do atributo open é sempre não modal, por isso não obtém nada da camada superior, do backdrop ou da inércia que provavelmente pretendia. Alternar o atributo à mão é explicitamente descrito como não sendo a prática recomendada.
- O que faz o form method="dialog"?
- Um formulário dentro de uma caixa de diálogo com method="dialog" fecha a caixa quando é submetido, em vez de enviar um pedido. Os estados dos controlos do formulário são guardados mas não submetidos, e a propriedade returnValue da caixa de diálogo passa a ter o valor do botão que foi ativado. Isso diz-lhe que botão o utilizador carregou sem qualquer JavaScript.
- A tecla Esc fecha uma caixa de diálogo?
- Para um modal aberto com showModal(), sim: pode ser dispensado com Esc por predefinição. Para uma caixa de diálogo não modal, não, o Esc não a dispensa por predefinição. O atributo closedby permite alterar isto: closerequest permite o Esc, any permite também um clique no exterior, e none significa que só o seu próprio código a pode fechar.
- Porque é que a minha caixa de diálogo não fecha quando o formulário tem um campo obrigatório?
- Porque o formulário continua a executar a validação. Se a caixa de diálogo contiver campos obrigatórios, um botão de submissão simples não a consegue fechar enquanto esses campos forem inválidos. Adicione formnovalidate ao botão destinado a dispensar a caixa de diálogo, ou chame dialog.close() a partir do seu próprio código.