3 pontos por GN⁺ 2024-12-08 | 1 comentários | Compartilhar no WhatsApp
  • O elemento <dialog> do HTML cria caixas de diálogo modais e não modais nativas do navegador, e a modal coloca o restante da página em estado inert, bloqueando a interação
  • As formas básicas de abrir e fechar são showModal(), show() e close(), e também é possível fazer controle declarativo com form, Invoker Commands API e Popover API
  • O atributo closedby divide as formas pelas quais o usuário pode fechar a caixa de diálogo em any, closerequest e none, e o comportamento padrão muda conforme a forma como ela foi aberta
  • Em acessibilidade, o foco inicial e um botão de fechar explícito são importantes, e caixas de diálogo abertas com showModal() fornecem por padrão foco no primeiro elemento focalizável e fechamento com a tecla Esc
  • A estilização usa :modal, :open e ::backdrop, e animações exigem tratamento de transições discretas como display, overlay, @starting-style e transition-behavior: allow-discrete

Função do <dialog> e comportamento modal

  • O elemento <dialog> do HTML serve para criar caixas de diálogo modais e não modais
  • A caixa de diálogo modal bloqueia a interação com outros elementos da UI e coloca o restante da página em estado inert
  • A caixa de diálogo não modal pode continuar interagindo com o restante da página mesmo quando está aberta

Estado de abertura e política de fechamento

  • <dialog> inclui atributos globais, mas o atributo tabindex não deve ser usado
    • O próprio <dialog> não é um elemento interativo e não recebe foco
    • O conteúdo interno e o botão de fechar podem receber foco e ser usados normalmente
  • O atributo open indica que a caixa de diálogo está ativa e pode ser usada
    • Sem o atributo open, ela não fica visível para o usuário
    • Para exibir a caixa de diálogo, é recomendado usar .show() ou .showModal() em vez do atributo open
    • Um <dialog> aberto com o atributo open é não modal
    • É possível alternar o estado aberto/fechado de uma caixa de diálogo não modal com o atributo open, mas isso não é recomendado
  • O atributo closedby especifica de que forma o usuário pode fechar a caixa de diálogo
    • any: pode ser fechada das três formas
      • Light dismiss, como clicar ou tocar fora da caixa de diálogo
      • Ações da plataforma, como tecla Esc, voltar no mobile ou gesto de dispensar
      • Formas definidas pelo desenvolvedor, como um botão que chama HTMLDialogElement.close() ou um envio de form
    • closerequest: pode ser fechada por ações da plataforma e por formas definidas pelo desenvolvedor
    • none: só pode ser fechada por formas definidas pelo desenvolvedor
    • Se não houver um valor closedby válido, o comportamento será como closerequest quando aberta com showModal(), e como none nos demais casos

JavaScript e controle declarativo

  • É possível controlar diretamente a exibição e o fechamento de <dialog> com JavaScript
    • showModal(): exibe uma caixa de diálogo modal
    • show(): exibe uma caixa de diálogo não modal
    • close(): fecha a caixa de diálogo
    • Também é possível fechar enviando um form dentro de <dialog> com method="dialog"
    • Caixas de diálogo modais também podem ser fechadas com a tecla Esc
  • A Invoker Commands API permite abrir e fechar caixas de diálogo modais apenas com atributos no botão
    • Define-se commandfor e command em <button>
    • Os comandos disponíveis para a caixa de diálogo são "show-modal", "close" e "request-close"
Open dialog

  This dialog was opened using an invoker command.

  Close

  • A Popover API permite abrir, fechar e alternar caixas de diálogo não modais de forma declarativa
    • Adiciona-se o atributo popover a <dialog> para transformá-lo em um popover
    • Define-se popovertarget e popovertargetaction em um botão ou elemento de entrada
    • Como o popover dialog é não modal, ele pode ser fechado clicando fora
    • Se nenhum valor for definido para popover, o padrão "auto" será usado, ativando o light dismiss
    • popover="manual" desativa o light dismiss, exigindo outra forma de fechamento, como um botão
    • Se popovertargetaction for omitido, o padrão toggle será usado

