Skip to content

Modais React com semântica de diálogo e diálogos in-app (v3.22.0) - #79

Merged
qzte merged 1 commit into
mainfrom
claude/loving-cori-68ipij
Sep 14, 2026
Merged

qzte merged 1 commit into
mainfrom
claude/loving-cori-68ipij

Conversation

@qzte

@qzte qzte commented Sep 14, 2026

Copy link
Copy Markdown
Owner

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, 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, em dois sítios — o modalAccessibility() do bloco imperativo e o overlay do ecrã de arranque, que é o único que usa inert. Passa a ser um hook, useModalA11y, em vez de uma terceira implementação a divergir das outras duas: role="dialog", aria-modal, aria-labelledby a apontar para o título que a modal já tem, foco inicial lá dentro, prisão do Tab, Escape, foco devolvido a quem abriu, e inert + aria-hidden no resto da página.

As modais passam a ser renderizadas num portal para o <body>. Sem isso, marcar #view-tracking como inerte tornaria inerte também a modal, que é renderizada lá dentro. Uma pilha (MODAL_STACK) trata o caso de uma modal abrir outra — o StepDrawer abre o StepDatesModal — que sem ela fechava as duas com um só Escape.

#8 — 28 alert()/confirm() nativos substituídos

13 alert e 15 confirm dão lugar a duas funções, definidas no bloco imperativo e usadas também pelo bloco React:

Função Para quê
await askConfirm(mensagem, { titulo, confirmar, cancelar, tom }) pergunta de sim/não
notify(mensagem, tom) aviso sem resposta, em cartão no canto inferior direito

A interface é o DialogHost, montado uma vez pelo App, que se regista em window.__SUPERMERCADOS_confirm e window.__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 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 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:

  1. O Escape que abria a pergunta era apanhado pela própria pergunta. O React monta-a no microtask seguinte, ou seja ainda durante o despacho do mesmo Escape, entre um listener do document e 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 o timeStamp do evento com o instante em que a modal entrou na pilha.
  2. O Escape sobre a pergunta accionava também o fecho da modal por baixo, que a tornava a abrir. Resolvido com uma saída antecipada no listener imperativo enquanto houver uma modal React aberta.

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 ci passa (86 testes unitários, lint, validação, build). Nos testes de browser passam 46; as 6 falhas são todas ERR_CERT_AUTHORITY_INVALID no Google Fonts — artefacto do proxy TLS do ambiente onde isto foi desenvolvido, presente de igual modo em main (4 falhas lá, com menos testes).

Notas

  • Versão bumpada para 3.22.0 (código, badge e README), para o aviso de versão nova chegar a quem tenha a página aberta.
  • README: a secção de acessibilidade passa a documentar as dez modais e a proibição de alert()/confirm().

🤖 Generated with Claude Code

https://claude.ai/code/session_01XwDMNpJzcA3Z2nP3jjwuJd


Generated by Claude Code

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
qzte marked this pull request as ready for review September 14, 2026 20:06
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@qzte
qzte merged commit fe5fd74 into main Sep 14, 2026
3 checks passed
@qzte
qzte deleted the claude/loving-cori-68ipij branch September 14, 2026 20:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants