Repository navigation
Description
Contexto
docs/UI-AAA-OVERHAUL.md §VII.4, bloco Respeito ao usuário (binário): "Teclado 100%; foco
gerenciado; WCAG AA". O único diálogo do produto não cumpre nenhuma das três coisas.
src/components/ui/Modal.tsx é usado num lugar só — HomeView.tsx:420, o modal // configurar sala, que é o caminho primário de criação de sala (linguagem via BannerCarousel, dificuldade,
máximo de jogadores, público/privado). Verificado em origin/main@31c1c2d:
- Não se declara um diálogo. O card em
Modal.tsx:37-43é ummotion.divcru: sem
role="dialog", semaria-modal="true", semaria-labelledby. Para leitor de tela não existe
diálogo nenhum — o conteúdo simplesmente aparece no meio da home. - O foco nunca entra. Não há nada que mova o foco para dentro ao abrir. Quem clica no
StarBorder "Criar sala"(HomeView.tsx:325) e passa a navegar por teclado continua com o foco
no botão atrás do backdrop. - O Tab passeia pela página de trás. Sem trap,
Tabsai do modal e percorre header, ranking,
login com Google, inputs de nick/código, salas públicas e footer — todos visualmente cobertos
pelobg-black/70 backdrop-blur(Modal.tsx:31). O usuário digita "às cegas" num formulário que
não vê. - O foco não volta. Ao fechar (Escape em
Modal.tsx:22, oX, o backdrop, "cancelar" ou
"criar sala"), o foco cai para o<body>— a próxima teclaTabrecomeça do topo da página.
Não é teórico: o modal contém o BannerCarousel, cuja navegação é inteiramente por teclado
(BannerCarousel.tsx:499-513, roving focus com setas). O componente foi feito acessível por dentro
e vive dentro de um contêiner que não gerencia foco.
Acceptance criteria
- Com o modal aberto,
document.activeElementestá dentro do card (primeiro focável, ou o
próprio card comtabIndex={-1}). Taba partir do último focável volta ao primeiro;Shift+Taba partir do primeiro vai ao
último. Em nenhum momento o foco alcança um elemento fora do modal.- Ao fechar por qualquer caminho (Escape, botão
X, clique no backdrop, "cancelar", "criar
sala"), o foco volta ao elemento que abriu o modal. - O card tem
role="dialog",aria-modal="true"earia-labelledbyapontando para oiddo
<h3>do título (Modal.tsx:45-47). - Não regride o carousel:
ArrowLeft/ArrowRight/Home/Endcontinuam navegando as
bandeiras dentro do modal — o trap intercepta apenasTab. - O comportamento vale para qualquer uso futuro do
Modal(a correção é no componente, não no
HomeView). - Gate verde:
pnpm typecheck·pnpm test·pnpm build.
Dica de abordagem
- Mudança local e sem dependência nova (§7.3). Não instale
focus-trap/radix-dialog: dep
nova cai em §7.2 e pede quórum, para um problema de ~40 linhas. - Trap:
onKeyDownno card,if (e.key !== "Tab") return, coletar focáveis com um
querySelectorAlldo seletor padrão (a[href], button:not([disabled]), input, select, textarea, [tabindex]:not([tabindex="-1"])), e ciclar come.preventDefault()só nas bordas. - Retorno de foco: guarde
document.activeElementnumuseRefno efeito que dispara quandoopen
viratrue, e restaure no cleanup — não noonClose, senão os 5 caminhos de fechamento
precisam lembrar de chamar. AnimatePresencemantém o nó montado durante a animação de saída (Modal.tsx:28): a restauração
de foco não pode depender do nó ter sumido.- Considere
aria-hidden/inertno conteúdo de fundo enquanto aberto. Se decidir não fazer,
declare o porquê no PR (o trap deTabjá resolve o essencial; o modo de navegação por
cursor virtual do leitor de tela é o que fica de fora). - Verificação — leia antes de tentar: o Browser pane não entrega
keydownreal
(computer{key}não dispara;typeusainsertText). Não tente provar o trap clicando no
preview e não conclua que está quebrado por isso. Prove por lógica: extraia o cálculo do próximo
foco numa função pura testável, e/ou dispareKeyboardEventsintético viajavascript_tool.
Escopo
§7.3 (UI/componentes). Não toca API, banco, engine nem a área sagrada — o modal só existe fora da
corrida. Fora de escopo: prefers-reduced-motion nas animações do modal (é a #108) e a11y da
tabela do ranking (é a #93).
Activity