# Field

> Un campo: la etiqueta, la ayuda y el error atados al control, sin un solo id escrito a mano.

```tsx
import { Field, FieldControl, FieldDescription, FieldError, … } from "sebs7n-ui/field"
```

## Ejemplos

### Un campo completo

La etiqueta, la ayuda y el error atados al control sin un solo `id` escrito a mano. Comparalo con armar lo mismo suelto: `useId`, `htmlFor`, `aria-describedby` condicional y `aria-invalid`.

```tsx
import { Field, FieldDescription, FieldError, FieldLabel } from "sebs7n-ui/field"
import { Input } from "sebs7n-ui/input"

function Basico() {
  return (
    <Field className="w-full max-w-sm" name="cuit">
      <FieldLabel required>CUIT</FieldLabel>
      <Input placeholder="30712345678" required />
      <FieldDescription>Once dígitos, sin guiones.</FieldDescription>
      <FieldError />
    </Field>
  )
}
```

### Cuándo se valida

`validationMode="onBlur"` marca el error recién al salir del campo. En `onChange` el rojo aparece a la segunda letra del email, cuando todavía falta todo: sirve para un campo que se puede evaluar entero mientras se escribe, como el largo de una contraseña.

```tsx
import { Field, FieldDescription, FieldError, FieldLabel } from "sebs7n-ui/field"
import { Input } from "sebs7n-ui/input"

function Validacion() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-6">
      <Field
        name="email"
        validate={(valor) => (String(valor).includes("@") ? null : "Falta el @: revisá el email")}
        validationMode="onBlur"
      >
        <FieldLabel>Email</FieldLabel>
        <Input placeholder="vos@empresa.com" type="email" />
        <FieldError />
      </Field>

      <Field
        name="password"
        validate={(valor) => (String(valor).length >= 8 ? null : "Mínimo 8 caracteres")}
        validationMode="onChange"
      >
        <FieldLabel>Contraseña</FieldLabel>
        <Input type="password" />
        <FieldDescription>Se valida mientras escribís porque acá sí se puede saber desde el primer carácter.</FieldDescription>
        <FieldError />
      </Field>
    </div>
  )
}
```

### El error que solo conoce el servidor

Que un email ya esté usado no se puede validar en el navegador. `Form` recibe un objeto `{ campo: mensaje }` y cada `FieldError` muestra el suyo, en el campo que corresponde y no en un cartel arriba de todo.

```tsx
import { Button } from "sebs7n-ui/button"
import { Field, FieldError, FieldLabel } from "sebs7n-ui/field"
import { Form } from "sebs7n-ui/form"
import { Input } from "sebs7n-ui/input"
import { useState } from "react"

function ErroresDelServidor() {
  const [errors, setErrors] = useState<Record<string, string>>({})
  const [enviando, setEnviando] = useState(false)

  return (
    <Form
      className="w-full max-w-sm"
      errors={errors}
      onFormSubmit={async (valores) => {
        setEnviando(true)
        await new Promise((r) => setTimeout(r, 600))
        setEnviando(false)
        // El servidor de mentira: cualquier email de este dominio ya está tomado.
        setErrors(
          String(valores.email).endsWith("@acme.com") ? { email: "Ese email ya tiene una cuenta" } : {}
        )
      }}
    >
      <Field name="email">
        <FieldLabel required>Email</FieldLabel>
        <Input placeholder="probá con algo@acme.com" required type="email" />
        <FieldError />
      </Field>
      <Button loading={enviando} type="submit">
        Crear cuenta
      </Button>
    </Form>
  )
}
```

### Obligatorio que valida el servidor

`FieldLabel indicator` dibuja el asterisco y el lector dice «Razón social, obligatorio», sin `required` en el control: el formulario no corta el envío y el error llega del servidor por `Form`.

```tsx
import { Field, FieldError, FieldLabel } from "sebs7n-ui/field"
import { Input } from "sebs7n-ui/input"

function IndicadorSinValidar() {
  return (
    <Field className="w-full max-w-sm" name="businessName">
      <FieldLabel indicator>Razón social</FieldLabel>
      <Input placeholder="Estudio Ruiz S.R.L." />
      <FieldError />
    </Field>
  )
}
```

## Props

### Field

Hereda las props de `Field.Root`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |
| `name` | `string` | — | **Heredada de Base UI.** Identifica al campo en los valores del submit y en el objeto `errors` de `Form`. |
| `validate` | `((value: unknown, formValues: Form.Values) => string \| string[] \| void \| Promise<string \| string[] \| void>)` | — | **Heredada de Base UI.** Devolvé el mensaje si el valor está mal, o `null` si está bien. Puede ser asíncrona. |
| `validationMode` | `"onBlur" \| "onChange" \| "onSubmit"` | — | **Heredada de Base UI.** `onSubmit` (default), `onBlur` u `onChange`. Tiene precedencia sobre el del `Form`. |

### FieldControl

Hereda las props de `Field.Control`.

