<Meta title="FP.REACT Forms/Form/Readme" />

# Form Components

A comprehensive set of accessible React form components built with TypeScript,
designed for building robust, WCAG 2.1 AA compliant forms with proper
validation, error handling, and status management.

> 💡 **Interactive Examples**: See the
> [Form stories](./?path=/docs/fp-react-forms-form--docs) for live, interactive
> examples with automated accessibility testing.

## Features

- ✅ **WCAG 2.1 AA Compliant** - Full accessibility support with proper ARIA
  attributes
- ✅ **Compound Component Pattern** - Intuitive API with Form.Field, Form.Input,
  etc.
- ✅ **TypeScript First** - Full type safety with comprehensive interfaces
- ✅ **Status Management** - Built-in loading states and submission tracking
- ✅ **Validation Support** - Client-side and server-side validation patterns
- ✅ **Flexible** - Supports both controlled and uncontrolled form patterns
- ✅ **Keyboard Navigation** - Full keyboard accessibility including Enter key
  handlers

---

## Components Overview

### Core Components

| Component         | Purpose                                 | Key Props                                   |
| ----------------- | --------------------------------------- | ------------------------------------------- |
| **Form**          | Form wrapper with submission handling   | `onSubmit`, `status`, `noValidate`          |
| **Form.Field**    | Label + input wrapper for accessibility | `label`, `labelFor`, `required`, `optional` |
| **Form.Input**    | Text input with validation              | `type`, `validationState`, `onEnter`        |
| **Form.Textarea** | Multi-line text input                   | `rows`, `cols`, `onEnter`                   |
| **Form.Select**   | Dropdown select with options            | `onSelectionChange`, `onEnter`              |

---

## Installation & Import

```tsx
import Form from "@fpkit/acss";
// Or import specific components
import { Form, Input, Field } from "@fpkit/acss";
```

---

## Basic Usage

### Simple Contact Form

A basic form with required fields, proper label associations, and submission
handling.

```tsx
import Form from "@fpkit/acss";

function ContactForm() {
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    // Handle form submission
  };

  return (
    <Form onSubmit={handleSubmit} aria-label="Contact form">
      <Form.Field label="Name" labelFor="name" required>
        <Form.Input id="name" name="name" required />
      </Form.Field>

      <Form.Field label="Email" labelFor="email" required>
        <Form.Input id="email" name="email" type="email" required />
      </Form.Field>

      <Form.Field label="Message" labelFor="message">
        <Form.Textarea id="message" name="message" rows={4} />
      </Form.Field>

      <button type="submit">Submit</button>
    </Form>
  );
}
```

---

## Advanced Patterns

### Form with Status Management

Use the `status` prop to manage form submission states. This example shows how
the form automatically sets `aria-busy` and disables fields during submission.

```tsx
import { useState } from "react";
import Form, { FormStatus } from "@fpkit/acss";

function RegistrationForm() {
  const [status, setStatus] = useState<FormStatus>("idle");

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    setStatus("submitting");

    try {
      const formData = new FormData(e.currentTarget);
      await fetch("/api/register", {
        method: "POST",
        body: formData,
      });
      setStatus("success");
    } catch (error) {
      setStatus("error");
    }
  };

  const isSubmitting = status === "submitting";

  return (
    <Form
      status={status}
      onSubmit={handleSubmit}
      aria-label="Registration form"
    >
      <Form.Field label="Username" labelFor="username" required>
        <Form.Input
          id="username"
          name="username"
          disabled={isSubmitting}
          required
        />
      </Form.Field>

      <Form.Field label="Password" labelFor="password" required>
        <Form.Input
          id="password"
          name="password"
          type="password"
          minLength={8}
          disabled={isSubmitting}
          required
        />
      </Form.Field>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Submitting..." : "Create Account"}
      </button>

      {status === "success" && <p>Account created successfully!</p>}
      {status === "error" && <p>Error creating account. Please try again.</p>}
    </Form>
  );
}
```

### Form with Validation

Implement client-side validation with error messages. Notice how the input uses
`validationState` to show visual feedback and `aria-invalid` for screen readers.

