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
// `error` drives the invalid STATE. Its text is the host's to render.
93
+
aria-invalid={!!error}
94
+
// The required STATE, on the control. Not a second asterisk.
95
+
aria-required={required||undefined}
96
+
>
88
97
{[1, 2, 3, 4, 5].map((star) => (
89
98
<Star
90
99
key={star}
91
100
filled={Number(value) >=star}
92
101
onClick={() =>!readonly&&onChange(star)}
93
102
/>
94
103
))}
95
-
{error&& <spanclassName="error">{error}</span>}
96
104
</div>
97
105
);
98
106
}
99
107
```
100
108
109
+
### Who Renders What
110
+
111
+
A widget shares a field's chrome with its host, and each piece below has exactly **one**
112
+
owner. Rendering one the host already renders is the classic custom-widget defect: the
113
+
same sentence, or the same asterisk, appears twice.
114
+
115
+
| Concern | Owner |
116
+
|---|---|
117
+
|`aria-invalid` on the control element |**the widget** — only it renders that element |
118
+
| The validation message **text**|**the host** (objectui's `<FormMessage />`) |
119
+
| The required marker `*`|**the host** (objectui's `<FormLabel>`) |
120
+
121
+
-**`error` is a signal, not text to render.** It carries the active message string —
122
+
objectui feeds it from react-hook-form's `fieldState.error?.message` and leaves it
123
+
`undefined` while the field is valid — but the host already renders that text below the
124
+
control. Read it to set `aria-invalid`, nothing else. A widget that also prints it
125
+
displays the same message twice (objectui#3222).
126
+
-**If you compute `aria-invalid` yourself, derive it from `error` — and mind the spread
127
+
order.** A host may already be injecting a correct `aria-invalid`: objectui's
128
+
`<FormControl>` is a Radix `Slot` that does. So forward the props you don't consume
129
+
onto the control you render, and never write an `aria-invalid` computed from something
130
+
else *after* that spread — it silently overwrites the host's correct value with `false`.
131
+
That is exactly what seven built-in objectui widgets did while the slot went unproduced,
132
+
so an invalid field was never announced to a screen reader (objectui#3222).
133
+
-**Never draw your own required marker.** The host's label owns the `*`; a second author
134
+
for it produces the same double display, which is why `required` is deliberately absent
135
+
from objectui's rendered props type. The one thing a widget genuinely adds is the state
136
+
on the control — `aria-required`, which assistive tech announces *as* a state instead of
137
+
folding it into the accessible name, and which keeps working for a field rendered with
138
+
no label at all (objectui#3290). Reflect `required` as `aria-required` and stop there;
139
+
objectui goes further and injects `aria-required` itself, so forwarding your leftover
140
+
props gets it for free. Do **not** set the native `required` attribute — that arms the
141
+
browser's own constraint-validation bubble alongside the host's messages.
142
+
101
143
## Field Types
102
144
103
145
Each field declares a `type`. The renderer auto-infers a widget from the type; a custom `widget` name on the field view overrides that inference. The full set of field types is defined by the `FieldType` enum in `packages/spec/src/data/field.zod.ts`:
0 commit comments