fix: aria-current="page" nos itens de navegação ativos (closes #118) - #135
fix: aria-current="page" nos itens de navegação ativos (closes #118)#135Guiroos wants to merge 3 commits into
Conversation
Sidebar, BottomNav e os links inline do dialog "Menu" marcavam o item ativo só via className, sem nada programaticamente determinável para leitor de tela — os 11+ links se anunciavam idênticos. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J1hGTYJx2gfTHSDdWFTWTa
Guiroos
left a comment
There was a problem hiding this comment.
Revisão do head 9665b8f. Os 3 sites da #118 estão fechados por inteiro (exigência 8): Sidebar.tsx:69, BottomNav.tsx:66 e BottomNav.tsx:202 — grep -rn "aria-current" components app devolve exatamente essas três ocorrências, e grep por isActive|usePathname não revela um quarto site de link de navegação. A escolha de 'page' sobre 'true' e do ternário com undefined sobre {active} está correta e justificada, e o escopo adjacente (as 4 <nav> sem aria-label) ficou de fora como a issue pediu.
Também conferi que a mudança não expõe dois marcadores de "página atual" ao mesmo tempo: Sidebar é hidden … lg:flex e o <nav> da BottomNav é lg:hidden, então em cada breakpoint só um dos dois chega à árvore de acessibilidade; e com o dialog "Menu" aberto o Radix marca o resto como aria-hidden, deixando os 7 links do site 3 como a informação inteira — que é o caso que a issue apontou como o que fecha sozinho.
Um achado, não-bloqueante: o gate novo passa verde com a correção inteira revertida por comentário. Verificado rodando tsc, lint e vitest sobre o fonte mutado, não deduzido. Detalhe e a correção de três linhas no comentário inline.
Generated by Claude Code
toMatch(/aria-current=.../) sem âncora passava com o atributo comentado, deixando os 22 links de navegação voltar a se anunciar idênticos sem que o gate acusasse — mesmo modo de falha que a11y-estado-selecao.test.ts (#107) já evita para aria-pressed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J1hGTYJx2gfTHSDdWFTWTa
Guiroos
left a comment
There was a problem hiding this comment.
Revisão do head f50ed9e (a anterior foi no 9665b8f). O achado da rodada passada está fechado: as três asserções passaram a ancorar na linha inteira (^\s*…$ com flag m), e comentar a linha do fix agora deixa o gate vermelho — mutei para conferir, não deduzi.
Reconferi por conta própria que não há um quarto site, já que a exigência 8 é o eixo do achado original: usePathname fora de Sidebar/BottomNav só aparece em ArchivedFilterChip.tsx e MonthSelector.tsx, que são filtros de URL, não links de navegação. Os 3 sites da #118 seguem sendo a lista inteira.
Rodei os gates nesta sessão, sobre este head: npm ci e npm test verdes (35 arquivos, 530 testes).
Um achado, nit — sobre o alcance do gate, não sobre o fix. A âncora nova é da linha, não do elemento: mover o mesmo aria-current={…} para um filho do <Link> mantém as três asserções verdes com o <a> sem aria-current na árvore. Verifiquei rodando nos dois arquivos (<div> do BottomNav, <Icon> do Sidebar), e é a mesma classe de correção errada que a família #126/#127/#128 nomeia ("aria-label no ícone em vez do botão"). A correção soma a âncora de elemento à de linha sem perder nenhuma das duas — reprodução, o diff das três asserções e a bateria de mutação nos quatro estados no comentário inline, incluindo por que a variante intuitiva [^>]* reprova o fonte correto.
Generated by Claude Code
A âncora de linha (f50ed9e) protegia contra o atributo comentado, mas não amarrava aria-current ao <Link> — o mesmo atributo movido para um filho (a <div>/<Icon> que já carrega o destaque visual do item ativo) mantinha as três asserções verdes com o <a> sem aria-current na árvore, a mesma classe de correção errada que a família #126/#127/#128 nomeia. [^<]*, não [^>]*: o <Link> do dialog "Menu" tem onClick={() => {...}} na tag de abertura, e a seta => contém um > que faria [^>]* parar cedo demais e reprovar o fonte correto. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J1hGTYJx2gfTHSDdWFTWTa
O que mudou
Nenhum dos 3 sites que marcam o item de navegação ativo (
Sidebar.tsx,BottomNav.tsx) expunha isso a leitor de tela — o estadoactive/isActive(href)alimentava sóclassName. Adicionadoaria-current="page"(via expressão, nunca literal fixo) nos três:components/layout/Sidebar.tsx:69—NavItem(11 links da sidebar, 12 com admin)components/layout/BottomNav.tsx:66—NavItem(3 links da barra inferior)components/layout/BottomNav.tsx:202— links inline do dialog "Menu" (7 links, única via para 7 das 12 rotas no mobile)Mudança puramente aditiva:
aria-current={active ? 'page' : undefined}(ouisActive(href)no site 3) — sem alterarclassName, estrutura JSX, roteamento ouisActive/pendingHref.Por que dessa forma
Segui a proposta da issue à risca, inclusive a escolha do token:
'page', não'true'—'page'é o token ARIA específico para "este link aponta para a página em que você está" (o que os leitores anunciam como "página atual");'true'é o fallback genérico para quando não há token que descreva a relação, e aqui há.? 'page' : undefined, não{active}—aria-current={active}renderizaaria-current="false"no HTML quando inativo, e ARIA trata isso como o estadofalse(presente), não como ausência do atributo — React só omitearia-*paraundefined/null. Isso deixaria a marcação sintaticamente presente e semanticamente inerte.Não toquei
isActive/pendingHref(o cálculo do estado já está correto) nem os 4<nav>semaria-labeldo shell — a issue registra isso como achado adjacente, deliberadamente fora deste escopo.Como testei
npm run lint && npm run format:check && npm run typecheck && npm test && npm run build— todos verdes (530 testes, 35 arquivos).__tests__/unit/a11y-pagina-atual.test.ts, seguindo o precedente dea11y-estado-selecao.test.ts([a11y] Chip e Segment não expõem qual opção está selecionada — o seletor de tipo do formulário de lançamento é um grupo de botões sem estado para leitor de tela #107): asserções ancoradas na expressão de estado (aria-current={active ? 'page' : undefined}), não emtoMatch(/aria-current/)genérico — as duas correções erradas mais prováveis ('page'fixo, ou{active}sem o ternário) passariam nesse gate genérico e não neste.ds-reviewerrodado sobre as duas mudanças (hookPostToolUse:Editexige a cada edição): confirmou quearia-currentnão introduz violação de DS (não é classe Tailwind, não é valor arbitrário, não substitui primitivo). Reportou 4 violações pré-existentes nos mesmos arquivos, sem relação com esta mudança (confirmado via diff) — ver seção abaixo.Risco e o que NÃO foi coberto
@testing-library/react); cobertura fica no nível do texto-fonte, mesmo padrão já usado para achados de a11y anteriores ([a11y] RowActions fica opacity-0 no desktop e só reaparece no hover do mouse — o foco de teclado é invisível em 29 telas #54, [a11y] Chip e Segment não expõem qual opção está selecionada — o seletor de tipo do formulário de lançamento é um grupo de botões sem estado para leitor de tela #107).Sidebar.tsx:135—font-[500]deveria serfont-mediumSidebar.tsx:140,157—uppercaseacumulado sobretext-labelBottomNav.tsx:189—left-[50%]/translate-x-[-50%]deveriam serleft-1/2/-translate-x-1/2BottomNav.tsx:78,171—stroke-[1.75](avaliação de menor confiança, sem tabela de tokens de stroke-width no DS)<nav>semaria-label) não coberto de propósito — é outro achado, com outro gate.npm run test:integrationnão roda em PR (exige credenciais Neon); esta mudança não toca camada de banco.Arquivos tocados
components/layout/Sidebar.tsxcomponents/layout/BottomNav.tsx__tests__/unit/a11y-pagina-atual.test.ts(novo)Generated by Claude Code