Skip to content

[a11y] Lápis de editar sem nome acessível em /metas, /configuracao-mes e /investimentos #128

Description

@Guiroos

Fatia (b2) de #106. Sites 2, 6 e 7 dos 9. Gêmea de #127 (mesmo defeito, telas diferentes); a fatia do DS é #126.

Por que (b) virou duas issues. O fatiamento sugerido na #106 propunha "os seis lápis" como uma fatia só — 6 arquivos de componente + 1 de teste = 7, acima do teto de 5 por PR do PASSO 4 (.claude/routines.md). Dividida por tela: cadastro (/categorias, /contas) em #127, o restante aqui.

Onde

3 sites, enumerados por inteiro (exigência 8). Todos são <Button size="icon"> cujo único filho é um <Pencil> do lucide-react, sem aria-label, aria-labelledby, title ou sr-only:

# (na #106) arquivo:linha o que faz onde é renderizado
2 components/investimentos/InvestmentEntryDialog.tsx:187-194 editar aporte/rendimento do mês InvestmentTypeAccordion.tsx:297, InvestmentTypeCard.tsx, dashboard/InvestmentList.tsx
6 components/configuracao-mes/BudgetOverrideDialog.tsx:125-132 editar orçamento do mês app/(app)/configuracao-mes/page.tsx:119
7 components/metas/GoalDialog.tsx:156-163 editar meta components/metas/MetasList.tsx:64, app/(app)/metas/page.tsx:27

Linhas reconferidas em 2026-08-29 — deslocaram ~1-2 em relação à tabela da #106. Reconfirmar por conteúdo, não por linha (exigência 8): o alvo é o <Button size="icon"> que envolve <Pencil>. Em GoalDialog é o ramo else do ternário mode === 'create' ?; em BudgetOverrideDialog é o único botão do gatilho; em InvestmentEntryDialog é o ramo existing ? de um ternário de três braços (existing / isGlobal / default). Os consumidores foram confirmados por grep (exigência 7) — nenhum é código morto.

Evidência

Padrão idêntico nos três. Exemplo do site 2:

// components/investimentos/InvestmentEntryDialog.tsx:187-194
<Button
  size="icon"
  variant="ghost"
  className="h-7 w-7 text-text-tertiary hover:text-text-primary"
  onClick={() => setOpen(true)}
>
  <Pencil className="h-3 w-3" />
</Button>

O <svg> não pode servir de nome: lucide-react está pinado em 1.8.0 (package.json:45) e o Icon dessa versão esconde o <svg> quando não recebe filho nem prop de a11y —

// lucide-react@1.8.0 — dist/esm/Icon.js:36  (lido do tarball do registry)
...!children && !hasA11yProp(rest) && { "aria-hidden": "true" },

— então a computação do nome do role=button fica sem fonte alguma. Medido na árvore real via CDP (Accessibility.getFullAXTree) na auditoria da #106: role=button name="".

WCAG 4.1.2 Name, Role, Value (nível A).

Falsificação (exigência 2)

  • O lint não pega. eslint.config.mjs:25-32 escopa jsx-a11y/label-has-associated-control a components/ui/field.tsx e nada mais. CI verde com os três sites presentes.
  • Não é decisão deliberada. git log de BudgetOverrideDialog.tsx e GoalDialog.tsx: só efc1a59 (commit de documentação). Nenhum commit escolhe não rotular — é omissão.
  • Não é convenção do repo. No recorte do lápis de editar são 6 arquivos que rotulam contra 6 que não. O argumento não é "a minoria é o bug" da categoria 5 — é falha normativa de WCAG medida na árvore, que valeria mesmo sem convenção nenhuma. A convenção só informa a redação.

Impacto

Em /metas e /investimentos cada linha da lista termina em lápis + DeleteButton (#126), ambos anunciados como "botão", "botão" — quem usa leitor de tela não distingue editar de excluir, e a exclusão é destrutiva e sem desfazer.

Em /configuracao-mes o lápis é o único caminho para ajustar o orçamento de uma categoria no mês: sem nome, a tela inteira fica sem ponto de entrada anunciável. Atinge também comando de voz — "clicar em Editar" não encontra alvo sem nome.

Proposta

Uma linha por arquivo, aditiva, sem mudança visual:

   <Button
     size="icon"
     variant="ghost"
+    aria-label="Editar meta"
     className="h-7 w-7 text-text-tertiary hover:text-text-primary"
     onClick={() => setOpen(true)}
   >
     <Pencil className="h-3.5 w-3.5" />
   </Button>
  • GoalDialog.tsxaria-label="Editar meta"
  • BudgetOverrideDialog.tsxaria-label="Editar orçamento"
  • InvestmentEntryDialog.tsxaria-label="Editar registro" — mesmo texto do title que o próprio componente já calcula na linha 183 (existing ? 'Editar registro' : 'Registrar aporte'); o ramo do lápis é exatamente o ramo existing, então o rótulo e o cabeçalho do diálogo passam a dizer a mesma coisa.

Rotular o botão, não o ícone. <Pencil aria-label="..." /> desliga o aria-hidden via hasA11yProp, mas põe o nome no <svg> em vez do controle — alvo de clique e nó nomeado divergem, contra os 20 controles do repo que rotulam o <Button>.

Cobertura (exigência 3)

Nenhum teste pega hoje e o lint está verde. Acrescentar um bloco describe a __tests__/unit/a11y-nomes-acessiveis.test.ts, criado pela #126 (se ela ainda não tiver mergeado, criar o arquivo com o mesmo cabeçalho). Varredura de texto-fonte, padrão de row-actions.test.ts (#54) e a11y-estado-selecao.test.ts (#107) — não há @testing-library/react no projeto.

A asserção precisa ancorar no botão que contém o <Pencil>, não no arquivo — e aqui isso é mais crítico que em #127: InvestmentEntryDialog.tsx tem três botões no mesmo ternário e GoalDialog.tsx tem dois, então um expect(source).toMatch(/aria-label/) passa assim que qualquer um deles for rotulado, com o lápis intacto. O recorte que só a correção certa satisfaz:

// (?:(?!<Button)[\s\S])*? garante que a fatia comece no <Button> MAIS PRÓXIMO
// do <Pencil>, e não num braço anterior do ternário.
const gatilho = source.match(/<Button\b(?:(?!<Button)[\s\S])*?<Pencil\b/)?.[0]
expect(gatilho).toBeDefined()
expect(gatilho).toMatch(/^\s*aria-label="[^"]+"$/m)

A âncora ^\s*...$ com flag m é a mesma de a11y-estado-selecao.test.ts: sem ela o atributo comentado deixa o teste verde com o bug de volta.

Custo estimado

M — 4 arquivos: InvestmentEntryDialog.tsx, BudgetOverrideDialog.tsx, GoalDialog.tsx + __tests__/unit/a11y-nomes-acessiveis.test.ts.

Fecha parcialmente #106 — usar refs #106, não closes (exigência 8).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    a11yclaude-auditAchado da auditoria automáticaclaude-wipJá tem PR aberto, não pegar de novo

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions