Modais React com semântica de diálogo e diálogos in-app (v3.22.0) - #79
Merged
Merged
Conversation
As cinco modais React — StepDrawer, StepDatesModal, NewProcessModal,
ExportModal e ServiceStepsModal — eram <div> sobrepostos e nada mais: sem
`role`, o leitor de ecrã continuava a anunciar a página por trás; o Tab saía
para os controlos escondidos; e o Escape não fechava nenhuma delas. O padrão
já existia no repositório — o `modalAccessibility()` do bloco imperativo e o
overlay de arranque, que é o único que usa `inert` — e passa a ser um hook,
`useModalA11y`, em vez de uma terceira implementação a divergir das outras.
As modais são agora renderizadas num portal para o <body>. Sem isso, marcar
`#view-tracking` como inerte tornaria inerte também a modal, que é
renderizada lá dentro.
Os 28 `alert()`/`confirm()` nativos (13 e 15) passam a duas funções, usadas
tanto pelo bloco React como pelo editor de workflow imperativo:
await askConfirm(mensagem, { titulo, confirmar, cancelar, tom })
notify(mensagem, tom)
A interface é o `DialogHost`, montado uma vez pelo `App`, que se regista em
`window.__SUPERMERCADOS_confirm`/`__SUPERMERCADOS_notify`. Fica um recurso ao
diálogo nativo para a janela em que o host ainda não montou — nunca a assumir
«sim», porque quase todas estas perguntas protegem uma eliminação. Nas
perguntas o foco arranca no «Cancelar»: ao contrário do `confirm()` nativo, um
Enter dado à pressa não elimina nada.
Dezanove chamadas estavam em código síncrono (`if(!confirm(...)) return;`), o
que obrigou a tornar `async` as funções do editor de templates, os controlos
de fase e de passo, e o `dragend` da reordenação.
Dois defeitos encontrados no ponto em que os dois mundos se encontram, ambos
com teste de regressão:
· o Escape que abre uma pergunta era apanhado pela própria pergunta — o
React monta-a no microtask seguinte, ainda durante o despacho do mesmo
evento, entre um listener do `document` e o outro —, pelo que ela aparecia
e desaparecia no mesmo instante, sempre respondida «não». Resolvido
comparando o `timeStamp` do evento com o instante de abertura da modal;
· o Escape sobre uma pergunta aberta por cima de uma modal imperativa
accionava também o fecho dessa modal, que tornava a abrir a pergunta.
Catorze testes de browser novos cobrem semântica, prisão do Tab, inércia do
resto da página, devolução do foco, ausência de diálogos do browser e as duas
regressões acima.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwDMNpJzcA3Z2nP3jjwuJd
qzte
marked this pull request as ready for review
September 14, 2026 20:06
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fecha os pontos #7 (acessibilidade das modais React) e #8 (substituir
alert()/confirm()) do plano de melhorias.#7 — As cinco modais React passam a diálogos a sério
StepDrawer,StepDatesModal,NewProcessModal,ExportModaleServiceStepsModaleram<div>sobrepostos e nada mais: semrole, o leitor de ecrã continuava a anunciar a página por trás; o Tab saía para os controlos escondidos; e o Escape não fechava nenhuma delas.O padrão já existia no repositório, em dois sítios — o
modalAccessibility()do bloco imperativo e o overlay do ecrã de arranque, que é o único que usainert. Passa a ser um hook,useModalA11y, em vez de uma terceira implementação a divergir das outras duas:role="dialog",aria-modal,aria-labelledbya apontar para o título que a modal já tem, foco inicial lá dentro, prisão do Tab, Escape, foco devolvido a quem abriu, einert+aria-hiddenno resto da página.As modais passam a ser renderizadas num portal para o
<body>. Sem isso, marcar#view-trackingcomo inerte tornaria inerte também a modal, que é renderizada lá dentro. Uma pilha (MODAL_STACK) trata o caso de uma modal abrir outra — oStepDrawerabre oStepDatesModal— que sem ela fechava as duas com um só Escape.#8 — 28
alert()/confirm()nativos substituídos13
alerte 15confirmdão lugar a duas funções, definidas no bloco imperativo e usadas também pelo bloco React:await askConfirm(mensagem, { titulo, confirmar, cancelar, tom })notify(mensagem, tom)A interface é o
DialogHost, montado uma vez peloApp, que se regista emwindow.__SUPERMERCADOS_confirmewindow.__SUPERMERCADOS_notify— é assim que o editor de workflow, que é código imperativo, usa os mesmos diálogos. Fica um recurso ao diálogo nativo para a janela em que o host ainda não montou; nunca a assumir «sim», porque quase todas estas perguntas protegem uma eliminação.Nas perguntas o foco arranca no Cancelar: ao contrário do
confirm()nativo, um Enter dado à pressa não elimina nada.Dezanove das chamadas estavam em código síncrono (
if(!confirm(...)) return;), o que obrigou a tornarasyncas funções do editor de templates, os controlos de fase e de passo, e odragendda reordenação.Dois defeitos encontrados pelo caminho
Ambos no ponto em que os dois mundos se encontram — uma pergunta React aberta a partir de uma modal imperativa — e ambos com teste de regressão:
documente o outro; o segundo listener via-a já montada e fechava-a com o mesmo evento que a abriu. A pergunta aparecia e desaparecia no mesmo instante, sempre respondida «não». Resolvido comparando otimeStampdo evento com o instante em que a modal entrou na pilha.Testes
14 testes de browser novos, em dois grupos: semântica e
role, portal para o<body>, inércia do resto da página, prisão do Tab (40 tabulações), Escape e devolução do foco, ausência de qualquer diálogo do browser (page.on('dialog')tem de ficar vazio), o percurso completo de eliminar um processo, os avisos in-app, e as duas regressões acima.npm run cipassa (86 testes unitários, lint, validação, build). Nos testes de browser passam 46; as 6 falhas são todasERR_CERT_AUTHORITY_INVALIDno Google Fonts — artefacto do proxy TLS do ambiente onde isto foi desenvolvido, presente de igual modo emmain(4 falhas lá, com menos testes).Notas
alert()/confirm().🤖 Generated with Claude Code
https://claude.ai/code/session_01XwDMNpJzcA3Z2nP3jjwuJd
Generated by Claude Code