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.tsx → aria-label="Editar meta"
BudgetOverrideDialog.tsx → aria-label="Editar orçamento"
InvestmentEntryDialog.tsx → aria-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).
Fatia (b2) de #106. Sites 2, 6 e 7 dos 9. Gêmea de #127 (mesmo defeito, telas diferentes); a fatia do DS é #126.
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/investimentos/InvestmentEntryDialog.tsx:187-194InvestmentTypeAccordion.tsx:297,InvestmentTypeCard.tsx,dashboard/InvestmentList.tsxcomponents/configuracao-mes/BudgetOverrideDialog.tsx:125-132app/(app)/configuracao-mes/page.tsx:119components/metas/GoalDialog.tsx:156-163components/metas/MetasList.tsx:64,app/(app)/metas/page.tsx:27Linhas 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>. EmGoalDialogé o ramoelsedo ternáriomode === 'create' ?; emBudgetOverrideDialogé o único botão do gatilho; emInvestmentEntryDialogé o ramoexisting ?de um ternário de três braços (existing/isGlobal/ default). Os consumidores foram confirmados porgrep(exigência 7) — nenhum é código morto.Evidência
Padrão idêntico nos três. Exemplo do site 2:
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 logdeBudgetOverrideDialog.tsxeGoalDialog.tsx: sóefc1a59(commit de documentação). Nenhum commit escolhe não rotular — é omissão.Impacto
Em
/metase/investimentoscada 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-meso 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.tsx→aria-label="Editar meta"BudgetOverrideDialog.tsx→aria-label="Editar orçamento"InvestmentEntryDialog.tsx→aria-label="Editar registro"— mesmo texto dotitleque o próprio componente já calcula na linha 183 (existing ? 'Editar registro' : 'Registrar aporte'); o ramo do lápis é exatamente o ramoexisting, 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 oaria-hiddenviahasA11yProp, 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
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). Varredura de texto-fonte, padrão derow-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 — e aqui isso é mais crítico que em #127:InvestmentEntryDialog.tsxtem três botões no mesmo ternário eGoalDialog.tsxtem dois, então umexpect(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:A âncora
^\s*...$com flagmé a mesma dea11y-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ãocloses(exigência 8).