Envio de formulário e tratamento de fechamento

  • Todo <dialog> precisa de um mecanismo de fechamento que funcione mesmo em dispositivos sem teclado físico
  • Há várias formas de fechar
    • Enviar um form interno com method="dialog"
    • Clicar fora da caixa de diálogo quando o light dismiss estiver ativado
    • Pressionar Esc em caixas de diálogo onde isso é permitido
    • Chamar HTMLDialogElement.close()
  • formmethod="dialog" em <form> ou no botão de envio fecha a caixa de diálogo
    • O estado dos controles do formulário é preservado, mas o formulário não é enviado
    • returnValue recebe o valor do botão ativado
  • Em formulários com campos obrigatórios, o user agent impede que a caixa de diálogo seja fechada por envio normal até que os valores sejam fornecidos
    • É possível contornar a validação do formulário usando formnovalidate no botão de fechar
    • Também é possível fechar com dialog.close() em JavaScript

Foco e acessibilidade

  • Ao abrir <dialog> com showModal(), o foco é colocado por padrão no primeiro elemento focalizável aninhado
  • Para definir a posição inicial de foco mais adequada em uma caixa de diálogo específica, pode-se usar o atributo autofocus
    • Se não houver um elemento para interação imediata, é recomendado colocar autofocus no botão de fechar
    • Se a posição inicial de foco for incerta, como em caixas de diálogo renderizadas dinamicamente, o próprio <dialog> pode ser um ponto inicial apropriado
  • A forma mais robusta de garantir que todos os usuários possam fechar a caixa de diálogo é incluir botões explícitos, como confirmar, cancelar ou fechar
  • Caixas de diálogo abertas com showModal() podem ser fechadas com Esc por padrão
    • Caixas de diálogo não modais não são fechadas com Esc por padrão
    • Usuários de teclado esperam conseguir fechar caixas de diálogo modais com Esc
    • Se houver várias caixas de diálogo modais abertas, a tecla Esc deve fechar apenas a última exibida, e o navegador fornece esse comportamento ao usar <dialog>
  • O <dialog> nativo oferece recursos de usabilidade e acessibilidade que precisariam ser reproduzidos manualmente em diálogos customizados feitos com outros elementos
  • O navegador expõe <dialog> de forma semelhante a um diálogo customizado com ARIA role="dialog"
    • Um <dialog> aberto com showModal() tem implicitamente aria-modal="true"
    • Um <dialog> exibido com show(), com o atributo open ou alterando o display padrão é exposto com aria-modal="false"
    • Ao implementar uma caixa de diálogo modal, tudo fora de <dialog> e seu conteúdo deve ser renderizado como inert, e o navegador fornece esse comportamento ao usar showModal()

Estilização em CSS e backdrop

  • <dialog> pode ser selecionado pelo nome do elemento como qualquer elemento comum
  • Para estilização baseada em estado, podem ser usadas as pseudoclasses :modal e :open
  • O plano de fundo da caixa de diálogo modal pode ser estilizado com o pseudoelemento ::backdrop
    • Ele é aplicado ao backdrop que aparece atrás de <dialog> quando a caixa de diálogo é exibida com showModal()
    • Pode ser usado para desfocar, escurecer ou cobrir o conteúdo ao fundo em estado inert

Padrões de uso vistos em exemplos

  • O exemplo da Invoker Commands API abre a caixa de diálogo com um botão command="show-modal" e fecha com um botão command="close"
    • Pode ser aberta com o botão “Open dialog”
    • Pode ser fechada com o botão “Close” ou com a tecla Esc
  • O exemplo da Popover API abre e fecha uma caixa de diálogo não modal com popover, popovertarget e popovertargetaction
    • Pode ser fechada com o botão “Close”, com Esc ou selecionando fora da caixa de diálogo
    • Ao usar popover="manual", o light dismiss é desativado
  • O exemplo com o atributo open cria uma caixa de diálogo não modal apenas em HTML, já aberta ao carregar a página
    • Pode ser fechada com o botão “OK” em <form>
    • Não há forma de reabri-la depois de fechada
    • Para exibir uma caixa de diálogo não modal, prefere-se usar HTMLDialogElement.show()
  • O exemplo de caixa de diálogo modal usa .showModal() para abrir e close() para fechar
    • O ::backdrop é estilizado com um fundo em gradiente
    • Quando a caixa de diálogo está aberta, a área fora dela entra em estado inert e não é possível interagir com o documento
  • O exemplo de returnValue trata o valor de retorno da caixa de diálogo com formulário e valores de botão
    • O returnValue padrão é uma string vazia ou o valor do botão que enviou o formulário interno da caixa de diálogo
    • O botão “Confirm” passa o valor selecionado para close() e o usa como valor de retorno
    • Se a caixa for fechada com Esc, returnValue não é atualizado e o evento close também não ocorre, então o texto de <output> não é atualizado

