import { Canvas, Controls, Meta } from "@storybook/addon-docs/blocks";

import * as FieldStories from "./Field.stories";

<Meta of={FieldStories} />

# Field

Sixteen files in the fleet spell a label, a control, and a hint, and no two spell the
wiring the same way.

An earlier version of this page said mux-magic's `FieldLabel` renders a `<label>` with
**no `htmlFor`**. That is false, and worth correcting rather than quietly deleting: it
renders ``htmlFor={`${stepId}-${field.name}`}``, and has since it was written. The defect
is at the *other* end of the pair — **8 of its 16 call sites never render that id on
anything**. Measured on `mux-magic@master`, the eight that do are `BooleanField`,
`ChapterSplitsField`, `EnumField`, `NumberField`, `NumberWithLookupField`, `PathField`,
`StringArrayField` and `StringField`. Of the rest, `RegexWithFlagsField` mints its own
`patternId` / `flagsId` / `sampleId` and `SubtitleRulesField` spells
``${step.command}-hasDefaultRules`` where the label says ``${step.id}-…``.

The outcome is the same unnamed textbox by a worse route: a `<label>` pointing at an id
that is nowhere in the document reads as correct in the label's own file, and is only
wrong in the *other* file. Half of them are broken and nothing says which half. That is
the argument for a component rather than a convention — here the id is minted and consumed
in one place and cannot be spelled twice.

<Canvas of={FieldStories.Playground} />

