docs(FormField): document the four remaining slots, and what #error actually does - #496
Merged
Conversation
…ctually does #461 documented `#label`. The other four slots had no examples, so the only signal they exist was the generated API table — which nobody reads to discover a capability. `#description` and `#hint` get sections in the shape #461 established. Two facts came out of writing them that the page could not have told you: **A hint needs a label.** It renders inside the label row, and the component only draws that row when `label` or `#label` is present. A `hint` on a label-less field renders nothing, silently — which is also why `with hint` and `with hint slot` were byte-identical to `with b24ui` in the snapshots (#454). Both cases now pass a label, and the collision baseline shrinks by two groups. **`#error` is worse than "not a drop-in replacement".** The issue said the error block renders unconditionally and hides `help`; both are true. What it did not say is that the aria wiring is computed from the props and does not follow. Measured, with `help` set and an `#error` slot and no error: error block rendered, in red help block gone control aria-invalid="false" aria-describedby="v-0-0-help" <- an id no longer in the DOM A visible error nobody is told about, over a dangling reference. The page now shows the pattern that works — bind `error` to the message or to `false`, and let the slot supply markup only — and says why a Form-driven field cannot use it, since the Form sets `undefined` rather than `false`. Checked against upstream at our cursor before writing any of it: the branch is line-for-line identical to `nuxt/ui`, so this is their design and documenting it is the right move rather than diverging (PORTING.md §7). Tests for what the snapshots cannot see: the `aria-describedby` association surviving the `#hint` and `#description` slots, the scoped payload arriving in each, and all three states of the `#error` branch — including the dangling reference, pinned so it cannot be fixed by accident and go unnoticed. Mirrored into the skill guideline, whose props table also omitted `help` and `error` entirely. Also: `documentation.md` gains the command that checks a page renders. MDC fails at render time, and without the `NUXT_PUBLIC_*` variables `deploy.yml` sets, the build dies with `ERROR completable is not defined` — a `@nuxtjs/mcp-toolkit` symbol, nothing to do with your page, and identical on a clean `main`. That cost me twelve minutes; it should cost the next person none. Resolves #462
…shape The first version of this PR found that `#error`'s aria wiring is computed from the props and does not follow the slots. It did not carry that insight to the other four slots, whose blocks render on the same `props.x || !!slots.x` while `useFormField` builds `aria-describedby` from `formField.value[type]` — the props, through `provide`. Slots are not in there at all. Every new example on the page used the slot **without** the prop. That is the one shape where the announcement disappears. Measured: #hint slot, no prop block rendered, aria-describedby absent #description slot, no prop block rendered, aria-describedby absent #help slot, no prop block rendered, aria-describedby absent hint prop, no label block NOT rendered, aria-describedby names it Three statements were false as written and are gone: that the hint is announced through `aria-describedby` (not in the example above it), that the description association "is unchanged" (only with the prop set), and that `#help` has no trap of its own. The page now leads with the rule that fixes all five — set the prop, use the slot for markup, which is the shape `#label` has had since #461 — with the disagreement tabulated in both directions. The `#error` section keeps its detail, and admits that even the recommended pattern names `…-help` while the help block is not rendered. Tests for the shape the docs teach, which the first version could not see: it set prop *and* slot, so it passed on the prop's strength. Three slot-only cases now pin the missing association, and one pins the dangling `…-hint`. The pre-existing `binds hints with aria-describedby` gets a label and an assertion that the id resolves — it checked one end of an association and was how the dangling reference stayed hidden. Filed as #497, with the measurements and the three repair options. Checked against upstream first: identical at our cursor, so fixing it here is a divergence to decide on purpose, not a side effect of a docs PR. Also from review: the skill guideline had the `#label` example orphaned under the new error prose, said `v-else` where the component says `v-else-if`, and described `description` as "below the input" when it renders above; the `documentation.md` recipe implied the environment was the only difference from CI, which also runs `pnpm build` ahead of it. Refs #462
9 tasks
IgorShevchik
added a commit
that referenced
this pull request
Sep 8, 2026
… were set (#549) The template renders each descriptive block from `props.x || !!slots.x`, while `useFormField` built `aria-describedby` from the props handed down through `provide`. Slots were not in that context at all, so the two conditions disagreed in both directions: a slot with no prop was text on screen that nothing announced, and a prop whose block never drew left the attribute naming an id that was not in the document — `hint` without a label to live in, `help` behind a rendered error, and `error: true`, which marks the field invalid without a message the error block could show. `FormField.vue` is the only scope where props and slots are both visible, so the ids are computed there and passed through the injected context as `describedBy`. The template binds the same `hasLabel` / `hasError` computeds that feed it, so the two cannot drift apart again. `useFormField` prefers that list and keeps the props-derived one as a fallback, since `formFieldInjectionKey` and `FormFieldInjectedOptions` are both published and a custom control may provide a context without it. `aria-invalid` is deliberately unchanged. It reports the field's error state, and the `#error` slot's `v-if` is satisfied by the slot merely existing — so keying invalidity off the rendered block would mark every field invalid for anyone who supplies the slot as markup. The six characterization tests added in #496 to pin this behaviour fail as intended and are rewritten; a seventh covers the `error: true` direction, which nothing had. Each of the four conditions in `describedBy` was mutated separately to confirm it is checked. Upstream still carries the defect at v4 HEAD, so this is a deliberate divergence: recorded as a §2 invariant in `.sync/PORTING.md`, with the docs page and the `skills/` forms guideline rewritten to match. Closes #497 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012MsMuj8Fic9tjWVjyEyrxc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linked issue
Resolves #462 · uncovered #497
Type of change
revert(Scope): ...)Description
#461 documented
#label. The other four slots had no examples, so the only signal they exist was the generated API table — which nobody reads to discover a capability.Writing the examples turned up two behaviours, and the review round turned up that I had only half-understood the second one.
A hint needs a label
It renders inside the label row, and the component only draws that row when
labelor#labelis present. Sohinton a label-less field renders nothing, silently. That is also whywith hintandwith hint slotwere byte-identical towith b24uiin the snapshots — two more of the vacuous cases #454 was about, sitting in its baseline. Both now pass a label; the baseline shrinks from 293 groups to 291.The blocks and the announcement disagree
FormFielddecides what to render fromprops.x || !!slots.x, and what to announce from the props alone:Measured, across all five blocks:
aria-describedbydescriptionprop#descriptionslot, no prop#hintslot, no prop#helpslot, no prophintprop, nolabelThe first version of this PR taught the broken shape. Every example used the slot without the prop, which is exactly the row where the announcement disappears — and three statements on the page were false as written: that the hint is announced through
aria-describedby, that the description association "is unchanged", and that#helphas no trap of its own. All three are gone.The page now leads with the rule that fixes all five — set the prop, use the slot for markup — which is the shape
#labelhas had since #461.#errorkeeps its own section, because itsv-ifis satisfied by the slot's mere existence:An
#errorslot with no error value: a red error block on screen,helpgone,aria-invalid="false", andaria-describedbynaming a…-helpthat is no longer in the document. The recommended pattern — binderrorto the message or tofalse— is spelled out with its three states, and the section admits that even its "good" row still names…-helpwhile the block is not rendered.Checked against upstream first
Per
PORTING.md§7, before writing a word:git -C <mirror> show <cursor>:src/runtime/components/FormField.vue. The component and the composable are line-for-line identical tonuxt/ui. So this is upstream's design, fixing it here is a deliberate divergence that a later port would silently revert, and that decision belongs in its own change rather than in a documentation PR. Filed as #497 with the measurements and three repair options.Tests
The issue asked for these, and it was right — the existing snapshot cases assert neither the association nor the scoped payload. Review found my first attempt was itself vacuous for the case that mattered.
#hint/#descriptionslots with the prop set — the recommended shape;…-hintwhenhintis set without a label;#errorbranch, including the dangling reference;binds hints with aria-describedby— pre-existing — gains a label and an assertion that the id resolves. It checked one end of an association, which is how the dangling reference stayed hidden.Each characterization test points at #497, so when the defect is fixed they fail and mark the documentation that has to change with them.
Also
documentation.mdgains the command that checks a page renders, corrected after review to namepnpm dev:prepareand to say that CI runsdocs:full:generate(pnpm build && pnpm docs:generate) — the environment is not the only difference. MDC fails at render time, and without theNUXT_PUBLIC_*variables the build dies withERROR completable is not defined, a@nuxtjs/mcp-toolkitsymbol with nothing to do with your page. Confirmed identical on a cleanmainbefore writing that down.skills/b24-ui-nuxt/references/guidelines/forms.mdis mirrored and restructured: the#labelexample had been orphaned under the new prose,v-elsesaid where the component saysv-else-if, anddescriptiondescribed as "below the input" when it renders above.Checklist
Gate:
lint0 ·typecheck0 ·test314/314 ·test:module3/3 ·test:workflows49/0 ·skill:checkclean · baseline regenerates to 291/870 with no drift ·docs:generate1264 routes,form-fieldamong them.