```tsx
import { useState } from "react";
import Form from "@fpkit/acss";

function ValidatedForm() {
  const [email, setEmail] = useState("");
  const [emailError, setEmailError] = useState("");

  const validateEmail = (value: string) => {
    if (!value) {
      setEmailError("Email is required");
    } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
      setEmailError("Please enter a valid email address");
    } else {
      setEmailError("");
    }
  };

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (!emailError) {
      // Submit form
    }
  };

  return (
    <Form onSubmit={handleSubmit}>
      <Form.Field
        label="Email"
        labelFor="email"
        required
        errorMessage={emailError}
      >
        <Form.Input
          id="email"
          name="email"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          onBlur={(e) => validateEmail(e.target.value)}
          validationState={emailError ? "invalid" : email ? "valid" : "none"}
          required
        />
      </Form.Field>

      <button type="submit">Submit</button>
    </Form>
  );
}
```

### Keyboard-Driven Workflows

Use the `onEnter` prop for keyboard-friendly interactions. This example
demonstrates `onEnter` on inputs, textareas, and selects.

```tsx
import { useState } from "react";
import Form from "@fpkit/acss";

function SearchForm() {
  const [query, setQuery] = useState("");

  const handleSearch = () => {
    console.log("Searching for:", query);
    // Perform search
  };

  return (
    <Form aria-label="Search form">
      <Form.Field
        label="Search"
        labelFor="search"
        hintText="Press Enter to search"
      >
        <Form.Input
          id="search"
          name="search"
          type="search"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          onEnter={handleSearch}
        />
      </Form.Field>
    </Form>
  );
}
```

### Uncontrolled Form with Native Submission

For server-side form handling:

```tsx
function ServerSideForm() {
  return (
    <Form action="/api/contact" formMethod="post">
      <Form.Field label="Name" labelFor="name" required>
        <Form.Input id="name" name="name" required />
      </Form.Field>

      <Form.Field label="Email" labelFor="email" required>
        <Form.Input id="email" name="email" type="email" required />
      </Form.Field>

      <button type="submit">Send</button>
    </Form>
  );
}
```

---

## Disabled State

Form components support an accessible disabled state using the `aria-disabled`
pattern, which provides better accessibility than the native HTML `disabled`
attribute.

### Why aria-disabled?

The `aria-disabled` pattern offers key advantages:

- **Keyboard Accessibility**: Disabled elements remain in the tab order,
  allowing keyboard users to discover them
- **Screen Reader Discovery**: Screen readers can announce the disabled state
  and read associated tooltips or help text
- **WCAG Compliance**: Meets WCAG 2.1.1 (Keyboard) and 4.1.2 (Name, Role, Value)
  requirements
- **Interactive Help**: Enables tooltips or contextual help on disabled elements
  to explain why they're disabled

### Basic Usage

Use the `disabled` prop to disable form inputs, textareas, and selects:

```tsx
import Form from "@fpkit/acss";

function DisabledInputExample() {
  return (
    <Form>
      <Form.Field label="Email" labelFor="email">
        <Form.Input
          id="email"
          name="email"
          type="email"
          disabled={true}
          value="locked@example.com"
        />
      </Form.Field>

      <Form.Field label="Comments" labelFor="comments">
        <Form.Textarea id="comments" name="comments" disabled={true} />
      </Form.Field>

      <Form.Field label="Country" labelFor="country">
        <Form.Select id="country" name="country" disabled={true}>
          <option value="us">United States</option>
          <option value="ca">Canada</option>
        </Form.Select>
      </Form.Field>
    </Form>
  );
}
```

### Migration from isDisabled

For backward compatibility, the deprecated `isDisabled` prop is still supported
but will be removed in a future version.

**Before (deprecated):**

```tsx
<Form.Input isDisabled={true} />
```

**After (recommended):**

```tsx
<Form.Input disabled={true} />
```

Both props work identically, but `disabled` follows standard HTML conventions
and should be used for all new code.

### Disabled State During Form Submission

A common pattern is disabling all form fields while a form is submitting:

