# FormField context

> Links a field to its label, description and error for screen readers; passes status, disabled and required down.

Primitive · forms. Registry item `form-field-context`, shown in the vega Style. JSON: https://nativecn.dev/r/styles/vega/form-field-context.json

## Installation

```sh
npx nativecn-cli@latest add form-field-context
```

## Props

### FormFieldProvider

| Prop | Type and description |
|---|---|
| `label` | string: the visible label and the control's accessible name |
| `description` | string: helper text, read as the control's hint while there is no error |
| `error` | string: read as the hint instead, and announced once when it appears |
| `status` | 'error' \| 'success' (defaults to 'error' while error is set) |
| `disabled` | boolean (default false) |
| `required` | boolean: adds "required" to the spoken name (default false) |
| `children` | the field's control |

### useFormField

| Prop | Type and description |
|---|---|
| `useFormField()` | FormFieldState \| null: label, description, error, status, disabled, required and accessibilityProps to spread onto the control; null outside a field |

### FormFieldContext

| Prop | Type and description |
|---|---|
| `value` | FormFieldState \| null: the raw context |

## Accessibility

- FormField renders FormFieldProvider; a custom control calls useFormField() and spreads accessibilityProps onto its focusable element, so it reads as one element (name, then hint or error).

## Notes

Controls take status and disabled from the field unless set on the control itself.

## Source

### {components}/primitives/form-field-context.tsx

```tsx
import { createContext, use, useEffect, useMemo, useRef, type ReactNode } from "react";

import { announce } from "@/utils/announce";

export type FormFieldStatus = "error" | "success";

export type FormFieldProps = {
  /** Visible label text. Also becomes the control's accessible name. */
  label?: string;
  /** Helper text read after the name when there is no error. */
  description?: string;
  /** Error message. Read as the control's description and announced once when it appears. */
  error?: string;
  /** Defaults to `"error"` while `error` is set. The screen sets and clears it. */
  status?: FormFieldStatus;
  disabled?: boolean;
  /** Adds "required" to the spoken name. */
  required?: boolean;
  children?: ReactNode;
};

/**
 * Accessibility props for the field's control. iOS has no `aria-labelledby`, so the label
 * text is copied into `aria-label`; the error (or description) becomes the hint. Together with
 * the control's own role this reads "Email, text field, Enter a valid email".
 */
export type FormFieldAccessibilityProps = {
  "aria-label"?: string;
  "aria-disabled"?: boolean;
  accessibilityHint?: string;
};

export type FormFieldState = {
  label?: string;
  description?: string;
  error?: string;
  status?: FormFieldStatus;
  disabled: boolean;
  required: boolean;
  accessibilityProps: FormFieldAccessibilityProps;
};

export const FormFieldContext = createContext<FormFieldState | null>(null);

/**
 * Behaviour-only provider behind FormField: passes the label, description, error, status,
 * disabled and required down to the control inside it. Renders no views.
 */
export function FormFieldProvider({
  label,
  description,
  error,
  status,
  disabled = false,
  required = false,
  children,
}: FormFieldProps) {
  const lastAnnounced = useRef<string | undefined>(undefined);

  useEffect(() => {
    // Announce once when an error appears (or its text changes), never on unrelated re-renders.
    if (error && error !== lastAnnounced.current) announce(error);
    lastAnnounced.current = error || undefined;
  }, [error]);

  const value = useMemo<FormFieldState>(() => {
    const name = label ? (required ? `${label}, required` : label) : undefined;
    const hint = error || description || undefined;
    return {
      label,
      description,
      error: error || undefined,
      status: status ?? (error ? "error" : undefined),
      disabled,
      required,
      accessibilityProps: {
        ...(name !== undefined && { "aria-label": name }),
        ...(hint !== undefined && { accessibilityHint: hint }),
        ...(disabled && { "aria-disabled": true }),
      },
    };
  }, [label, description, error, status, disabled, required]);

  return <FormFieldContext value={value}>{children}</FormFieldContext>;
}

/** The enclosing FormField's state, or `null` when the control is used on its own. */
export function useFormField(): FormFieldState | null {
  return use(FormFieldContext);
}
```