Sin props propias: pasa todo al primitivo.

### FieldDescription

Hereda las props de `Field.Description`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### FieldError

Hereda las props de `Field.Error`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `alert` | `boolean` | `false` | `role="alert"` para que el error se anuncie al aparecer. Solo con `validationMode="onChange"`. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### FieldLabel

Hereda las props de `Field.Label`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `indicator` | `boolean` | `false` | El asterisco más «, obligatorio» para el lector (`labels.field.required`), sin la restricción nativa: para un campo que valida el servidor. No va con un control `required` o `aria-required`, que lo diría dos veces. |
| `required` | `boolean` | `false` | Dibuja el asterisco. No hace obligatorio al campo: eso es el `required` del control. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### FieldValidity

Hereda las props de `Field.Validity`.

Sin props propias: pasa todo al primitivo.

## Teclado

| Tecla | Qué hace |
|---|---|
| Tab | Entra y sale del control. La etiqueta no recibe foco: al clickearla, lo recibe el control. |

## Accesibilidad

- **Esto es lo que resuelve el componente.** La etiqueta nombra al control, la ayuda y el error lo describen, y el error pone `aria-invalid`: todo por anidar las partes, sin `useId`, sin `htmlFor` y sin armar un `aria-describedby` condicional que es justo lo que se olvida.
- El asterisco de `required` es `aria-hidden`: nadie escucha "Razón social asterisco". Que el campo sea obligatorio lo anuncia el `required` del control.
- Si lo valida el servidor y el control no lleva `required`, `FieldLabel indicator`: el mismo asterisco, y el nombre del control pasa a ser «Razón social, obligatorio» (el texto es `labels.field.required`). Con `required` en la etiqueta también, ese texto se calla: lo anuncia el `required` del control. El del control solo no se detecta: `indicator` va sin `required` ni `aria-required` en el control, o se diría dos veces.
- `FieldError` no ocupa lugar mientras el campo está bien, y cuando aparece ya está referenciado: no hace falta mover el foco para que se lea.
- **Escribí siempre el mensaje, con `match` o con `validate`.** El del navegador sale en el idioma del navegador y no en el de la página: con Chrome en inglés, abajo de «Razón social» aparece «Please fill out this field». `match="valueMissing"` y compañía además hablan del dato —«Falta la razón social»— en vez del input.
- El mensaje se lee al enfocar el campo, porque el campo lo referencia con `aria-describedby`. Con `validationMode="onChange"` eso no alcanza: el error aparece con el foco ya adentro y nada lo anuncia. Para ese caso está `alert`, que le pone `role="alert"`. Es opt-in porque `role="alert"` interrumpe: en el camino de enviar duplicaría el anuncio y cortaría el del nombre del campo, que es la mitad que da contexto.
- `Input`, `Textarea` y `Select` se enganchan solos. Para cualquier otro control va `FieldControl` con `render`.
- **Un control propio se engancha solo si reenvía lo que recibe.** `FieldControl` le pasa `id`, `name`, `aria-describedby`, `aria-invalid` y una `ref`; un componente que declara `id` y `name` como props propias y no hace spread del resto se queda sin nada, y la etiqueta del campo apunta al vacío. Pasa seguido con un date picker o un autocomplete hechos con un `<input type="hidden">` más un botón: el arreglo va adentro de ese componente, no en cada uso.

## Reglas de uso

- **El `name` es la bisagra con `Form`**: es la clave de los valores del submit y la del objeto `errors` que devuelve el servidor. Para datos anidados se usa punto (`domicilio.calle`), que es lo que devuelve el adaptador de schemas.
- **`validationMode="onSubmit"` (el default) casi siempre.** Marcar el email en rojo mientras se escribe es castigar a alguien por no haber terminado. `onBlur` para un dato que recién se puede juzgar completo; `onChange` solo cuando se puede evaluar desde el primer carácter, como el largo de una contraseña.
- La ayuda va visible en `FieldDescription`, no en un tooltip: una ayuda que hay que descubrir no ayuda a quien más la necesita.
- Un mensaje propio para un motivo puntual se escribe con `match` (`<FieldError match="valueMissing">Falta el email</FieldError>`): habla del dato, no del input.
- Validar contra Zod, Valibot o ArkType: `fieldValidator` de `sebs7n-ui/lib/schema`.
- **`FieldError` es una fuente o la otra, no las dos.** Sin `match` se muestra ante *cualquier* invalidez —la del navegador y la que vino del servidor—, así que puesto al lado de uno con `match` imprime el mensaje dos veces. Si escribís mensajes propios con `match`, renderizá el genérico solo cuando el servidor devolvió algo para ese campo.
- Un mensaje propio por `match` no es adorno: el del navegador sale en el idioma del navegador, no en el de la página, y habla del input ("complete este campo") en vez del dato que se pide.

## Relacionados

[form](/docs/components/form.md) · [fieldset](/docs/components/fieldset.md) · [input](/docs/components/input.md) · [select](/docs/components/select.md)
