You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
main. Found while writing the slot documentation in #496, which documents the behaviour rather than changing it — see Why this is filed rather than fixed below.
Description
FormField decides what to render from the prop or the slot:
// src/runtime/composables/useFormField.ts:127constdescriptiveAttrs=['error','hint','description','help'].filter(type=>formField?.value?.[type])// ← the props, via provide().map(type=>`${formField?.value.ariaId}-${type}`)
Slots are not in formField.value at all. The two conditions therefore disagree, in both directions.
Measured
Mounted B24FormField with B24Input, useId stubbed to v-0-0:
what is passed
block rendered
aria-describedby
hint prop (with a label)
v-0-0-hint
v-0-0-hint
#hint slot, no prop
v-0-0-hint
absent
hint prop, no label
none
v-0-0-hint
description prop
v-0-0-description
v-0-0-description
#description slot, no prop
v-0-0-description
absent
help prop
v-0-0-help
v-0-0-help
#help slot, no prop
v-0-0-help
absent
Two distinct failures:
Slot without prop — text on screen, nothing announced. The block has an id; the control never names it.
Prop without a rendered block — a dangling reference.hint needs a label (the hint lives inside the label row, which is only drawn when label or #label is present), so hint alone renders nothing while aria-describedby still names it. The same happens whenever error and help are both set: help is the v-else-if of the error branch, so it is not rendered, and aria-describedby reads …-error …-help.
The #error slot makes the second failure worse, because its v-if is satisfied by the slot's mere existence:
With an #error slot and no error value: a red error block on screen, help gone, aria-invalid="false", and aria-describedby="…-help" pointing at an element that is not in the document. A visible error that assistive technology is told nothing about, over a broken reference.
Impact
WCAG 4.1.2 (Name, Role, Value) — aria-invalid does not reflect the state, and aria-describedby names ids that do not resolve. Indirectly 1.3.1 (Info and Relationships): the association between a message and its control is not exposed programmatically.
This is a Bitrix24 component library; the fields in question are the ones every form in every app is built out of.
Why this is filed rather than fixed
Checked against upstream at our sync cursor (.sync/PORTING.md §7): FormField.vue's conditions and useFormField.ts's ariaAttrs are line-for-line identical to nuxt/ui, modulo the ui → b24ui rename. So this is not a porting slip, and fixing it locally is a deliberate divergence — one that a later port would silently revert unless it is recorded in §2 first. That decision is worth making on purpose rather than as a side effect of a documentation PR.
Note that PORTING.md is explicit that matching upstream is not itself a defence: "Upstream is the control, not the authority: matching it answers where the mistake happened … and settles nothing about whether it is a mistake."
Pinned, not left loose
#496 adds characterization tests so the current behaviour cannot change unnoticed, each pointing here:
renders the #hint/#description/#help slot with no prop, and announces nothing
names the hint in aria-describedby even when no hint was drawn
renders #error with no error, and leaves aria pointing at nothing
When this is fixed those tests fail, which is the intended signal — they also mark the documentation that has to change with it.
Options
Build aria-describedby from what rendered. Pass slot presence into the injected context, or compute the attribute in FormField.vue where both are in scope. Fixes both directions. Diverges from upstream; needs a §2 entry.
Report upstream first and port the fix back. Slower, no divergence, and the same bug is presumably live for every nuxt/ui consumer.
Environment
main. Found while writing the slot documentation in #496, which documents the behaviour rather than changing it — see Why this is filed rather than fixed below.Description
FormFielddecides what to render from the prop or the slot:and what to announce from the props alone:
Slots are not in
formField.valueat all. The two conditions therefore disagree, in both directions.Measured
Mounted
B24FormFieldwithB24Input,useIdstubbed tov-0-0:aria-describedbyhintprop (with a label)v-0-0-hintv-0-0-hint#hintslot, no propv-0-0-hinthintprop, nolabelv-0-0-hintdescriptionpropv-0-0-descriptionv-0-0-description#descriptionslot, no propv-0-0-descriptionhelppropv-0-0-helpv-0-0-help#helpslot, no propv-0-0-helpTwo distinct failures:
hintneeds a label (the hint lives inside the label row, which is only drawn whenlabelor#labelis present), sohintalone renders nothing whilearia-describedbystill names it. The same happens whenevererrorandhelpare both set:helpis thev-else-ifof the error branch, so it is not rendered, andaria-describedbyreads…-error …-help.The
#errorslot makes the second failure worse, because itsv-ifis satisfied by the slot's mere existence:With an
#errorslot and no error value: a red error block on screen,helpgone,aria-invalid="false", andaria-describedby="…-help"pointing at an element that is not in the document. A visible error that assistive technology is told nothing about, over a broken reference.Impact
WCAG 4.1.2 (Name, Role, Value) —
aria-invaliddoes not reflect the state, andaria-describedbynames ids that do not resolve. Indirectly 1.3.1 (Info and Relationships): the association between a message and its control is not exposed programmatically.This is a Bitrix24 component library; the fields in question are the ones every form in every app is built out of.
Why this is filed rather than fixed
Checked against upstream at our sync cursor (
.sync/PORTING.md§7):FormField.vue's conditions anduseFormField.ts'sariaAttrsare line-for-line identical tonuxt/ui, modulo theui→b24uirename. So this is not a porting slip, and fixing it locally is a deliberate divergence — one that a later port would silently revert unless it is recorded in §2 first. That decision is worth making on purpose rather than as a side effect of a documentation PR.Note that PORTING.md is explicit that matching upstream is not itself a defence: "Upstream is the control, not the authority: matching it answers where the mistake happened … and settles nothing about whether it is a mistake."
Pinned, not left loose
#496 adds characterization tests so the current behaviour cannot change unnoticed, each pointing here:
renders the #hint/#description/#help slot with no prop, and announces nothingnames the hint in aria-describedby even when no hint was drawnrenders #error with no error, and leaves aria pointing at nothingWhen this is fixed those tests fail, which is the intended signal — they also mark the documentation that has to change with it.
Options
aria-describedbyfrom what rendered. Pass slot presence into the injected context, or compute the attribute inFormField.vuewhere both are in scope. Fixes both directions. Diverges from upstream; needs a §2 entry.nuxt/uiconsumer.#erroractually does #496: "set the prop, use the slot for markup", with the failure modes spelled out.3 is in place. 1 or 2 is the actual repair.