<!-- Generated by scripts/gen-docs.mts from the JSDoc in registry/. Do not edit; run `pnpm run docs`. -->

# EditSheet

Edits something in a form inside a dialog: the whole interaction, done right. It opens from `trigger` as a centered dialog, or a bottom sheet on phones, with your fields, Cancel and a save button. Saving runs the browser's checks, then `onSubmit` with the fields as `FormData`; a promise shows `loading` on the save button and keeps the sheet open (it cannot be dismissed meanwhile). Errors it returns land on the matching fields, with focus on the first; a failed save shows `failureMessage` at the top of the form; success closes the sheet and returns focus to the trigger. Dismissing it after changing a field (Escape, a tap outside, a swipe down, Cancel) asks inside the sheet before the edits are lost. With controlled fields, reset your state in `onDiscard`, or the discarded values come back on the next opening.

```tsx
import { EditSheet } from "@/components/ouro/form/edit-sheet";
```

## Example

```tsx
<EditSheet
  title="Edit recipe"
  trigger={<Button variant="outline">Edit</Button>}
  onSubmit={async (data) => (await saveRecipe(data)).errors}
>
  <TextField label="Name" name="name" defaultValue={recipe.name} required />
  <Select label="Course" name="course" defaultValue={recipe.course} options={courses} />
</EditSheet>
```

## Use instead when

- Use `Form` instead for a form on the page, `Dialog` for a dialog that is not a form, and `ConfirmDialog` to confirm one action.

## Props

The props the modes share. Type: `EditSheetProps`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `cancelLabel` | `string` | `"Cancel"` | The cancel button's label. Translate it with your app. |
| `children` (required) | `ReactNode` |  | The fields: `TextField`, `Select` and other controls with a `name`. |
| `className` | `string` |  | Layout only, applied to the panel: margins (`mt-4`). Your classes win over Ouro UI's. Appearance classes warn in development. |
| `closeLabel` | `string` | `"Close"` | The label of the close button in the corner. Translate it with your app. |
| `description` | `ReactNode` |  | A short line under the title, read out when the sheet opens. |
| `discardLabel` | `string` | `"Discard"` | The button that discards the edits and closes the sheet. |
| `discardMessage` | `string` | `"Discard your changes?"` | The question the sheet asks, in place of its buttons, when the user dismisses it (Escape, a tap outside, a swipe down, Cancel or the close button) after changing a field. Translate it with your app. |
| `failureMessage` | `string` | `"Couldn't save. Try again."` | What the sheet shows, at the top of the form, when `onSubmit` rejects or throws. Translate it with your app. |
| `hideTitle` | `boolean` |  | Hides the title visually. It still names the sheet for screen readers. |
| `keepEditingLabel` | `string` | `"Keep editing"` | The button that goes back to the form with the edits kept; it takes focus. |
| `onDiscard` | `(() => void)` |  | Called when the user confirms Discard, before the sheet closes. With controlled fields (`value` and `onValueChange`), reset your state here: the sheet cannot reset values it does not own, so they would come back on the next opening: `<TextField name="name" value={name} onValueChange={setName} />` with `onDiscard={() => setName(recipe.name)}`. Not called for a save, or a dismissal with nothing changed. |
| `onSubmit` (required) | `(data: FormData) => EditSheetResult \| Promise<EditSheetResult>` |  | Saves the form. It gets the fields as `FormData`, after the browser's own checks (`required`, `type="email"`) pass. Return nothing to close the sheet and return focus to the trigger, or errors by field name (`{ name: "That name is taken." }`) to keep it open with each message on its field and focus on the first; a `form` key, or a key no field is named, shows in an alert at the top of the form. Return a promise to show `loading` on the save button until it settles; meanwhile Cancel is disabled and the sheet cannot be dismissed. If it rejects (or throws), the sheet stays open with `failureMessage`. |
| `submitLabel` | `string` | `"Save"` | The save button's label: the action, such as `"Save recipe"`. Translate it with your app. |
| `title` (required) | `string` |  | Names the sheet for screen readers and shows at its top: `title="Edit recipe"`. Hide it with `hideTitle`. |

## Modes

`EditSheetProps` is a union: pass the props of exactly one mode. Mixing them is a type error.

### Controlled: `EditSheetControlledProps`

A controlled EditSheet: you own `open` and update it in `onOpenChange`.

| Prop | Type | Description |
| --- | --- | --- |
| `onOpenChange` (required) | `(open: boolean) => void` | Called with `false` when the user cancels or dismisses the sheet (after confirming, with edits), and when a save succeeds. |
| `open` (required) | `boolean` | Whether the sheet is open. |

Not in this mode (typed `never`):

- `trigger`: Not with `open`. Remove `open` and `onOpenChange` to let a trigger open the sheet.

### With trigger: `EditSheetWithTriggerProps`

An uncontrolled EditSheet: `trigger` opens it.

| Prop | Type | Description |
| --- | --- | --- |
| `trigger` (required) | `ReactElement` | The element that opens the sheet, usually a Button: `trigger={<Button>Edit</Button>}`. |

Not in this mode (typed `never`):

- `onOpenChange`: Not with `trigger`. Remove `trigger` to control the sheet with `open` and `onOpenChange`.
- `open`: Not with `trigger`. Remove `trigger` to control the sheet with `open` and `onOpenChange`.

## Don't use

These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.

- `errors`: Not a prop. Return errors from `onSubmit`: `onSubmit={async (data) => (await save(data)).errors}`.
- `isOpen`: Not a prop. Ouro UI uses `open` with `onOpenChange`: `<EditSheet open={open} onOpenChange={setOpen}>`.
- `onClose`: Not a prop. `onOpenChange(false)` fires on close: `<EditSheet open={open} onOpenChange={setOpen}>`.
- `onSave`: Not a prop. Ouro UI uses `onSubmit`: `<EditSheet onSubmit={(data) => save(data)}>`.
- `saveLabel`: Not a prop. Ouro UI uses `submitLabel`: `<EditSheet submitLabel="Save">`.
- `submitText`: Not a prop. Ouro UI uses `submitLabel`: `<EditSheet submitLabel="Save">`.

## `className` is for layout only

Pass margins, width and grid placement (`mt-4`, `w-full`, `col-span-2`).

Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (`hover:`, `sm:`) and any `!` are removed, it starts with `bg-`, `text-`, `border-`, `rounded-`, `shadow-`, `font-`, `ring-`, `from-`, `via-`, `to-`, `fill-` or `stroke-`, or is `border`, `rounded`, `shadow` or `ring`, or has a `dark:` variant. Text wrapping, overflow and table layout classes (`text-wrap`, `text-ellipsis`, `border-collapse`) are layout and do not warn.

Alignment classes (`text-center`) warn too: set alignment on a parent element.

## Types

### `EditSheetResult`

What `onSubmit` may return: nothing (saved), or errors by field name (not saved).

```ts
type EditSheetResult = void | undefined | ReadonlyFormErrors;
```

### `ErrorMessage`

One error message: text, or a React element for a message that carries a link or other markup.

```ts
type ErrorMessage = string | ReactElement;
```

### `ReadonlyErrorMessages`

A read-only message view accepted by fields and Form; mutable input arrays remain valid.

```ts
type ReadonlyErrorMessages = ErrorMessage | readonly ErrorMessage[];
```

### `ReadonlyFormErrors`

A read-only display projection for Form and EditSheet; mutable raw `FormErrors` remain valid inputs.

```ts
type ReadonlyFormErrors = Readonly<Record<string, ReadonlyErrorMessages | null | undefined>>;
```
