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.tsx → aria-label="Editar categoria"
GroupDialog.tsx → aria-label="Editar grupo"
AccountDialog.tsx → aria-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).
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.
Onde
3 sites, enumerados por inteiro (exigência 8). Todos são
<Button size="icon">cujo único filho é um<Pencil>dolucide-react, semaria-label,aria-labelledby,titleousr-only:components/categorias/CategoryDialog.tsx:156-163app/(app)/categorias/page.tsx:99components/categorias/GroupDialog.tsx:88-95app/(app)/categorias/page.tsx:70components/contas/AccountDialog.tsx:164-171app/(app)/contas/page.tsx:70Linhas 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
elsedo ternáriomode === '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 porgrep(exigência 7) — nenhum é código morto.O
<Button>do ramocreateno 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:
O
<svg>não pode servir de nome:lucide-reactestá pinado em1.8.0(package.json:45) e oIcondessa versão esconde o<svg>quando não recebe filho nem prop de a11y —— então a computação do nome do
role=buttonfica 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)
eslint.config.mjs:25-32escopajsx-a11y/label-has-associated-controlacomponents/ui/field.tsxe nada mais. CI verde com os três sites presentes.git logdeCategoryDialog.tsx,GroupDialog.tsxeAccountDialog.tsx: sóefc1a59(commit de documentação que passou por eles). Em nenhum houve escolha sobre rotulagem — é omissão.aria-labele 1sr-onlyem 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
/categoriase/contascada linha da lista termina em dois controles anunciados como "botão", "botão" — o lápis e oDeleteButton(#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.tsx→aria-label="Editar categoria"GroupDialog.tsx→aria-label="Editar grupo"AccountDialog.tsx→aria-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.InstallmentGroupEditDialogusa"Editar"seco, mas ali há um controle por tela.Rotular o botão, não o ícone.
<Pencil aria-label="..." />desliga oaria-hiddenviahasA11yProp, 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
describea__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 emrow-actions.test.ts(#54) ea11y-estado-selecao.test.ts(#107) — não há@testing-library/reactno projeto.A asserção precisa ancorar no botão que contém o
<Pencil>, não no arquivo. Umexpect(source).toMatch(/aria-label/)sobreCategoryDialog.tsxpassaria 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:A âncora
^\s*...$com flagmé a mesma dea11y-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ãocloses(exigência 8).