Animações de <dialog>

  • <dialog> oculto usa display: none, e <dialog> visível usa display: block
  • Quando o estado de exibição muda, ele é adicionado ou removido da top layer e da árvore de acessibilidade
  • Para animar <dialog>, a propriedade display precisa ser animável
    • Navegadores com suporte tratam display como uma animação discreta
    • Ao mudar de none para block, a troca para block acontece no ponto 0%, então ele permanece visível durante toda a animação
    • Ao mudar de block para none, a troca para none acontece no ponto 100%, então ele permanece visível durante toda a animação
  • Animação com CSS transition

    • Para animar <dialog> com CSS transition, os seguintes recursos são necessários
    • @starting-style: fornece o valor inicial da transição toda vez que a caixa de diálogo é aberta
    • Transição de display: mantém a caixa de diálogo no valor visível de display durante a transição
    • Transição de overlay: atrasa a remoção da top layer até o fim da transição
    • transition-behavior: allow-discrete: ativa transições discretas para display e overlay, que normalmente não são animados
    • Em navegadores sem suporte à pseudoclasse :open, é possível estilizar o estado aberto com o seletor de atributo dialog[open]
    • Como <dialog> muda de display: none para display: block sempre que é exibido, cada transição de entrada vai de @starting-style para o estilo dialog:open
  • Animação com keyframes

    • Animações CSS com keyframes têm restrições diferentes das transitions
    • Não fornecem @starting-style
    • Incluem o valor de display dentro do keyframe
    • Não exigem ativação explícita de allow-discrete
    • Também não é necessário definir overlay no keyframe
    • O fade-out do backdrop não pode ser animado ao fechar, porque o backdrop é removido imediatamente do DOM quando a caixa de diálogo é fechada

Resumo técnico

  • Categoria de conteúdo: flow content, sectioning root
  • Conteúdo permitido: flow content
  • As tags de abertura e fechamento são obrigatórias, sem omissão de tags
  • Pai permitido: qualquer elemento que aceite flow content
  • Papel ARIA implícito: dialog
  • Papel ARIA permitido: alertdialog
  • Interface DOM: HTMLDialogElement
  • Especificação: HTML # the-dialog-element

