# FormField

> Componentes / Formulário e ação · Estado: **Em andamento** — Implementado e utilizável, mas ainda sem a validação completa de acessibilidade.

O invólucro de um campo de formulário: rótulo (Label), o controle, a dica e a mensagem de erro, ligados entre si. É a fonte única de `aria-invalid` do sistema.

- Função: Formulário e ação
- Também chamado de: campo de formulário, grupo do campo, form field
- Mora em: `app/components/molecules/FormField.tsx`
- Página: [https://phorte-design-system.vercel.app/styleguide/peca/FormField](https://phorte-design-system.vercel.app/styleguide/peca/FormField)
- Ficha no Storybook: [components-complex-formfield](https://main--69b4847d498a9bd8659db9ba.chromatic.com/?path=/docs/components-complex-formfield--docs)
- Prévia ao vivo, sozinha: [https://phorte-design-system.vercel.app/previa/FormField/0](https://phorte-design-system.vercel.app/previa/FormField/0)
- Todas as peças: [https://phorte-design-system.vercel.app/llms.txt](https://phorte-design-system.vercel.app/llms.txt)

## Como usar no código

```tsx
import { FormField } from '@/app/components/molecules/FormField';

<FormField label={…}>…</FormField>
```

O `…` marca as props obrigatórias — ver *Propriedades*.

## Uso

### Variantes na ficha

1. [Padrão](https://phorte-design-system.vercel.app/previa/FormField/0)
2. [Com Erro](https://phorte-design-system.vercel.app/previa/FormField/1)
3. [Com Dica](https://phorte-design-system.vercel.app/previa/FormField/2)
4. [Obrigatório](https://phorte-design-system.vercel.app/previa/FormField/3)
5. [Dica e erro](https://phorte-design-system.vercel.app/previa/FormField/4)
6. [Com Select](https://phorte-design-system.vercel.app/previa/FormField/5)
7. [Id do consumidor prevalece](https://phorte-design-system.vercel.app/previa/FormField/6)
8. [Grupo](https://phorte-design-system.vercel.app/previa/FormField/7)
9. [Com contador](https://phorte-design-system.vercel.app/previa/FormField/8)
10. [Desabilitado](https://phorte-design-system.vercel.app/previa/FormField/9)

Em volta de todo Input, Select ou controle de formulário que precise de rótulo, dica ou erro — ou seja, quase sempre.

### Quando não usar

Para o Checkbox, o Radio e o Toggle soltos, o rótulo já vem pela prop `label`. Para um grupo de opções, use `as="fieldset"` (ou o RadioGroup, para rádios).

### Faça

- Passe **um** controle como filho: é nele que o FormField põe `id`, `aria-invalid`, `aria-describedby` e `aria-required`.
- Escreva a dica como instrução ("Use o e-mail da empresa"); ela continua visível quando o erro aparece.
- Escreva o erro dizendo como acertar: "Informe um e-mail com @", não "Inválido".

### Evite

- Mais de um filho sem `as="fieldset"`: com uma lista de filhos, o campo único não liga nenhum. Vários controles para uma pergunta são um grupo.
- Limitar nome ou endereço com o `contador`: gente de verdade não cabe no limite de alguém.
- Mostrar o erro antes de a pessoa terminar de preencher.

### Nota de uso

**Grupo (E59):** `as="fieldset"` junta várias opções numa pergunta, com a dica e o erro ligados ao `<fieldset>`. O `contador` liga o "142 de 280 caracteres" ao campo — e não se limita nome nem endereço. Com asterisco nos campos, a `LegendaDeObrigatorio` vai uma vez no topo. O erro está **ligado ao campo** por `aria-describedby` e é anunciado por região viva. Esta é a fonte única de `aria-invalid` do sistema: o `Checkbox`, o `Radio` e o `Toggle` derivam o estado visual dela. **Com dica e erro, os dois aparecem** e o campo é descrito pelos dois, dica primeiro — até a E48 o erro escondia a dica, que é justamente a instrução de como acertar. **Com o `ResumoDeErros` (E85)**, passe `anunciarErro={false}`: o resumo é o alerta do envio, e o erro continua ligado ao campo, com "Erro:" na frente para quem ouve.

### Anatomia

1. **Rótulo** — Um Label ligado ao controle, com asterisco se `required` e vermelho com ícone se houver erro.
2. **Controle** — O filho — Input, Select… —, com os atributos de ligação injetados.
3. **Dica** — `hint`, em `text-xs`, abaixo do controle.
4. **Contador** — `contador={{ minimo, maximo }}` (E152), um ou os dois: "142 de 280 caracteres", "faltam 12 para o mínimo de 50". Sem `atual`, conta pelo valor do controle. Ligado por `aria-describedby`; acima do limite, vermelho com ícone e quanto passou; o restante é anunciado depois que a pessoa para de digitar.
5. **Erro** — `error`, vermelho com ícone, com "Erro:" na frente para quem ouve (E85); `role="alert"` — menos com `anunciarErro={false}`, quando o formulário tem o ResumoDeErros.
6. **Grupo** — Com `as="fieldset"`: `<fieldset>` com `<legend>`; a dica, o contador e o erro ficam ligados ao grupo, e os filhos não são clonados.

### Estados

| Estado | O que muda |
| --- | --- |
| Padrão | Rótulo e controle; `aria-describedby` só se houver dica. |
| Obrigatório | `required`: asterisco no rótulo e `aria-required` no controle. |
| Com dica | A dica aparece e descreve o controle. |
| Com erro | Rótulo vermelho com ícone, mensagem com `role="alert"`, `aria-invalid` no controle — que pinta a própria borda de vermelho. |
| Dica e erro | Os dois aparecem, e o controle é descrito pelos dois, dica primeiro. |

### Comportamento

| Situação | O que acontece |
| --- | --- |
| Erro aparece | É anunciado na hora pelo `role="alert"`. Com `anunciarErro={false}` (E85), não: o ResumoDeErros é o alerta, e o erro fica na descrição do campo. |
| Filho com `id` próprio | O `id` do filho prevalece sobre `htmlFor` e sobre o gerado. |

### Relacionados

- [Label](https://phorte-design-system.vercel.app/styleguide/md/Label.md) — O rótulo que ele usa.
- [Input](https://phorte-design-system.vercel.app/styleguide/md/Input.md) — O controle mais comum dentro dele.
- [Select](https://phorte-design-system.vercel.app/styleguide/md/Select.md) — Também vai dentro dele.

## Especificação

### Propriedades

| Prop | Tipo | Padrão | Obrigatória | Descrição |
| --- | --- | --- | --- | --- |
| `label` | `string` | — | sim |  |
| `as` | `'div' \| 'fieldset'` | `'div'` | não | `fieldset` (E59): o campo vira um **grupo** — `<fieldset>` com `<legend>` —, para rádios, caixas e interruptores que respondem à mesma pergunta. A dica e o erro ficam ligados ao grupo, e o leitor de tela diz o nome do grupo junto de cada opção. Sem `as`, é um campo só. |
| `htmlFor` | `string` | — | não | Só é necessário quando o controle não é um elemento React clonável — um componente de terceiro embrulhado, por exemplo. No caso normal o `FormField` gera o id sozinho e liga tudo. |
| `required` | `boolean` | — | não |  |
| `opcional` | `boolean` | — | não | Escreve "(opcional)" no rótulo. Não use junto com `required`. |
| `disabled` | `boolean` | — | não | Desabilita o controle (repassado a ele) e esmaece o rótulo. |
| `error` | `string` | — | não |  |
| `anunciarErro` | `boolean` | `true` | não | O erro é anunciado na hora, como alerta (padrão). Desligue quando o formulário tem o ResumoDeErros (E85): o resumo recebe o foco e é lido uma vez; com os alertas de cada campo junto, o leitor falaria tudo em sequência, sem por onde começar. O erro continua ligado ao campo. |
| `hint` | `string` | — | não |  |
| `contador` | `{ atual?: number; minimo?: number; maximo?: number }` | — | não | Contador de caracteres (E59), ligado ao campo por `aria-describedby`. Com `maximo` ("142 de 280 caracteres"), `minimo` ("faltam 12 para o mínimo de 50") ou os dois (E152). Sem `atual`, o FormField conta sozinho, pelo valor do controle. Quem usa leitor de tela ouve quanto falta ou sobra quando para de digitar. Regra do Base: não limite nome nem endereço — gente de verdade não cabe no limite de alguém. |
| `classNameDoRotulo` | `string` | — | não | Classe do rótulo (ou da legenda, no grupo). |
| `children` | `React.ReactNode` | — | sim |  |

Aceita também: `Omit<React.HTMLAttributes<HTMLElement>, 'children'>`.

### Tokens de cor

| Token | Valor | Usado como |
| --- | --- | --- |
| `--destructive` | #BA0517 | `text-destructive` |
| `--muted-foreground` | #595959 | `text-muted-foreground` |

## Acessibilidade

### Leitor de tela

- O ícone do erro é `aria-hidden`: quem ouve já recebe o texto e o alerta.
- O FormField não recebe foco; o Tab vai direto ao controle, que é lido com o rótulo, a dica e o erro.
- Uma descrição que o controle já traz (`aria-describedby` próprio) **soma** à da dica e do erro, primeiro a dele — conferido no teste da ficha.

### O que sustenta o estado

Usa os padrões: formulario, regiaoViva. Falta ouvir com leitor de tela: formulario, regiaoViva.

## Status e recursos

| Tipo | Recurso | Status |
| --- | --- | --- |
| Código | `app/components/molecules/FormField.tsx` | Disponível |
| Ficha | https://main--69b4847d498a9bd8659db9ba.chromatic.com/?path=/docs/components-complex-formfield--docs | Disponível |
| Teste de acessibilidade | axe em `npm run test` | No gate de todo commit |
| Leitor de tela | Passada da E33 | Falta ouvir |
| Design | Figma | Desativado até a Fase 2 |

### Histórico

- `251416b` 2026-10-01 — E159: Testimonials — o vídeo toca dentro do cartão, YouTube e Vimeo
- Histórico completo no GitHub: https://github.com/srbunnie/Pos-Phorte-Design-System/commits/main/app/components/molecules/FormField.tsx
