Skip to content

[a11y] Lápis de editar sem nome acessível em /categorias e /contas — três diálogos de cadastro #127

Description

@Guiroos

Fatia (b1) de #106. Sites 3, 4 e 5 dos 9. Gêmea de #128 (mesmo defeito, telas diferentes); a fatia do DS é #126; o gate repo-wide é #131.

Por que (b) virou duas issues. O fatiamento sugerido no corpo da #106 e no comentário de 2026-08-26 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). A fatia que motivou o fatiamento não cabia no limite que a motivou. Dividida por tela: cadastro (/categorias, /contas) aqui, o restante em #128.

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
3 components/categorias/CategoryDialog.tsx:156-163 editar categoria app/(app)/categorias/page.tsx:99
4 components/categorias/GroupDialog.tsx:88-95 editar grupo app/(app)/categorias/page.tsx:70
5 components/contas/AccountDialog.tsx:164-171 editar conta/cartão app/(app)/contas/page.tsx:70

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 em cada arquivo é o ramo else do ternário mode === 'create' ? <botão com texto> : <botão só de ícone>, ou seja, o <Button size="icon"> que envolve <Pencil>. Os três consumidores foram confirmados por grep (exigência 7) — nenhum é código morto.

O <Button> do ramo create no mesmo arquivo já tem nome (texto "Categoria" / "Novo grupo" / "Nova conta"); ele não é alvo desta issue e é justamente o que torna a asserção ingênua perigosa (ver Cobertura).

Evidência

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

// components/categorias/GroupDialog.tsx:88-95
<Button
  size="icon"
  variant="ghost"
  className="h-7 w-7 text-text-tertiary hover:text-text-primary"
  onClick={() => setOpen(true)}
>
  <Pencil className="h-3.5 w-3.5" />
</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 CategoryDialog.tsx, GroupDialog.tsx e AccountDialog.tsx: só efc1a59 (commit de documentação que passou por eles). Em nenhum houve escolha sobre rotulagem — é omissão.
  • Não é convenção do repo. Repo-wide há 20 aria-label e 1 sr-only em controles de ícone. No recorte que importa — o lápis de editar — 6 arquivos rotulam (WithdrawalEditButton:192, ContributionEditButton:95, InstallmentGroupEditDialog:198, IncomeEditDialog:56,81, TransactionEditDialog:107,132, FixedExpenseEditDialog:101,126) contra 6 que não. O argumento aqui não é "a minoria é o bug" da categoria 5 — é falha normativa de WCAG medida na árvore, que valeria mesmo se ninguém no repo rotulasse. A convenção só diz qual redação usar.

Impacto

Em /categorias e /contas cada linha da lista termina em dois controles anunciados como "botão", "botão" — o lápis e o DeleteButton (#126). Quem usa leitor de tela não consegue distinguir editar de excluir, e a exclusão é destrutiva e sem desfazer. 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. Redação seguindo os 6 arquivos que já rotulam:

   <Button
     size="icon"
     variant="ghost"
+    aria-label="Editar categoria"
     className="h-7 w-7 text-text-tertiary hover:text-text-primary"
     onClick={() => setOpen(true)}
   >
     <Pencil className="h-3.5 w-3.5" />
   </Button>
  • CategoryDialog.tsxaria-label="Editar categoria"
  • GroupDialog.tsxaria-label="Editar grupo"
  • AccountDialog.tsxaria-label="Editar conta"

Rótulo específico e não "Editar" seco porque as duas listas de /categorias (grupos e categorias) coexistem na mesma página: dois "Editar" idênticos por linha não resolvem a ambiguidade que a issue existe para resolver. InstallmentGroupEditDialog usa "Editar" seco, mas ali há um controle por tela.

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 passam a divergir, 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). Padrão de varredura de texto-fonte, já usado em 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. Um expect(source).toMatch(/aria-label/) sobre CategoryDialog.tsx passaria com o bug intacto: o arquivo já contém o botão "+ Categoria" (linha 146) e bastaria alguém rotular aquele. 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 botão anterior do mesmo arquivo (o do ramo `create`).
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, pelo mesmo motivo: sem ela o atributo comentado deixa o teste verde com o bug de volta.

Custo estimado

M — 4 arquivos: CategoryDialog.tsx, GroupDialog.tsx, AccountDialog.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