`Field` is also how a `Picker` (or `Button`, or an app's own control) sits next to
its label **without butting against the last word**. Those controls are
flush — no outer margin — on purpose. The stacked `gap-1.5` here is the
gutter. An inline label + trigger is a row with `gap-*`, not a margin
on the control. See [Picker](../Picker/Picker.mdx#the-trigger-is-flush--the-parent-owns-the-gap).

## Props

<Controls />

## The control is a slot

Cloned, not wrapped — the same contract as `Popover`'s trigger. An `<input>`, a
`<textarea>`, a `Picker`, or an app's own control all go in the same hole, and the `id`,
`aria-describedby`, `aria-invalid` and `required` are injected onto whatever arrives.

<Canvas of={FieldStories.AllVariants} />

## A slot is a pass-through

Which is a rule this component needed and did not have. `Field` and `Tooltip` both clone
onto their single child, so the obvious thing to write —

```tsx
<Field label="Rename pattern" isRequired>
  <Tooltip label="A JavaScript regular expression.">
    <input />
  </Tooltip>
</Field>
```

— handed `id`, `aria-describedby`, `aria-invalid` and `required` to the **`Tooltip`
component**, which declares none of them. `cloneElement` does not care. React drops all
four without a warning, TypeScript never sees it (`Children.only` returns a `ReactElement`
whose props are `any`), every test still passed, and the render was pixel-identical. The
only symptom was a `<label htmlFor>` pointing at an id nowhere in the document — the exact
defect above, reproduced by the component built to prevent it, and this time by the
library rather than by a consumer.

**Silent prop-dropping is the worst version of this**, so anything a slot receives from
above is forwarded to the child it clones onto, and a chain of slots behaves like the one
control at the bottom of it. `aria-describedby` — the one prop that is a *list* — is
merged rather than overwritten, outer first.

It works in both orders, and the second is not symmetrical: a `Tooltip` around a `Field`
hands down not one attribute but a working component — floating-ui's hover, focus and
dismiss handlers and `refs.setReference`. Those reach the control too, or the tip is a
floating node with no trigger and no anchor.

<Canvas of={FieldStories.Nested} />

## Where a prop lands

`FieldProps` and `FieldGroupProps` were **closed** types until 2026-08-21: the only prop a
caller could legally pass was `className`. That is the sharpest argument for opening them —
a component whose whole purpose is *configured by props, not a borrowed class* accepted only
the escape hatch that rule exists to close, so it forced the very thing the rule forbids.
queuepilot paid for it twice in one modal: a `FieldGroup` that gave up the `id` its
screenshot suite drives by, and a box beside it that stayed a hand-rolled `<fieldset>`
because it needed `hidden`.

The rule that opens them is one sentence — **a component's props land on the element it
owns** — and it reads differently in each component because they own different elements:

| | `Field` | `FieldGroup` |
| --- | --- | --- |
| Renders | `<div>` around a **cloned** child | a `<fieldset>` around children rendered as-is |
| Rest props land on | the **control** | the **`<fieldset>`** |
| `className` lands on | the `<div>` | the `<fieldset>` |
| So you can pass | `name`, `placeholder`, `autoFocus`, `aria-*`, `ref` | `id`, `hidden`, `ref`, and `disabled` / `form` / `name` |

`Field`'s destination is not a new one, it is the existing one finally declared: the runtime
already spread everything it received into the clone, and `id` has named the control since
the component shipped. Routing the rest to the `<div>` would have moved that `id`, silently,
in every consumer.

It also could not have been done safely. A slot is a pass-through and **cannot tell who
wrote a prop** — a `Tooltip` above hands down `getReferenceProps()`, and the author types on
the same object. Holding the author's props back at the `<div>` needs a closed runtime
allow-list, and an allow-list is `FieldTooltip`'s silent drop rebuilt inside the fix for it:
the first slot that writes an `aria-expanded` breaks, and nothing says so.

So a caller who wants a handle on the **box** — an `id` for a screenshot, a `hidden` that
takes the label with it — wants `FieldGroup`. It wraps rather than clones, so its props have
nowhere else to go.

`disabled` on a `<fieldset>` is worth knowing before reaching for it: the platform disables
every control inside the element, not just the box. That is usually what a caller means, and
it is something a `<div>`-based group could not have offered at all.

<Canvas of={FieldStories.ForwardsRestProps} />

## Several controls are a `FieldGroup`, not a `Field`

`Field` mints one id and clones it onto one child. An `id` names one element and a
`<label htmlFor>` points at one, so the moment there are two controls the label names one
of them and leaves the rest anonymous. Six of mux-magic's sixteen field components are in
that position — `RegexWithFlagsField` (pattern, flags, sample, and a checkbox),
`NumberWithLookupField`, `LanguageCodeField`, `LanguageCodesField`, `SubtitleTypesField`
and `RenameRegexField`.

`FieldGroup` renders `<fieldset>` + `<legend>` and does **not** clone. This is the one
place in the library where `<fieldset>` is right: `AccordionSection` rejected it for its
panel precisely because that element means *form-control grouping*, and here the content
really is one. `<legend>` names the group natively, so no `role="group"` and no
`aria-labelledby`.

What a group cannot carry is `aria-invalid`, which has no group form — it belongs on the
control that is actually invalid, and a `FieldGroup` does not know which one that is. So
`error` here is **described, not asserted**: it joins the `<fieldset>`'s
`aria-describedby`. Cloning `aria-invalid` onto every child would mark the valid ones
invalid. A control that is itself invalid says so in its own `Field`.

<Canvas of={FieldStories.Group} />

## `createLinkedIds` is *not* here, and that is the finding

The plan expected `useLinkedIds` to do this wiring. It does not fit, for the same reason it
did not fit `Tabs`: that kind exists for a **dynamic** pairing, where the registration
multiset is what stops an `aria-describedby` naming a node React has already removed.

A field renders its own description and its own error, so it knows *at render* which exist.
The ids come from one `useUniqueId` and a conditional join, and they cannot get out of step
at all.

That leaves `createLinkedIds` with **no consumer in this package across twenty-five
components**. It is the one state kind the component layer has never needed, and saying so
is more useful than manufacturing a use for it.

## One source of invalidity

There is no `isInvalid` prop. `error` being present *is* the invalidity:

| `error` | `aria-invalid` | `aria-describedby` |
| --- | --- | --- |
| absent | not set | description only, if any |
| present | `"true"` | description then error |

Two props for one fact is how a control ends up `aria-invalid="true"` with nothing saying
why — which is what `RegexWithFlagsField` does in mux-magic today.

<Canvas of={FieldStories.AllStates} />

## The error is *described*, not announced

No `role="alert"` and no live region, deliberately. An error bound through
`aria-describedby` is read when the control takes focus — which is when the user can act on
it. A live region on the same text announces it a second time, from nowhere, while the caret
is somewhere else.

A form that fails on submit should say so **once, at the top**, with the `Alert` this
library already ships. Then the user tabs into the field and hears the specific reason.
`ToastRegion` draws the same line for the same reason.

## Order is behaviour

`aria-describedby` is read in sequence, so description comes before error:

> "Absolute paths only. That path is not writable."

reads correctly; its reverse does not. Nothing enforces this but the order of one array in
the component, which is why that array is not built from an object.
