Skip to content

fix: DeleteButton do DS ganha aria-label no gatilho de exclusão (closes #126) - #139

Draft
Guiroos wants to merge 2 commits into
mainfrom
claude/issue-126-delete-button-aria-label
Draft

fix: DeleteButton do DS ganha aria-label no gatilho de exclusão (closes #126)#139
Guiroos wants to merge 2 commits into
mainfrom
claude/issue-126-delete-button-aria-label

Conversation

@Guiroos

@Guiroos Guiroos commented Sep 2, 2026

Copy link
Copy Markdown
Owner

O que mudou

components/ui/delete-button.tsx — o <Button> gatilho (icon-only, size="icon" variant="ghost", envolve só o <Trash2>) não tinha aria-label, aria-labelledby, title nem sr-only. lucide-react está pinado em 1.8.0, e o Icon dessa versão marca o <svg> como aria-hidden="true" quando não recebe filho nem prop de a11y — então o role=button ficava 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 DeleteButton compartilhado 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:

  • Rotular o botão, não o ícone. <Trash2 aria-label="..."> também desligaria o aria-hidden (via hasA11yProp do 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ão aria-label={title}. Nenhum dos 4 call sites (categorias/page.tsx, contas/page.tsx, MetasList.tsx, WithdrawalTable.tsx) passa title customizado — 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); o aria-label do gatilho replica esse mesmo texto, sem acoplar rotulagem a um prop que ninguém sobrescreve hoje.

Como testei

Risco e o que NÃO foi coberto

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

#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 Guiroos left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. fix revertido (sem aria-label nenhum);
  2. aria-label só no botão "Cancelar" do Dialog, com o gatilho sem nome — o falso-positivo que o comentário de :11-13 existe para barrar;
  3. aria-label no <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 é o printWidth do prettier que mantém <Trash2 className="h-3.5 w-3.5" aria-label="Excluir" /> em uma linha só (conferido com prettier --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

Comment thread __tests__/unit/a11y-nomes-acessiveis.test.ts Outdated
…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
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.

[a11y] DeleteButton do DS não tem nome acessível — 6 pontos de render anunciam o botão destrutivo como "botão"

2 participants