fix: DeleteButton do DS ganha aria-label no gatilho de exclusão (closes #126) - #139
fix: DeleteButton do DS ganha aria-label no gatilho de exclusão (closes #126)#139Guiroos wants to merge 2 commits into
Conversation
#126) O trigger `<Button>` do DeleteButton (components/ui/delete-button.tsx) é icon-only e não tinha aria-label, aria-labelledby, title ou sr-only. O Trash2 do lucide-react@1.8.0 marca o svg como aria-hidden quando não recebe filho nem prop de a11y, então o role=button ficava sem nenhuma fonte de nome (WCAG 4.1.2). Afeta 6 pontos de render em /categorias, /contas, /metas e /investimentos — o botão destrutivo do DS. Adiciona __tests__/unit/a11y-nomes-acessiveis.test.ts, seguindo o padrão de gate por texto-fonte de row-actions.test.ts (#54) e a11y-estado-selecao.test.ts (#107): ancora a asserção no bloco do <Button> que é o gatilho (contém o onClick de abrir o dialog e o ícone Trash2), não no arquivo inteiro. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vttNSZKJfEaaPukZWKhvB
Guiroos
left a comment
There was a problem hiding this comment.
Revisão do head 0f6f326. A correção está certa e fecha a #126 por inteiro: o aria-label vai no <Button> e não no <Trash2> — que o Icon do lucide-react@1.8.0 marca como aria-hidden quando não recebe filho nem prop de a11y —, e o {...props} de components/ui/button.tsx:80 repassa o atributo ao <button> real, então o nome chega mesmo à árvore. Site único do DS, com os 6 pontos de render herdando o rótulo.
Confirmei por conta própria a afirmação do corpo que sustenta a escolha de "Excluir" fixo sobre aria-label={title}, porque era o ponto em que o PR poderia estar decidindo por cima da issue: nenhum dos 6 call sites passa title. categorias/page.tsx:71,110, contas/page.tsx:79, MetasList.tsx:75,141 e WithdrawalTable.tsx:74 passam só onDelete e, em três deles, errorMessage.
Rodei os gates nesta sessão, sobre este head: npm ci, npm run lint, npm run typecheck e npm test verdes (35 arquivos, 529 testes).
O gate novo cobre de verdade, e isso eu mutei o fonte para verificar em vez de deduzir. As três correções erradas plausíveis deixam o teste vermelho:
- fix revertido (sem
aria-labelnenhum); aria-labelsó no botão "Cancelar" doDialog, com o gatilho sem nome — o falso-positivo que o comentário de:11-13existe para barrar;aria-labelno<Trash2>em vez do<Button>— a correção errada que a própria #126 nomeia. Vale registrar que ela é barrada com folga estreita: a asserção exige o atributo sozinho numa linha, e é oprintWidthdo prettier que mantém<Trash2 className="h-3.5 w-3.5" aria-label="Excluir" />em uma linha só (conferido comprettier --check).
Um achado, nit — sobre o alcance do gate, não sobre o fix. O comentário de :11-13 promete que um aria-label em qualquer outro botão do arquivo não deixa o teste verde. O regex entrega isso só para os botões posteriores ao gatilho: um <Button> rotulado antes dele é atravessado pelo [^]*?, e a asserção passa com o gatilho sem nome. Não é falso-positivo hoje (o gatilho é o primeiro <Button> do arquivo), então a proteção depende da ordem atual do fonte e não do regex. Verificado rodando, e a correção já existe no repo — é a lookahead negativa do PR #140, que cria este mesmo arquivo. Detalhe e a troca de uma linha no comentário inline.
Generated by Claude Code
…arquivo Nit de revisão em #139: o regex anterior (<Button\b[^]*?onClick=...[^]*?<Trash2...) podia atravessar um <Button> anterior no mesmo arquivo, caso ele existisse antes do gatilho — a proteção dependia da ordem do fonte (o gatilho ser o primeiro <Button>), não do regex em si. Sem falso-positivo hoje, mas frágil. Troca por findClosestButtonWithIcon(source, iconTag), com lookahead negativa que impede o match de atravessar outro "<Button" antes de alcançar o ícone-alvo — mesmo helper que o PR #140 (issue #127) introduz ao criar este mesmo arquivo. Validado que a lookahead rejeita o cenário do reviewer (Button rotulado antes do gatilho, gatilho ainda sem nome) e que o comportamento correto (rejeita pré-fix, aceita pós-fix) se mantém. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vttNSZKJfEaaPukZWKhvB
O que mudou
components/ui/delete-button.tsx— o<Button>gatilho (icon-only,size="icon" variant="ghost", envolve só o<Trash2>) não tinhaaria-label,aria-labelledby,titlenemsr-only.lucide-reactestá pinado em1.8.0, e oIcondessa versão marca o<svg>comoaria-hidden="true"quando não recebe filho nem prop de a11y — então orole=buttonficava sem nenhuma fonte de nome (WCAG 4.1.2 Name, Role, Value, nível A).<Button size="icon" variant="ghost" className="h-7 w-7 text-text-secondary hover:text-negative" onClick={() => setOpen(true)} + aria-label="Excluir" > <Trash2 className="h-3.5 w-3.5" /> </Button>Uma linha, aditiva, sem mudança visual. É o
DeleteButtoncompartilhado do DS — 6 pontos de render em 4 telas (/categorias×2,/contas,/metas×2,/investimentos), todos passam a herdar o rótulo automaticamente.Por que dessa forma
Segui a proposta da issue à risca:
<Trash2 aria-label="...">também desligaria oaria-hidden(viahasA11yPropdo lucide), mas poria o nome no<svg>em vez do controle — o alvo de clique e o nó nomeado ficariam em elementos diferentes, divergindo dos 20 controles do repo que já rotulam o<Button>.aria-label="Excluir"fixo, nãoaria-label={title}. Nenhum dos 4 call sites (categorias/page.tsx,contas/page.tsx,MetasList.tsx,WithdrawalTable.tsx) passatitlecustomizado — todos usam o default'Excluir item', que é o texto do heading do dialog, não uma descrição por item. O botão de confirmação visível já usa o texto"Excluir"(delete-button.tsx:74,94); oaria-labeldo gatilho replica esse mesmo texto, sem acoplar rotulagem a um prop que ninguém sobrescreve hoje.Como testei
npm ci && npm run lint && npm run format:check && npm run typecheck && npm test && npm run build— todos verdes (35 arquivos, 529 testes).ds-reviewerrodado sobre a mudança (hookPostToolUse:Edit): aprovado sem violações —aria-labelé atributo ARIA padrão, ortogonal às 6 regras do DS.__tests__/unit/a11y-nomes-acessiveis.test.ts, seguindo o precedente derow-actions.test.ts([a11y] RowActions fica opacity-0 no desktop e só reaparece no hover do mouse — o foco de teclado é invisível em 29 telas #54) ea11y-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) — gate por texto-fonte, já que o projeto não tem@testing-library/react. A asserção ancora no bloco do<Button>que é o gatilho (contém oonClick={() => setOpen(true)}e o<Trash2>), não no arquivo inteiro — validei manualmente que o regex (a) rejeita o código anterior ao fix, (b) passa com a correção, (c) não teria um falso-positivo se umaria-labelfosse adicionado em outro botão do mesmo arquivo (ex: o "Cancelar").npm run test:integrationnão foi rodado — exige credenciais do Neon e só roda em push paramain; esta mudança não toca camada de banco.Risco e o que NÃO foi coberto
@testing-library/react); cobertura fica no nível de gate por texto-fonte, mesmo padrão de achados de a11y anteriores.DeleteButton— os "lápis" de editar ([a11y] Lápis de editar sem nome acessível em /categorias e /contas — três diálogos de cadastro #127, [a11y] Lápis de editar sem nome acessível em /metas, /configuracao-mes e /investimentos #128) e oSplitSection([a11y] Dois "X" idênticos e sem nome na divisão de despesa do /registro — um fecha tudo, o outro remove uma pessoa #129) são issues separadas, ainda não implementadas; o arquivo de teste novo (a11y-nomes-acessiveis.test.ts) fica pronto para receber osdescribedelas.Arquivos tocados
components/ui/delete-button.tsx__tests__/unit/a11y-nomes-acessiveis.test.ts(novo)Closes #126
🤖 Generated with Claude Code
https://claude.ai/code/session_014vttNSZKJfEaaPukZWKhvB
Generated by Claude Code