```tsx
import { useState } from "react";
import Form, { FormStatus } from "@fpkit/acss";

function SubmitDisabledForm() {
  const [status, setStatus] = useState<FormStatus>("idle");

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setStatus("submitting");

    try {
      const formData = new FormData(e.currentTarget);
      await fetch("/api/save", {
        method: "POST",
        body: formData,
      });
      setStatus("success");
    } catch (error) {
      setStatus("error");
    }
  };

  const isSubmitting = status === "submitting";

  return (
    <Form status={status} onSubmit={handleSubmit}>
      <Form.Field label="Username" labelFor="username" required>
        <Form.Input
          id="username"
          name="username"
          disabled={isSubmitting}
          required
        />
      </Form.Field>

      <Form.Field label="Bio" labelFor="bio">
        <Form.Textarea id="bio" name="bio" disabled={isSubmitting} />
      </Form.Field>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Saving..." : "Save Profile"}
      </button>
    </Form>
  );
}
```

### Behavior

When a form control is disabled:

- **Keyboard Navigation**: Element remains focusable via Tab key (maintains tab
  order)
- **Interaction Prevention**: All interaction events are prevented (click,
  change, keydown, etc.)
- **Focus Events**: Focus events still work, allowing screen readers to discover
  and announce the element
- **Visual Styling**: `.is-disabled` class and `aria-disabled="true"` attribute
  are applied
- **Screen Readers**: Announce "disabled" state when focused

### Styling

Disabled elements can be styled using CSS custom properties:

```css
:root {
  --disabled-opacity: 0.6; /* Visual opacity for disabled state */
  --disabled-cursor: not-allowed; /* Cursor style */
  --disabled-color: hsl(0 0% 40%); /* Text color (3:1 contrast minimum) */
}
```

Override these properties for custom styling:

```tsx
<Form.Input
  disabled={true}
  styles={{
    "--disabled-opacity": "0.5",
    "--disabled-color": "#666666",
  }}
/>
```

**Selectors Available:**

- `.is-disabled` - Class added to disabled elements
- `[aria-disabled="true"]` - Attribute selector

### WCAG Compliance

The `aria-disabled` pattern ensures compliance with:

- **WCAG 2.1.1 (Keyboard)**: Elements remain in keyboard tab order for discovery
- **WCAG 4.1.2 (Name, Role, Value)**: `aria-disabled` announces state to screen
  readers
- **WCAG 1.4.3 (Contrast Minimum)**: Disabled text maintains 3:1 contrast ratio
- **WCAG 2.4.7 (Focus Visible)**: Focus indicators preserved on disabled
  elements

---

## Component API

### Form

The main form wrapper component.

#### Props

| Prop         | Type                                             | Default  | Description                                |
| ------------ | ------------------------------------------------ | -------- | ------------------------------------------ |
| `onSubmit`   | `(event: FormEvent) => void`                     | -        | Form submission handler (prevents default) |
| `status`     | `'idle' \| 'submitting' \| 'success' \| 'error'` | `'idle'` | Current form status                        |
| `action`     | `string`                                         | -        | Form submission URL                        |
| `formMethod` | `'get' \| 'post'`                                | `'post'` | HTTP method                                |
| `noValidate` | `boolean`                                        | `false`  | Disable HTML5 validation                   |
| `target`     | `string`                                         | -        | Form submission target                     |
| `id`         | `string`                                         | -        | Unique form identifier                     |
| `name`       | `string`                                         | -        | Form name attribute                        |
| `classes`    | `string`                                         | -        | CSS class names                            |
| `styles`     | `CSSProperties`                                  | -        | Inline styles                              |

#### Accessibility

- **role**: Automatically set to `"form"`
- **aria-busy**: Set to `true` when `status="submitting"`
- **data-status**: Reflects current status for CSS styling

### Form.Field

Wrapper component that associates labels with inputs for accessibility.

#### Props