1 comentários

 
GN⁺ 2024-12-08
Comentários do Hacker News
  • Elementos HTML interativos incluem seletor de arquivos, seletor de cores, seletor de data/hora, controle deslizante numérico, opções sugeridas em campos de texto, resumo/detalhes expansíveis, FAQ e players de mídia com controles
    Esses elementos são bons por serem leves e semânticos, e <details> que compartilham o mesmo name também podem ter o comportamento de fechar o item anterior quando um novo é aberto

    • Hoje em dia, no fim das contas, o comportamento não é previsível o suficiente entre navegadores ou versões, então esses elementos muitas vezes acabam sendo substituídos por toolkits
    • Players de mídia com controles têm aparência completamente diferente em cada navegador, e não dá para estilizar sem JavaScript
      Seria bom se fossem melhor implementados
  • Uso <dialog> desde 2019 e, mesmo que Firefox e Safari tenham levado alguns anos a mais para dar suporte, a qualidade do polyfill do Google era boa o suficiente para usá-lo em produção em um SaaS corporativo sem problemas
    O ponto mais decepcionante é que o elemento quase não tinha estilo. Era só uma borda preta grossa bem básica, com cantos retos, o que foi diferente da expectativa de que o navegador seguisse as convenções do sistema operacional para posicionamento de diálogos/janelas e fizesse com que parecesse um diálogo nativo do macOS, iOS ou Windows
    Como o elemento aberto fica em uma camada superior separada, eu esperava que ele também pudesse sair para fora do viewport do navegador, mas essa expectativa também foi frustrada. Do ponto de vista do usuário, ninguém quer um pop-up ou diálogo modal imóvel que cubra completamente o conteúdo do documento
    Os fornecedores de navegadores parecem não querer que se use alert(), prompt() e confirm() porque eles bloqueiam o JavaScript/a thread principal, mas não ofereceram uma API substituta igualmente simples e eficaz, que não exija habilidade de design de UI. Em vez de tentar convencer que <dialog> é melhor em todos os casos, seria bom oferecer uma API de alert/prompt/confirm não bloqueante baseada em Promise

    • Sair para fora do viewport do navegador e ter estilo de diálogo do sistema é um recurso que provavelmente os desenvolvedores querem mais do que usuários ou fabricantes de navegadores
      O fato de os novos recursos não se comportarem como a antiga família alert() não é uma decepção acidental; também há vantagens nisso
      Ainda assim, o estilo padrão de <dialog> precisava de um pouco mais de atenção, e teria sido muito melhor se houvesse ao menos um estilo padrão que combinasse com o estilo nativo do navegador, mesmo sem parecer e se comportar 100% como um diálogo de sistema fora do DOM
      Para web apps com mais permissões do que páginas comuns, como PWAs, essa direção poderia ser mais bem aceita, como recursos separados de estilização de janelas ou interação com o sistema
    • Se um fornecedor fizer <dialog> parecer uma janela nativa do navegador, isso pode borrar a line of death
      Fica mais fácil para um site malicioso exibir um pop-up de “atualização do navegador” no meio da página e redirecionar para uma página de download do Chrome convincente, fazendo o usuário baixar um executável adulterado
      https://textslashplain.com/2017/01/14/the-line-of-death/
    • Um modal que bloqueia o foco de toda a janela do navegador não é uma boa ideia
      Usuários mantêm várias abas abertas, e outra aba pode conter informações necessárias para concluir o diálogo
      Também é preciso ter muito cuidado com quanto controle visual será dado ao diálogo em si. Especialmente se ele puder parecer o sistema operacional, já há muita gente que cai em alertas falsos de vírus no navegador; um diálogo que pareça real e exija interação pode aumentar muito a taxa de sucesso dos ataques
    • O fato de quase não ser estilizado parece ser o principal motivo que impede o uso amplo de componentes embutidos do navegador
      A maioria das empresas em que trabalhei tinha algum grau de linguagem de design consistente, e, independentemente do que digam os defensores de HTML/CSS puro, usar componentes embutidos do navegador sem alterações quase sempre parecia amador e deslocado
      Se esse problema não for resolvido, será difícil haver adoção ampla
    • Estou testando a ideia de fazer substitutos para alert(), prompt() e confirm() funcionarem como await Prompts.alert("This is an alert message!");, await Prompts.confirm(...), await Prompts.prompt(...)
      Demo: https://tools.simonwillison.net/prompts-js
      O código foi escrito pelo o1: https://chatgpt.com/share/67539c28-4df0-8006-b021-4f468e011f...
  • Escrevi um post chamado “The HTML dialog element API is a mess”: https://lapcatsoftware.com/articles/2024/2/1.html

    • Não acho que muita gente diria que os padrões Web são excelentes, bem projetados e bem mantidos
      Ainda assim, o valor está em serem padrões. A Web é ótima porque funciona em qualquer lugar, e inevitavelmente acaba sendo bagunçada
      Nesse contexto, vejo o <dialog> como um sucesso, especialmente em ferramentas internas de administração. Não quero me preocupar com a última moda de front-end; só quero economizar espaço na tela e exibir conteúdo como uma sobreposição modal sobre a visualização principal
    • Na prática, parece muito improvável abrir a mesma caixa de diálogo como modal duas vezes
      Para abrir uma caixa de diálogo é necessária uma entrada do usuário, e uma caixa de diálogo modal bloqueia a entrada do usuário; então, para isso acontecer, uma entrada dentro da própria caixa de diálogo teria de abrir a mesma caixa de diálogo de novo
      Se você abrir a caixa de diálogo por uma operação assíncrona, precisa acompanhar o que está aberto e fechado, e algo parecido acontece até em ambientes como Qt
    • Eu esperava que o <dialog> fosse uma versão reforçada de await confirm/prompt, customizável, que não mostrasse alertas diferentes em cada navegador quando aberta várias vezes
      Mas, na prática, ele está mais para uma div glorificada
    • O Google Chrome também criou o URLPattern, e eu esperava que Chrome e Safari não dessem suporte a ele
      A Compression Streams API não era ruim, mas era uma API bem pequena
      Pelo padrão, parece que o Google não lida bem com experiência do usuário e experiência do desenvolvedor
      Além disso, fui procurar a posição dos padrões e vi que ambos dão suporte a URLPattern
  • Independentemente da implementação, acho que foi um passo na direção certa
    Também há uma proposta em andamento que parece uma versão aprimorada do <select>: https://open-ui.org/components/combobox.explainer/
    Para notificações toast, a Popover API já chegou aos navegadores: https://mdn.github.io/dom-examples/popover-api/
    Também existe uma proposta de popover hint para tooltips: https://open-ui.org/components/popover-hint.research.explain...

  • Gosto do elemento <dialog>, especialmente das considerações de acessibilidade incorporadas e padronizadas
    Estou ansioso pelo dia em que poderei usá-lo sem polyfill quando Safari abaixo da versão 15.4 sair dos critérios de suporte
    Mas minha maior reclamação é a dependência de JavaScript. Quase todos os visitantes de um site vêm com JavaScript ativado, mas também sinto certa satisfação em fazer com que isso não seja necessário
    Não sei por que CSS ou um botão de destino não podem controlar o estado aberto da caixa de diálogo, e gostaria de saber se eu estiver errado

    • Por isso, costumo usar um elemento customizado com o atributo popover=""
      Ele pode ser facilmente usado como alvo por um botão, não precisa de JavaScript no lado do cliente, e também pode ser fechado ao clicar fora. Esse é um comportamento que o <dialog> não tem por padrão
      Não tenho certeza quanto à acessibilidade, mas imagino que não seja pior do que as abordagens antigas com labels/checkboxes/elementos de formulário ocultos; é muito mais simples e parece menos uma gambiarra
    • Isso deve se tornar possível com invokers
      https://css-tricks.com/invoker-commands-additional-ways-to-w...
  • Seria bom se HTML desse suporte ao conceito de uma tag como <page>, capaz de definir várias páginas dentro de um único arquivo e mostrar uma por vez
    Sem parecer uma caixa de diálogo, e permitindo que cada PAGE puxasse seções comuns, como cabeçalho, barra lateral e rodapé dentro da mesma página, conforme o estado selecionado
    Hoje dá para fazer isso escondendo e mostrando divs, mas dar suporte a essa abordagem com uma tag HTML específica poderia ajudar

    • Acho que recentemente houve melhorias nas folhas de estilo para impressão que permitem algo desse tipo, mas, pelo que sei, ainda não existe para exibição na tela
  • Tentei usar isso hoje e encontrei um problema que não consegui contornar
    Quando há um formulário dentro da caixa de diálogo, se você pressiona Enter com o foco em um campo de entrada, ou pressiona Space com o foco no botão de envio, a caixa de diálogo é fechada junto com o envio do formulário
    Não encontrei uma forma limpa de impedir isso. Normalmente um formulário recarrega a página, então talvez não seja um problema comum, mas aqui eu estava usando htmx

    • É bem provável que a última frase esteja certa. Por padrão, formulários enviam uma requisição de rede
      Uso um editor em que um formulário dentro de <dialog> atualiza um iframe, e apenas o iframe de destino é recarregado; a caixa de diálogo não fecha
      Também não consegui reproduzir um caso em que a caixa de diálogo fechasse ao pressionar Enter. Deveria ser igual a submit, e submit também não fecha a caixa de diálogo
      A propósito, aprendi ontem que botões podem fechar uma caixa de diálogo
    • Não bastaria usar preventDefault e stopPropagation?
    • Talvez seja melhor reportar um bug ao HTMX
  • Enquanto procurava um gerenciador de consentimento de cookies para colocar numa build recém-otimizada, não gostei de ver que as opções open source passavam de 100 KB, então fiz o meu próprio: https://github.com/replete/biscuitman
    Para deixá-lo o menor possível, dependi do <dialog>, e com apenas algumas regras CSS ele funciona de forma nativa mesmo sem estilo
    No fim, também acabei escrevendo uma ferramenta de build que compila até para IE11 e versões realmente antigas de navegadores
    O <dialog> funciona bem no geral e, em navegadores antigos, exige alguns truques de CSS, mas não é difícil de lidar. É uma boa adição à plataforma Web, mas, depois de 20 anos fazendo isso, não quero ter de recriar controles customizados de seleção múltipla a cada poucos anos. Controles nativos são bons

  • O comportamento comum de fechar na maioria dos exemplos não funciona no Firefox para Android

    • Ontem, ao usar em um projeto, o atributo autofocus do botão de fechar não funcionou
      No fim, toda vez que chamava show(), adicionei uma linha como $('#thatModal *[autofocus]').focus()
      Segundo a MDN, por padrão deveria funcionar como esperado
    • Você poderia explicar melhor o que quer dizer com “fechar comum na maioria dos exemplos”? Todos os exemplos que vi tinham um trecho de JavaScript adicionando um event listener ao botão de fechar, e funcionavam bem no Firefox para Android
    • É igual no Chrome do Windows 10
      Parece que só os listeners adicionados via JavaScript funcionam corretamente