fix(FormField): announce the blocks that rendered, not the props that were set - #549
Merged
Conversation
… were set 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
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012MsMuj8Fic9tjWVjyEyrxc
9 tasks
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
Closes #497
Type of change
revert(Scope): ...)Description
FormFielddecided what to render from the prop or the slot, and what to announce from the props alone. Slots are not in the injected context at all, so the two conditions disagreed in both directions.aria-describedby#hint/#description/#helpslot, no prophintprop, nolabelerror+helperror: true, no messageThe last row is the one #497 did not list, and nothing covered it.
error?: boolean | string;truemarks the field invalid without a message, and the error block needs a string, so it never draws while the attribute named it anyway.The fix
FormField.vueis the only scope where props and slots are both visible, so the ids are computed there and passed through the injected context asdescribedBy. The template binds the samehasLabel/hasErrorcomputeds that feed it, so the two cannot drift apart again — that pair is the point of the change, not the list itself.useFormFieldprefersdescribedByand keeps the props-derived list as a fallback:formFieldInjectionKeyandFormFieldInjectedOptionsare both published, so a custom control may provide a context without it. Adding an optional field rather than replacing the three keeps that non-breaking.aria-invalidis deliberately unchanged. It reports the field's error state, and the#errorslot'sv-ifis 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. An#errorslot with no error value now has its block described, since it drew; it is still not reported as invalid, and the docs still recommend bindingerrorinstead.Upstream
Checked at v4 HEAD (
970025f), not just at our sync cursor:useFormField.ts'sariaAttrsandFormField.vue's five conditions are still line-for-line identical, moduloui→b24ui. Upstream has not fixed this, so the bug is live for everynuxt/uiconsumer and a faithful port of any commit touching those two files would silently revert us.Recorded as a §2 invariant in
.sync/PORTING.mdso that cannot happen quietly.Tests
The six characterization tests #496 added to pin the broken behaviour did exactly what they were written to do — 66 failures per project on the first run, every one of them a test that existed to say "this is wrong on purpose". They are rewritten to assert the fixed behaviour, and a seventh covers
error: true.Each of the four conditions in
describedBywas mutated separately, because a guard checked at one value is not a guard checked:hasLabelterm!hasErrorterm onhelpdescribedByinuseFormFieldWhy nothing caught this
Two measurements worth recording, because both are limits rather than oversights.
axedoes not check that an idref resolves. This spec already runsaxeover a field with label, description, help, hint and error set at once — precisely the configuration where…-helpdangled. Measured with the fix reverted:aria-describedbynamedv-0-0-0-help, which is provably absent from the DOM, andaxereportedaria-valid-attr-valueamong its passes, zero violations.A snapshot guard would have been vacuous. Scanning all 218 snapshot files for an
aria-describedbyid that noid=in the same file defines returns zero hits — and returned zero before the fix too, since no.snapchanged here. The corpus simply never rendered the offending combination. So no corpus guard was added; the seven unit cases are the coverage.Docs
Both places that documented the old behaviour in detail are rewritten:
docs/content/docs/2.components/form-field.md— the sectionSlots do not replace their propsbecomesWhat is announced is what was drawn, with both in-page anchors updated, and the#errortable's second row corrected from…-error …-helpto…-errorskills/b24-ui-nuxt/references/guidelines/forms.md— same rewrite, and the note that this is now a divergence rather than upstream behaviourChecks
pnpm lint,pnpm typecheckgreenChecklist
🤖 Generated with Claude Code
https://claude.ai/code/session_012MsMuj8Fic9tjWVjyEyrxc
Generated by Claude Code