| Prop           | Type        | Default | Description                            |
| -------------- | ----------- | ------- | -------------------------------------- |
| `label`        | `ReactNode` | -       | **Required.** Label text or element    |
| `labelFor`     | `string`    | -       | ID of associated input (for `htmlFor`) |
| `required`     | `boolean`   | `false` | Show required indicator (\*)           |
| `optional`     | `boolean`   | `false` | Show optional indicator                |
| `errorMessage` | `string`    | -       | Error message to display               |
| `hintText`     | `string`    | -       | Helper text below input                |

### Form.Input

Text input component with validation support.

#### Props

| Prop              | Type                             | Default  | Description                                                     |
| ----------------- | -------------------------------- | -------- | --------------------------------------------------------------- |
| `type`            | `string`                         | `'text'` | Input type (text, email, password, etc.)                        |
| `validationState` | `'none' \| 'valid' \| 'invalid'` | `'none'` | Validation state                                                |
| `errorMessage`    | `string`                         | -        | Error message for `aria-describedby`                            |
| `hintText`        | `string`                         | -        | Hint text for `aria-describedby`                                |
| `onEnter`         | `(event: KeyboardEvent) => void` | -        | Handler for Enter key press                                     |
| `disabled`        | `boolean`                        | `false`  | Disable input using `aria-disabled` pattern (remains focusable) |
| `isDisabled`      | `boolean`                        | `false`  | **Deprecated.** Use `disabled` instead                          |
| `readOnly`        | `boolean`                        | `false`  | Make input read-only                                            |
| `required`        | `boolean`                        | `false`  | Mark input as required                                          |

### Form.Textarea

Multi-line text input component.

#### Props

| Prop         | Type                             | Default | Description                                                        |
| ------------ | -------------------------------- | ------- | ------------------------------------------------------------------ |
| `rows`       | `number`                         | `5`     | Number of visible rows                                             |
| `cols`       | `number`                         | `25`    | Number of visible columns                                          |
| `onEnter`    | `(event: KeyboardEvent) => void` | -       | Handler for Enter (without Shift)                                  |
| `disabled`   | `boolean`                        | `false` | Disable textarea using `aria-disabled` pattern (remains focusable) |
| `isDisabled` | `boolean`                        | `false` | **Deprecated.** Use `disabled` instead                             |

**Note**: Shift+Enter adds a new line without triggering `onEnter`.

### Form.Select

Dropdown select component with keyboard support.

#### Props

| Prop                | Type                             | Default | Description                                                      |
| ------------------- | -------------------------------- | ------- | ---------------------------------------------------------------- |
| `onSelectionChange` | `(event: ChangeEvent) => void`   | -       | Selection change handler                                         |
| `onEnter`           | `(event: KeyboardEvent) => void` | -       | Handler for Enter key press                                      |
| `required`          | `boolean`                        | `false` | Mark select as required                                          |
| `disabled`          | `boolean`                        | `false` | Disable select using `aria-disabled` pattern (remains focusable) |
| `isDisabled`        | `boolean`                        | `false` | **Deprecated.** Use `disabled` instead                           |

---

## Accessibility Features

### WCAG 2.1 AA Compliance

All form components meet WCAG 2.1 Level AA standards:

1. **WCAG 3.3.1 Error Identification** ✅

   - Error messages clearly associated with inputs via `aria-describedby`
   - Validation states communicated via `aria-invalid`

2. **WCAG 3.3.2 Labels or Instructions** ✅

   - All inputs have associated labels via `<label htmlFor>`
   - Required fields marked with visual and programmatic indicators

3. **WCAG 4.1.2 Name, Role, Value** ✅

   - All inputs have proper `role`, `name`, and `aria-` attributes
   - Form status communicated via `aria-busy`

4. **WCAG 2.4.7 Focus Visible** ✅

   - Focus indicators styled via `:focus-visible` in SCSS

5. **WCAG 2.1.1 Keyboard** ✅

   - All interactive elements keyboard accessible
   - `onEnter` prop for custom keyboard workflows

6. **Accessible Disabled State** ✅
   - Uses `aria-disabled` pattern instead of native `disabled` attribute
   - Elements remain keyboard focusable (stay in tab order)
   - Screen readers announce disabled state while allowing discovery
   - See [Disabled State](#disabled-state) section for details

### Screen Reader Support

- Form status changes announced via `aria-busy`
- Error messages linked to inputs via `aria-describedby`
- Required fields announced via `aria-required`
- Validation states communicated via `aria-invalid`

### Keyboard Navigation

- **Tab**: Navigate between form fields
- **Shift+Tab**: Navigate backwards
- **Enter**:
  - In Input: Trigger `onEnter` handler
  - In Textarea: Trigger `onEnter` (or new line with Shift)
  - In Select: Trigger `onEnter` after selection
  - In Form: Submit form (default button behavior)
- **Escape**: Clear focus (native browser behavior)

---

## Styling

Form components use CSS custom properties for theming:

```css
:root {
  --input-border-color: gray;
  --input-bg: inherit;
  --input-outline: thin solid var(--input-border-color);
  --input-px: 0.6rem;
  --input-py: 0.4rem;
  --input-fs: var(--fs);
  --input-w: clamp(200px, 100%, 500px);
  --placeholder-color: gray;
  --form-direction: column;
}
```

### Status-Based Styling

Use the `data-status` attribute for CSS styling:

```css
form[data-status="submitting"] {
  opacity: 0.6;
  pointer-events: none;
}

form[data-status="error"] {
  border-left: 0.25rem solid var(--color-error);
}

form[data-status="success"] {
  border-left: 0.25rem solid var(--color-success);
}
```

### Validation Styling

Use the `data-validation` attribute on inputs:

```css
input[data-validation="invalid"] {
  outline-color: var(--color-error);
}

input[data-validation="valid"] {
  outline-color: var(--color-success);
}
```

---

## TypeScript Support

All components are fully typed with comprehensive interfaces:

```tsx
import Form, { FormProps, FormStatus, InputProps } from "@fpkit/acss";

const MyForm: React.FC = () => {
  const [status, setStatus] = useState<FormStatus>("idle");

  const handleSubmit: FormProps["onSubmit"] = (e) => {
    // Fully typed event
  };

  return (
    <Form status={status} onSubmit={handleSubmit}>
      ...
    </Form>
  );
};
```

---

## Testing

Form components are fully tested with Vitest and React Testing Library:

```tsx
import { render, screen, userEvent } from "@testing-library/react";
import Form from "@fpkit/acss";

test("submits form data", async () => {
  const handleSubmit = vi.fn();
  const user = userEvent.setup();

  render(
    <Form onSubmit={handleSubmit}>
      <Form.Input name="email" />
      <button type="submit">Submit</button>
    </Form>
  );

  await user.type(screen.getByRole("textbox"), "test@example.com");
  await user.click(screen.getByRole("button"));

  expect(handleSubmit).toHaveBeenCalled();
});
```

---

## Best Practices

### ✅ DO

- Use `Form.Field` to wrap inputs for proper label association
- Provide `aria-label` or `aria-labelledby` for forms
- Use the `status` prop to manage loading states
- Disable form fields when `status="submitting"`
- Validate on `onBlur` for better UX
- Use `required` attribute for HTML5 validation
- Provide clear error messages

### ❌ DON'T

- Don't forget to associate labels with inputs
- Don't submit forms without validation
- Don't disable submit buttons without explaining why
- Don't use placeholders as labels
- Don't forget to handle keyboard events

---

## More Examples

See the [Form Interactive Guide](./?path=/docs/fp-react-forms-form--docs) for
complete examples including:

### Form with Hint Text

Guide users with helpful hint text below fields, properly associated using
`aria-describedby`.

### Form with Select Dropdown

Combine different form control types for rich data collection.

### Form with Optional Fields

Clearly distinguish between required and optional fields for better UX.

### Complete Registration Form

A comprehensive example combining all features: validation, hints, different
field types, and proper accessibility.

---

## Browser Support

- ✅ Chrome/Edge 90+
- ✅ Firefox 88+
- ✅ Safari 14+
- ✅ Mobile Safari (iOS 14+)
- ✅ Chrome Android

---

## Contributing

Found an issue or have a suggestion? Please
[open an issue](https://github.com/your-repo/issues) or submit a pull request.

---

## License

MIT License - see LICENSE file for details
