@object-ui/fields 17.2.0 → 17.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +592 -0
- package/dist/__tests__/cell-truncation.test.d.ts +0 -0
- package/dist/__tests__/field-carrier-sdui.test.d.ts +0 -0
- package/dist/__tests__/widget-aria-invalid-e2e.test.d.ts +0 -0
- package/dist/__tests__/widget-aria-invalid-registry-e2e.test.d.ts +0 -0
- package/dist/__tests__/widget-dom-leak-e2e.test.d.ts +0 -0
- package/dist/index.cjs +5 -5
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2881 -2566
- package/dist/widgets/AddressField.d.ts +1 -1
- package/dist/widgets/AddressField.uniqueIds.test.d.ts +0 -0
- package/dist/widgets/AvatarField.d.ts +1 -1
- package/dist/widgets/CharacterCount.d.ts +56 -0
- package/dist/widgets/CheckboxesField.d.ts +1 -1
- package/dist/widgets/CodeField.d.ts +1 -1
- package/dist/widgets/ColorField.d.ts +1 -1
- package/dist/widgets/CurrencyField.d.ts +1 -1
- package/dist/widgets/EmailField.d.ts +1 -1
- package/dist/widgets/FileField.d.ts +1 -1
- package/dist/widgets/FilterConditionField.d.ts +1 -1
- package/dist/widgets/FullscreenFieldEditor.d.ts +155 -0
- package/dist/widgets/GeolocationField.d.ts +1 -1
- package/dist/widgets/GeolocationField.uniqueIds.test.d.ts +0 -0
- package/dist/widgets/ImageField.d.ts +1 -1
- package/dist/widgets/LocationField.d.ts +1 -1
- package/dist/widgets/LookupField.d.ts +1 -1
- package/dist/widgets/LookupField.gateHintLabel.test.d.ts +8 -0
- package/dist/widgets/MultiSelectField.d.ts +1 -1
- package/dist/widgets/ObjectField.d.ts +1 -1
- package/dist/widgets/ObjectRefField.d.ts +1 -1
- package/dist/widgets/OptionsEmptyState.d.ts +45 -0
- package/dist/widgets/OptionsEmptyState.no-provider.test.d.ts +0 -0
- package/dist/widgets/OptionsEmptyState.test.d.ts +0 -0
- package/dist/widgets/PercentField.d.ts +1 -1
- package/dist/widgets/PhoneField.d.ts +1 -1
- package/dist/widgets/QRCodeField.d.ts +1 -1
- package/dist/widgets/RadioField.d.ts +1 -1
- package/dist/widgets/RatingField.d.ts +1 -1
- package/dist/widgets/RecipientPickerField.d.ts +1 -1
- package/dist/widgets/RecordPickerDialog.d.ts +17 -1
- package/dist/widgets/RecordPickerDialog.filterOptions.test.d.ts +8 -0
- package/dist/widgets/RecordPickerDialog.selectLabel.test.d.ts +8 -0
- package/dist/widgets/RichTextField.d.ts +19 -1
- package/dist/widgets/TagsField.d.ts +1 -1
- package/dist/widgets/TagsField.placeholder.no-provider.test.d.ts +0 -0
- package/dist/widgets/TagsField.placeholder.test.d.ts +0 -0
- package/dist/widgets/TextAreaField.d.ts +44 -5
- package/dist/widgets/UrlField.d.ts +1 -1
- package/dist/widgets/__tests__/FullscreenFieldEditor.i18n.test.d.ts +0 -0
- package/dist/widgets/__tests__/FullscreenFieldEditor.no-provider.test.d.ts +0 -0
- package/dist/widgets/__tests__/RichTextField.fullscreenDisabled.test.d.ts +0 -0
- package/dist/widgets/__tests__/RichTextField.mobileFullscreen.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.characterCount.announcements.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.characterCount.i18n.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.characterCount.no-provider.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.fullscreenDisabled.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.mobileFullscreen.test.d.ts +0 -0
- package/dist/widgets/toDomProps.d.ts +91 -0
- package/dist/widgets/types.d.ts +128 -33
- package/dist/withFieldCarrier.d.ts +56 -0
- package/package.json +13 -13
- package/src/__tests__/cell-truncation.test.tsx +96 -0
- package/src/__tests__/field-carrier-sdui.test.tsx +169 -0
- package/src/__tests__/spec-symbol-batch7.test.ts +29 -15
- package/src/__tests__/validation-feedback.test.tsx +22 -13
- package/src/__tests__/widget-aria-invalid-e2e.test.tsx +265 -0
- package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +338 -0
- package/src/__tests__/widget-dom-leak-e2e.test.tsx +598 -0
- package/src/__tests__/widget-props-contract.test.tsx +22 -9
- package/src/index.tsx +74 -26
- package/src/widgets/AddressField.tsx +31 -12
- package/src/widgets/AddressField.uniqueIds.test.tsx +92 -0
- package/src/widgets/AvatarField.tsx +9 -2
- package/src/widgets/BooleanField.tsx +3 -3
- package/src/widgets/CharacterCount.tsx +263 -0
- package/src/widgets/CheckboxesField.tsx +31 -14
- package/src/widgets/CodeField.tsx +9 -2
- package/src/widgets/ColorField.tsx +11 -2
- package/src/widgets/CurrencyField.tsx +5 -4
- package/src/widgets/DateField.tsx +2 -2
- package/src/widgets/DateTimeField.tsx +2 -2
- package/src/widgets/EmailField.tsx +5 -5
- package/src/widgets/FileField.tsx +13 -3
- package/src/widgets/FilterConditionField.tsx +11 -0
- package/src/widgets/FormulaField.tsx +1 -1
- package/src/widgets/FullscreenFieldEditor.tsx +271 -0
- package/src/widgets/GeolocationField.tsx +21 -6
- package/src/widgets/GeolocationField.uniqueIds.test.tsx +88 -0
- package/src/widgets/GridField.tsx +1 -1
- package/src/widgets/ImageField.tsx +9 -2
- package/src/widgets/LocationField.tsx +9 -2
- package/src/widgets/LookupField.gateHintLabel.test.tsx +135 -0
- package/src/widgets/LookupField.tsx +41 -6
- package/src/widgets/MasterDetailField.tsx +1 -1
- package/src/widgets/MultiSelectField.tsx +30 -14
- package/src/widgets/NumberField.tsx +3 -3
- package/src/widgets/ObjectField.tsx +17 -8
- package/src/widgets/ObjectRefField.tsx +10 -0
- package/src/widgets/OptionsEmptyState.no-provider.test.tsx +88 -0
- package/src/widgets/OptionsEmptyState.test.tsx +169 -0
- package/src/widgets/OptionsEmptyState.tsx +83 -0
- package/src/widgets/PasswordField.tsx +3 -3
- package/src/widgets/PeoplePicker.tsx +7 -1
- package/src/widgets/PercentField.tsx +5 -4
- package/src/widgets/PhoneField.tsx +5 -5
- package/src/widgets/QRCodeField.tsx +9 -2
- package/src/widgets/RadioField.tsx +24 -14
- package/src/widgets/RatingField.tsx +18 -3
- package/src/widgets/RecipientPickerField.tsx +18 -0
- package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +611 -0
- package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +112 -0
- package/src/widgets/RecordPickerDialog.tsx +173 -8
- package/src/widgets/RichTextField.tsx +151 -24
- package/src/widgets/SelectField.tsx +48 -17
- package/src/widgets/SliderField.tsx +1 -1
- package/src/widgets/SummaryField.tsx +1 -1
- package/src/widgets/TagsField.placeholder.no-provider.test.tsx +68 -0
- package/src/widgets/TagsField.placeholder.test.tsx +95 -0
- package/src/widgets/TagsField.tsx +23 -2
- package/src/widgets/TextAreaField.tsx +171 -93
- package/src/widgets/TextField.tsx +3 -3
- package/src/widgets/TimeField.tsx +2 -2
- package/src/widgets/UrlField.tsx +5 -5
- package/src/widgets/UserField.tsx +1 -1
- package/src/widgets/VectorField.tsx +1 -1
- package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +248 -0
- package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +116 -0
- package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +244 -0
- package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +195 -0
- package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +417 -0
- package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +287 -0
- package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +160 -0
- package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +161 -0
- package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +346 -0
- package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +246 -0
- package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +175 -0
- package/src/widgets/toDomProps.ts +175 -0
- package/src/widgets/types.ts +135 -35
- package/src/widgets/useFieldTranslation.ts +60 -0
- package/src/withFieldCarrier.tsx +75 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,597 @@
|
|
|
1
1
|
# @object-ui/fields
|
|
2
2
|
|
|
3
|
+
## 17.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3889ffb: Console chrome i18n gaps (objectstack#5407).
|
|
8
|
+
|
|
9
|
+
- A dependency-gated lookup now names its controlling field by its **label**
|
|
10
|
+
instead of its raw API name. The sentence was localized but the interpolated
|
|
11
|
+
name was not, so every locale — English included — read `Select crm_account
|
|
12
|
+
first`. The form renderer passes a new `dependsOnLabels` widget prop (the
|
|
13
|
+
lookup-side counterpart of `emptyHint`, which it already resolves to labels
|
|
14
|
+
for the fixed-option widgets); a name the host does not cover still falls
|
|
15
|
+
back to itself.
|
|
16
|
+
- The page-header overflow trigger's `More actions` accessible name now reads
|
|
17
|
+
`detail.moreActions`, the same key `action:menu`'s own overflow trigger uses,
|
|
18
|
+
so the two cannot diverge per locale.
|
|
19
|
+
- The activity-feed reaction button's `Add reaction` accessible name is now a
|
|
20
|
+
bundle key (`detail.addReaction`, added to all ten packs).
|
|
21
|
+
- The "check the highlighted fields" toast joins field names with a per-locale
|
|
22
|
+
separator (`validation.formInvalidJoiner`) instead of a hardcoded `、`
|
|
23
|
+
(U+3001) — right for zh/ja by accident, wrong in English and every Latin
|
|
24
|
+
locale. Latin packs use `, `, CJK `、`, Arabic `، `.
|
|
25
|
+
- The Spanish `validation.required` / `validation.unique` templates gained
|
|
26
|
+
their own masculine head noun (`El campo {{field}} es obligatorio`) so the
|
|
27
|
+
adjective agrees for feminine field labels too — `Cuenta es obligatorio` was
|
|
28
|
+
ungrammatical.
|
|
29
|
+
|
|
30
|
+
- 19b8c9b: Field widgets no longer spread renderer-only props — or arbitrary keys from a
|
|
31
|
+
field config — onto the DOM element they render (objectui#3291).
|
|
32
|
+
|
|
33
|
+
**Behaviour change:** an unknown key written on a field configuration (or on an
|
|
34
|
+
SDUI `field:*` node) stops becoming an HTML attribute on the rendered control.
|
|
35
|
+
Nothing reads those attributes, but they were serialized into the DOM, into
|
|
36
|
+
snapshots, and into anything scraping rendered markup.
|
|
37
|
+
|
|
38
|
+
## What was happening
|
|
39
|
+
|
|
40
|
+
Widgets forwarded their leftover props with a bare spread, so whatever a host
|
|
41
|
+
handed them became an attribute. Measured on a real form with a real widget:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
<input placeholder="PH-f" zzcanary="CANARY-STR" zzcanaryobj="[object Object]"
|
|
45
|
+
zzcanarynum="42" id="…" type="text" value="" name="f">
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`zzcanaryobj="[object Object]"` is an ordinary extra key on the field config
|
|
49
|
+
being `String()`-ed onto an attribute. React 19 does not warn about any of it:
|
|
50
|
+
an all-lowercase unknown attribute is passed through in complete silence, which
|
|
51
|
+
is why this went unnoticed.
|
|
52
|
+
|
|
53
|
+
Eleven widgets carried a line that looked like it prevented exactly this:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
const { inputType, ...domProps } = props as any; // "Filter out non-DOM props"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`inputType` is already stripped by the form renderer before a widget sees it,
|
|
60
|
+
so that line filtered nothing — the comment actively misled. It is gone.
|
|
61
|
+
|
|
62
|
+
## What changed
|
|
63
|
+
- New `toDomProps(props)`, exported from `@object-ui/fields`. It keeps only
|
|
64
|
+
what may legitimately become a DOM attribute and drops the rest.
|
|
65
|
+
- The 14 field widgets that spread onto a host element now go through it:
|
|
66
|
+
`text`, `textarea`, `number`, `boolean`, `date`, `datetime`, `time`, `email`,
|
|
67
|
+
`phone`, `url`, `password`, `currency`, `percent`, and `select`.
|
|
68
|
+
|
|
69
|
+
**It is a whitelist, not a list of keys to drop.** The largest leak source is
|
|
70
|
+
not any named renderer prop — it is the open tail of author-supplied keys. The
|
|
71
|
+
form renderer destructures a fixed set of known keys and forwards the rest
|
|
72
|
+
verbatim, and `SchemaRenderer` is wider still: it spreads the whole authored
|
|
73
|
+
node as props with no strip layer at all, so on that path a widget's own spread
|
|
74
|
+
is the only line of defence. A blacklist of today's renderer-only props would
|
|
75
|
+
pass every canary above and would not stop the next authored key either.
|
|
76
|
+
|
|
77
|
+
The forwarded set is the one `FieldWidgetComponentProps` already **declares**:
|
|
78
|
+
`id`, `name`, `autoFocus`, `tabIndex`, `onBlur`, `onFocus`, `onClick`,
|
|
79
|
+
`className`, `disabled`, plus `aria-*` and the `data-*` family. Until now that
|
|
80
|
+
was a type-level claim a widget could violate at runtime just by spreading;
|
|
81
|
+
`toDomProps` is its executable form.
|
|
82
|
+
|
|
83
|
+
Two compile-time assertions tie the helper to the declaration, and it is worth
|
|
84
|
+
being exact about which drift each one prevents:
|
|
85
|
+
|
|
86
|
+
- the contract's DOM pass-through block is now a named type
|
|
87
|
+
(`FieldWidgetDomProps`), and **both directions are compiler-bound**:
|
|
88
|
+
forwarding a key the contract does not declare fails to compile, and
|
|
89
|
+
declaring a DOM key the helper does not forward fails to compile too. The
|
|
90
|
+
second direction guards _declared but not delivered_ — a key that
|
|
91
|
+
type-checks, reads as supported, and silently never reaches the element. The
|
|
92
|
+
leak test structurally cannot see that class of bug: it looks for attributes
|
|
93
|
+
that arrive, not for ones that go missing.
|
|
94
|
+
- `className` and `disabled` are bound in the **forward direction only**. They
|
|
95
|
+
are DOM-legal and are forwarded, but they live in the controlled-input block
|
|
96
|
+
because widgets also interpret them, so they are deliberately outside
|
|
97
|
+
`FieldWidgetDomProps`.
|
|
98
|
+
|
|
99
|
+
An HTML global attribute the contract does not declare (`role`, say) is no
|
|
100
|
+
longer forwarded. It only ever arrived through the open spread. If a field node
|
|
101
|
+
should be able to author one, declare it on `FieldWidgetComponentProps` and add
|
|
102
|
+
it to the whitelist — the fix belongs at the contract, not in a wider spread.
|
|
103
|
+
|
|
104
|
+
## Regression gate
|
|
105
|
+
|
|
106
|
+
A new contract test renders **every** registered field widget through **both**
|
|
107
|
+
hosts — the real form renderer and `SchemaRenderer` — and fails on any
|
|
108
|
+
attribute HTML does not define for that element. It walks real DOM attributes
|
|
109
|
+
rather than listening for React warnings (React 19 is silent for the exact case
|
|
110
|
+
that leaked), asserts a validation error genuinely rendered before scanning the
|
|
111
|
+
error variant, and calibrates its own judge against two fixtures: standard
|
|
112
|
+
markup that must produce zero findings, and planted fake attributes that must
|
|
113
|
+
all be found. A new widget type is covered automatically — the sweep is derived
|
|
114
|
+
from the widget registry, so adding one without covering it fails the test.
|
|
115
|
+
|
|
116
|
+
- 56409c2: Field widgets are finally told when their field fails validation, and the props
|
|
117
|
+
slot that carries it takes the name the published contract gives it
|
|
118
|
+
(objectui#3222).
|
|
119
|
+
|
|
120
|
+
**Breaking** for anyone implementing a field widget (see migration below). The
|
|
121
|
+
repo version policy keeps this a `minor` — objectui's major tracks
|
|
122
|
+
`@objectstack`'s — so read the bump as "breaking within objectui".
|
|
123
|
+
|
|
124
|
+
## The a11y defect this fixes
|
|
125
|
+
|
|
126
|
+
`@objectstack/spec/ui`'s `FieldWidgetPropsSchema` — the published contract that
|
|
127
|
+
third-party and AI-authored field widgets are written against — has always
|
|
128
|
+
declared `error?: string`. `@object-ui/fields` declared its own slot as
|
|
129
|
+
`errorMessage`. That looked like a naming split; it was worse:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
producers of `errorMessage` anywhere in packages/ + apps/ : 0
|
|
133
|
+
reads of `errorMessage` in packages/fields/src : 15 (7 widgets)
|
|
134
|
+
reads of `props.error` : 0
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The slot was dead under BOTH spellings. No host ever passed it: the form
|
|
138
|
+
renderer showed validation text through its own `<FormMessage/>` and never
|
|
139
|
+
forwarded the prop. So `EmailField`, `CurrencyField`, `UrlField`,
|
|
140
|
+
`RichTextField`, `PercentField`, `TextAreaField` and `PhoneField` each computed
|
|
141
|
+
`aria-invalid={!!errorMessage}` from a value that was `undefined` forever —
|
|
142
|
+
**`aria-invalid` had never once been set, and a screen reader was never told
|
|
143
|
+
the field had failed validation.**
|
|
144
|
+
|
|
145
|
+
Worse than "never set": `<FormControl>` is a Radix `Slot` that hands its child a
|
|
146
|
+
CORRECT `aria-invalid`, but a widget's own attribute is written after the props
|
|
147
|
+
spread, so it wins. Those seven widgets were actively overwriting the right
|
|
148
|
+
answer with `false`.
|
|
149
|
+
|
|
150
|
+
FROM: `renderFieldComponent` received no validation state, and the widget props
|
|
151
|
+
type declared `errorMessage?: string`, which nothing produced.
|
|
152
|
+
TO: the form renderer passes react-hook-form's `fieldState.error?.message` down
|
|
153
|
+
as `error` when it renders a registered widget, and the props type declares
|
|
154
|
+
`error?: string`. Both ends of the contract are live for the first time; a
|
|
155
|
+
rename alone would only have swapped one dead key for another.
|
|
156
|
+
|
|
157
|
+
## Migration for widget authors
|
|
158
|
+
|
|
159
|
+
```diff
|
|
160
|
+
-export function MyField({ value, onChange, field, readonly, errorMessage }: FieldWidgetComponentProps< string >) {
|
|
161
|
+
- return <Input value={value} aria-invalid={!!errorMessage} />;
|
|
162
|
+
+export function MyField({ value, onChange, field, readonly, error }: FieldWidgetComponentProps< string >) {
|
|
163
|
+
+ return <Input value={value} aria-invalid={!!error} />;
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
No alias is kept. `errorMessage` was retained nowhere on purpose — a tolerant
|
|
167
|
+
second spelling is exactly the de-facto second contract AGENTS.md #0.1 forbids,
|
|
168
|
+
and it is what would let a missed call site go quiet again. Because
|
|
169
|
+
objectui#3221 had already removed the type's `[key: string]: any`, every missed
|
|
170
|
+
site is a compile error rather than a silent `any`, so the compiler — not grep
|
|
171
|
+
— validated this rename.
|
|
172
|
+
|
|
173
|
+
## Responsibilities are split, not duplicated
|
|
174
|
+
|
|
175
|
+
The widget consumes `error` **only** to drive `aria-invalid` on the control it
|
|
176
|
+
renders (which only it can do — `aria-invalid` has to sit on the input element).
|
|
177
|
+
The message TEXT stays with `<FormMessage/>` in the form renderer. A widget that
|
|
178
|
+
also renders the text double-displays it, and the docs, the agent prompt and the
|
|
179
|
+
tests all now say so.
|
|
180
|
+
|
|
181
|
+
For the same reason `required` — also declared by the spec, also never delivered
|
|
182
|
+
— is deliberately NOT lowered into widget props: the required marker has exactly
|
|
183
|
+
one author, the renderer's `<FormLabel>`, and giving widgets the flag invites a
|
|
184
|
+
second asterisk. The a11y state a widget could legitimately carry is
|
|
185
|
+
`aria-required`, which needs no contract change at all (`AriaAttributes` is
|
|
186
|
+
already part of the type and widgets already forward it).
|
|
187
|
+
|
|
188
|
+
Builtin field types are unaffected: they render inside `<FormControl>`, whose
|
|
189
|
+
Slot already supplies `aria-invalid`, so `error` is stripped there rather than
|
|
190
|
+
leaking into the DOM as a stray attribute.
|
|
191
|
+
|
|
192
|
+
Docs updated to match: `content/docs/guide/plugin-development.md`,
|
|
193
|
+
`skills/objectui/guides/plugin-development.md` and
|
|
194
|
+
`.github/prompts/component.prompt.md` — the last of which additionally used the
|
|
195
|
+
spec's non-generic type alias as a generic (`FieldWidgetProps< number >`) and
|
|
196
|
+
destructured a `mode` prop that exists on neither type.
|
|
197
|
+
|
|
198
|
+
- 042e09d: **BREAKING (v17)** — field widgets receive their metadata on ONE key, `field`.
|
|
199
|
+
`schema` is removed from the widget contract (objectui#3233).
|
|
200
|
+
|
|
201
|
+
## What changed
|
|
202
|
+
|
|
203
|
+
`schema` was a second carrier for what `field` already means. Two producers fed
|
|
204
|
+
it: `SchemaRenderer` passed the authored node as `schema`, and the form
|
|
205
|
+
renderer's `renderFieldComponent` passed `schema={props.field || props.schema ||
|
|
206
|
+
props}` _alongside_ `field`. The predictable result was ~30 widgets resolving
|
|
207
|
+
their config as `field || schema` — one concept, two spellings, a de-facto
|
|
208
|
+
second contract (AGENTS.md #0.1).
|
|
209
|
+
|
|
210
|
+
- `FieldWidgetComponentProps` no longer declares `schema`. Reading
|
|
211
|
+
`props.schema` is now a compile error, not a silent `any`.
|
|
212
|
+
- Both producers converged. The form renderer passes `field` only. The SDUI node
|
|
213
|
+
→ `field` translation happens exactly once, in a new registration adapter
|
|
214
|
+
(`withFieldCarrier`), which every built-in field widget is registered through.
|
|
215
|
+
- All `field || schema` reads in `@object-ui/fields` are now plain `field` reads.
|
|
216
|
+
|
|
217
|
+
## Migrating a widget you wrote
|
|
218
|
+
|
|
219
|
+
**Reading the metadata** — replace the fallback with the single key:
|
|
220
|
+
|
|
221
|
+
```diff
|
|
222
|
+
-const config = field || (props as any).schema;
|
|
223
|
+
+const config = field;
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Registering a widget** — if your widget can be rendered from a schema node
|
|
227
|
+
(anything `SchemaRenderer` dispatches, not just forms), wrap it once so it still
|
|
228
|
+
gets `field`:
|
|
229
|
+
|
|
230
|
+
```diff
|
|
231
|
+
+import { withFieldCarrier } from '@object-ui/fields';
|
|
232
|
+
+
|
|
233
|
+
-ComponentRegistry.register('color', ColorField, { namespace: 'field' });
|
|
234
|
+
+ComponentRegistry.register('color', withFieldCarrier(ColorField), { namespace: 'field' });
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
`withFieldCarrier` forwards the node **by reference** — nothing is copied,
|
|
238
|
+
narrowed or renamed — and consumes `schema` so it cannot reach the DOM through a
|
|
239
|
+
widget's `...props` spread.
|
|
240
|
+
|
|
241
|
+
A third-party widget that still reads `props.schema` and is **not** re-registered
|
|
242
|
+
through the adapter will read `undefined` in v17 and silently render an empty /
|
|
243
|
+
default state. That is the deliberate cost of a major boundary: one contract
|
|
244
|
+
beats N dialects, and a widget that picks the wrong spelling should fail at
|
|
245
|
+
compile time rather than work under one host and not another.
|
|
246
|
+
|
|
247
|
+
## What did NOT change
|
|
248
|
+
- **Host metadata (SDUI JSON) is untouched.** No authored schema changes; this is
|
|
249
|
+
a change to how widgets are _written_, not to what apps declare.
|
|
250
|
+
- **`schema` is still the universal SDUI prop** every registered component
|
|
251
|
+
receives from `SchemaRenderer` (`element:*`, `page:*`, grids, reports). Only
|
|
252
|
+
the _field-widget_ contract retired it. In particular `renderFieldComponent`
|
|
253
|
+
still passes `schema` when a form field type resolves to a plain component
|
|
254
|
+
through the bare-name fallback (e.g. `type: 'text'` reaching the display text
|
|
255
|
+
widget) — that component's contract is the node, and dropping it there would
|
|
256
|
+
render `undefined.className`.
|
|
257
|
+
|
|
258
|
+
## Payload equivalence
|
|
259
|
+
|
|
260
|
+
Every path that used to deliver a payload through `schema` now delivers the
|
|
261
|
+
identical object through `field`, and both halves are pinned by tests asserting
|
|
262
|
+
**object identity**, not shape:
|
|
263
|
+
|
|
264
|
+
- form path — `packages/components/src/renderers/form/__tests__/form-field-carrier.test.tsx`
|
|
265
|
+
- SDUI path — `packages/fields/src/__tests__/field-carrier-sdui.test.tsx`
|
|
266
|
+
|
|
267
|
+
- 30ae33a: `RichTextField` honours `mobile_fullscreen`, so `mobile.fullscreenLongText` is
|
|
268
|
+
finally true of rich text too (objectui#3301).
|
|
269
|
+
|
|
270
|
+
`ObjectFormSchema.mobile.fullscreenLongText` has always been documented as
|
|
271
|
+
"textarea/rich-text get an expand button", and `ObjectForm` has always stamped
|
|
272
|
+
`mobile_fullscreen` onto `field:markdown` / `field:html` fields to deliver it.
|
|
273
|
+
Both of those types resolve to `RichTextField`, and that widget never read the
|
|
274
|
+
flag: a producer with no consumer. Turning the setting on gave a phone user an
|
|
275
|
+
expand affordance on their textareas and nothing at all on their markdown or
|
|
276
|
+
HTML fields, with nothing anywhere reporting that half the feature was inert.
|
|
277
|
+
|
|
278
|
+
FROM: `RichTextField` ignored the flag entirely (`grep fullscreen` over that
|
|
279
|
+
file returned nothing). TO: it reads `field.mobile_fullscreen` — the same single
|
|
280
|
+
metadata carrier `TextAreaField` reads, and nowhere else — and renders the same
|
|
281
|
+
expand affordance and full-height editing dialog.
|
|
282
|
+
|
|
283
|
+
**The affordance now has one implementation, not two.** One form-level setting
|
|
284
|
+
should produce one behaviour, so the expand button, the dialog and the
|
|
285
|
+
draft/commit semantics moved into a shared `FullscreenFieldEditor` that both
|
|
286
|
+
widgets render; only the EDITOR is per-widget. A second hand-written copy of
|
|
287
|
+
that state machine would be the same defect this release fixes, with an extra
|
|
288
|
+
step — it drifts, and nothing reports the drift. The rich-text dialog hosts the
|
|
289
|
+
widget's real editing surface (same format indicator, same editor), not a bare
|
|
290
|
+
textarea, so whatever that editor grows into, both positions get it at once.
|
|
291
|
+
|
|
292
|
+
Behaviour is identical across the two widgets and unchanged for
|
|
293
|
+
`TextAreaField`: the dialog seeds its draft from the committed value at open
|
|
294
|
+
time, keeps typing local (a react-hook-form field is not marked dirty by an
|
|
295
|
+
edit the user may still cancel), commits once on "Done", and discards on
|
|
296
|
+
"Cancel". Test ids follow the existing convention per widget —
|
|
297
|
+
`richtext-fullscreen-toggle` / `-dialog` / `-input` / `-save` alongside the
|
|
298
|
+
`textarea-*` ones, since a single form can contain both.
|
|
299
|
+
|
|
300
|
+
There is deliberately no prop spelling of the flag and no `??` fallback chain in
|
|
301
|
+
either widget. The field metadata is the one carrier (objectui#3233), so a
|
|
302
|
+
misspelled or misplaced flag stays inert and visible rather than being quietly
|
|
303
|
+
caught by a tolerant consumer.
|
|
304
|
+
|
|
305
|
+
Also removes a dead type from the producer: `ObjectForm` stamped the flag on
|
|
306
|
+
`'string-multiline'`, a string that `grep -rn` finds exactly once across both
|
|
307
|
+
this repo and `objectstack` — that line itself. No producer emitted it, no
|
|
308
|
+
registry key matched it, no widget read it. The remaining four stamped types
|
|
309
|
+
(`textarea`, `field:textarea`, `field:markdown`, `field:html`) each have a real
|
|
310
|
+
reader.
|
|
311
|
+
|
|
312
|
+
### Patch Changes
|
|
313
|
+
|
|
314
|
+
- b7165ce: AddressField / GeolocationField sub-inputs now derive their DOM ids from a `useId()` prefix + sub-field name (the RadioField / CheckboxesField `groupId` paradigm) instead of hardcoded literals ("street", "city", "state", "zipCode", "country", "latitude", "longitude"). Two address or geolocation fields in one form no longer produce duplicate DOM ids, and each sub-label's `htmlFor` resolves to and focuses its own field's input instead of the first match in the document (#3343).
|
|
315
|
+
- 7d08c3f: `TextAreaField` / `RichTextField` now honour `disabled` on their fullscreen editing path. `disabled` used to reach the inline control only: `showFullscreenButton` never consulted it, neither widget forwarded it to `FullscreenFieldEditor`, and that component did not declare the prop at all. A disabled long-text or rich-text field therefore sat correctly greyed out next to a live expand button whose dialog accepted any edit and wrote it straight back through `onCommit` — reproduced dynamically before the fix as toggle `disabled=false`, dialog input `disabled=false`, `onChange` called with "EDITED WHILE DISABLED". The state was easy to miss precisely because the visible control looked right.
|
|
316
|
+
|
|
317
|
+
`FullscreenFieldEditor` now declares `disabled`: the expand button stays (disabled means "not interactive, muted", unlike `readonly`, which suppresses the affordance entirely via each widget's read-only early return) but is disabled and refuses to open, the dialog's editor is disabled through a new third `children` argument, and "Done" is both disabled and gated before `onCommit`. The dialog locks on its own rather than trusting the button, because the form renderer folds `isSubmitting` into `disabled` — so a submit starting while the dialog was already open used to leave the field editable for the duration of the submit. Cancel and Esc stay live in every state. This is the registered-widget half of the same defect #3400 / #3401 fixed on the built-in `form.tsx` path, so both render paths now give the same metadata the same behaviour (#3402).
|
|
318
|
+
|
|
319
|
+
- 6fe485b: fix(fields): translate the registered path's fullscreen long-text dialog (objectui#3404)
|
|
320
|
+
|
|
321
|
+
`FullscreenFieldEditor` — the expand button and dialog that `TextAreaField`
|
|
322
|
+
(`field:textarea`) and `RichTextField` (`field:markdown` / `field:html`) render
|
|
323
|
+
when `ObjectFormSchema.mobile.fullscreenLongText` is on — shipped four English
|
|
324
|
+
literals: the toggle's accessible name (`Edit {label} fullscreen`), the title
|
|
325
|
+
fallback `Edit text`, `Cancel` and `Done`.
|
|
326
|
+
|
|
327
|
+
No translation was missing. `form.fullscreen.*` and `common.cancel` have shipped
|
|
328
|
+
in all ten locale packs since objectui#3272 translated the built-in branch; this
|
|
329
|
+
path simply never consumed them. The result was visible inside a SINGLE form: a
|
|
330
|
+
zh session saw 「取消 / 完成」 on a built-in-rendered long-text field and
|
|
331
|
+
`Cancel / Done` on a registered-widget one.
|
|
332
|
+
|
|
333
|
+
All four now consume those existing keys — **no new keys, no locale-pack
|
|
334
|
+
change**. The dialog also gained the sr-only `form.fullscreen.description` the
|
|
335
|
+
built-in branch already carries, so it has an accessible description
|
|
336
|
+
(`aria-describedby`) instead of none.
|
|
337
|
+
|
|
338
|
+
Copy resolves through `useFieldTranslation()` (`createSafeTranslation`), as the
|
|
339
|
+
built-in branch does, whose English defaults are byte-identical to the literals
|
|
340
|
+
they replace — so widgets rendered with no `I18nProvider` (standalone/embedded
|
|
341
|
+
hosts) render exactly what they did before rather than raw i18n keys.
|
|
342
|
+
|
|
343
|
+
- 825bbe3: The option widgets' "this list cannot be filled" message now has one source, and
|
|
344
|
+
it is translated (objectui#3231).
|
|
345
|
+
|
|
346
|
+
FROM: `SelectField`, `MultiSelectField`, `RadioField` and `CheckboxesField` each
|
|
347
|
+
carried their own copy of the empty/gated state, each destructured the declared
|
|
348
|
+
`emptyHint` prop into `_emptyHint` and dropped it, and each rendered a hardcoded
|
|
349
|
+
English literal (`'No options available'`, `` `Select ${…} first` ``) even in a
|
|
350
|
+
Chinese or Japanese session. TO: one shared `OptionsEmptyState` — the host's
|
|
351
|
+
`emptyHint` when it supplied one, otherwise a translated fallback
|
|
352
|
+
(`fields.options.empty` / `fields.options.selectFirst`, added to all ten locale
|
|
353
|
+
packs).
|
|
354
|
+
|
|
355
|
+
`emptyHint` was declared, produced by the form renderer and transported, then
|
|
356
|
+
lost three times over — so no registered widget could ever render it. All three
|
|
357
|
+
breaks are fixed, because closing only the last one delivers nothing:
|
|
358
|
+
|
|
359
|
+
- `isOptionField` compared the raw resolved type against `'select'` /`'radio'` /
|
|
360
|
+
`'multiselect'` / `'checkboxes'`. Object-derived forms emit
|
|
361
|
+
`mapFieldTypeToFormType`'s prefixed ids (`field:select`), which matched none of
|
|
362
|
+
them, so for every option field coming from an object schema — the normal case
|
|
363
|
+
in the console — the whole cascade block was skipped and no hint was computed
|
|
364
|
+
at all. It now normalizes the `field:` prefix, the same normalization
|
|
365
|
+
`stripRegisteredFieldProps` already applied a few lines below.
|
|
366
|
+
- `stripRegisteredFieldProps` then removed the `emptyHint` key from what was
|
|
367
|
+
left. It is now forwarded to the four cascade option types, alongside
|
|
368
|
+
`dependentValues`. This stays an allow-list rather than a blanket
|
|
369
|
+
pass-through: every other registered widget spreads its leftover props onto a
|
|
370
|
+
DOM node, where an unknown `emptyHint` attribute is a React warning.
|
|
371
|
+
- the widgets themselves discarded it. Keeping it out of the `...props` spread
|
|
372
|
+
was correct; not using it afterwards was not.
|
|
373
|
+
|
|
374
|
+
User-visible effect: a dependency-gated option list now prompts with the
|
|
375
|
+
controlling field's **label** ("Select Country first") instead of its raw
|
|
376
|
+
metadata name, in the session's language; an unconfigured list says so in the
|
|
377
|
+
session's language too. The gate sentence is one i18n key shared by the renderer
|
|
378
|
+
and the widget fallback, so the two sides cannot word it differently.
|
|
379
|
+
|
|
380
|
+
Untouched: the built-in (unregistered) `select` branch of the form renderer,
|
|
381
|
+
which already consumed `emptyHint`. That is a separate live path.
|
|
382
|
+
|
|
383
|
+
- 34d9169: The record picker's filter panel now sends the AUTHORED value of a picked `select` filter option instead of the control's stringified form. Radix `Select` speaks strings — options render as `String(opt.value)` and `onValueChange` returns that string — and the panel stored it as-is, so a filter option whose value is a number or a boolean queried `{ level: "1" }` against records storing `level: 1`. The user picked an option that plainly has records and got an empty list. This bit the `lookup_filters` auto-derivation in particular: `lookup_filters: [{ field: 'level', operator: 'in', value: [1, 2, 3] }]` derives options whose values keep the author's type (`LookupFilterDef.value` is `unknown`), so every non-string option in the picker was unfilterable. The control's string is now mapped back through `col.options` at the control boundary — the control speaks string, the payload keeps the authored type — reusing the `matchOptionValue` / `toControlValue` semantics `@object-ui/components` introduced for the standalone form's select (#3090), rather than coercing in `filterValuesToRecord`, which would have to guess whether `"1"` meant `1` or `"1"`. Options coming from the object schema are strings by spec and round-trip unchanged, and the `number` / `boolean` filter inputs (which already converted explicitly) are untouched — the `select` branch is lifted to their standard, resolving the three-way inconsistency (#3422).
|
|
384
|
+
- 5881a2c: The lookup "Browse all records" Record Picker's filter panel now offers the
|
|
385
|
+
options a `select` field declares in its schema (objectui#3336). `LookupField`
|
|
386
|
+
turns each typed picker column into a filter column, and those carried no
|
|
387
|
+
`options` — so the filter panel's dropdown opened EMPTY and the column could
|
|
388
|
+
not be filtered at all, even though the same column's table cells had rendered
|
|
389
|
+
the authored option labels since objectui#3333.
|
|
390
|
+
|
|
391
|
+
`RecordPickerDialog` now fills a `select` filter column's missing `options`
|
|
392
|
+
from `fieldsMeta` (the referenced object's schema `fields` map) through the
|
|
393
|
+
same resolver — and the same i18n option translation — the table cells use, so
|
|
394
|
+
the filter dropdown and the cells can never disagree about what an option is
|
|
395
|
+
called. Explicitly authored filter `options` still win (including the ones
|
|
396
|
+
auto-derived from an `in`/`notIn` `lookup_filters` entry), and a select field
|
|
397
|
+
whose schema declares no options keeps an empty dropdown: no options are
|
|
398
|
+
synthesised from the loaded page's raw stored values.
|
|
399
|
+
|
|
400
|
+
- 9bc3709: The lookup "Browse all records" Record Picker now formats its columns with
|
|
401
|
+
the same field metadata the list view uses (objectui#3333). Previously the
|
|
402
|
+
dialog handed cell renderers a bare `{ name, type }` descriptor, so a
|
|
403
|
+
`select` column had no `options` and fell back to title-casing the raw
|
|
404
|
+
stored value (`manufacturing` rendered as "Manufacturing" instead of the
|
|
405
|
+
authored option label, e.g. "03 制造") — while the same field displayed
|
|
406
|
+
correctly in the list view and on the record detail page.
|
|
407
|
+
|
|
408
|
+
`RecordPickerDialog` gains an optional `fieldsMeta` prop (the referenced
|
|
409
|
+
object's schema `fields` map). When provided, each column's field descriptor
|
|
410
|
+
is enriched from the schema — `options` (run through the shared i18n option
|
|
411
|
+
translation), `currency`, `scale`, `precision`, `format`, `reference_to`, … —
|
|
412
|
+
and columns authored as plain strings in `lookup_columns` inherit the schema
|
|
413
|
+
field's `type`, so they format identically to typed columns. `LookupField`
|
|
414
|
+
passes the referenced object's schema it already fetches for `titleFormat`.
|
|
415
|
+
Callers that don't pass `fieldsMeta` keep the previous behavior.
|
|
416
|
+
|
|
417
|
+
- 49f7449: `field:select` now announces its validation state to assistive tech: the
|
|
418
|
+
widget's DOM pass-through lands on the Radix `SelectTrigger` — the focusable
|
|
419
|
+
`<button role="combobox">` a user actually interacts with — instead of
|
|
420
|
+
`Select.Root`, which renders no DOM element of its own and silently dropped
|
|
421
|
+
every `aria-*` the form renderer delivered (objectui#3306).
|
|
422
|
+
|
|
423
|
+
Before this, a required select that failed validation showed the red message
|
|
424
|
+
while a screen reader was told nothing: `aria-invalid`, `aria-describedby`
|
|
425
|
+
(the link to the message text) and `aria-required` (objectui#3290's state
|
|
426
|
+
channel) all landed on Root and vanished. All three now reach the trigger,
|
|
427
|
+
and the widget computes `aria-invalid` from the published `error` slot after
|
|
428
|
+
the spread, the objectui#3222 discipline — a valid field explicitly says
|
|
429
|
+
`aria-invalid="false"` rather than staying mute.
|
|
430
|
+
|
|
431
|
+
Two keys deliberately stay on Root: `name` (Root forwards it to the hidden
|
|
432
|
+
native `<select>` that takes part in form submission) and `disabled` (Root is
|
|
433
|
+
the single authority that disables trigger, items and hidden select together).
|
|
434
|
+
|
|
435
|
+
Guarding the whole class forward, a new registry-wide sweep
|
|
436
|
+
(`widget-aria-invalid-registry-e2e.test.tsx`, the objectui#3291 leak-sweep
|
|
437
|
+
paradigm) renders every registered field widget through the real form, drives
|
|
438
|
+
a real validation failure, and asserts `aria-invalid="true"` appears inside
|
|
439
|
+
the field's row. The 29 widget types measured not to deliver yet are pinned in
|
|
440
|
+
a ratchet ledger (tracked in objectui#3318): fixing one turns its ledger row
|
|
441
|
+
red until the entry is removed, so the ledger only ever shrinks.
|
|
442
|
+
|
|
443
|
+
- c7ed4c3: `TagsField` no longer ships a hardcoded Chinese input placeholder
|
|
444
|
+
(objectui#3342, AGENTS.md Commandment #-1). The placeholder now resolves
|
|
445
|
+
through the pinned chain: the author-declared `field.placeholder` wins
|
|
446
|
+
(previously ignored by this widget); otherwise the widget's own copy arrives
|
|
447
|
+
via `useFieldTranslation()` under the new `fields.tags.placeholder` key, added
|
|
448
|
+
at full parity across all locale packs (Chinese lives in the zh pack, not in
|
|
449
|
+
code); with no `I18nProvider` mounted the English default from FIELD_DEFAULTS
|
|
450
|
+
renders — never a raw key.
|
|
451
|
+
- 2409e1d: `TextAreaField`'s character counter now announces itself in the session locale. The counter block — rendered only when the field declares `maxLength` — carried the accessible name `Character count: {n} of {max}` as an English literal, and the element is an `aria-live="polite"` region, so a zh/ja/ar session had that English sentence read out on every keystroke while sighted users saw only the language-independent `{n}/{max}` digits. Nothing was wrong on screen, which is why it survived: only screen reader users could perceive it (#3406).
|
|
452
|
+
|
|
453
|
+
Unlike #3404, no key existed to consume — none of the ten locale packs had any character-count string. `fields.textarea.characterCount` is new in all ten, interpolating `{{count}}` and `{{max}}` as one sentence rather than parts assembled in code, because `ja` and `ko` put the cap before the count ("of {{max}} characters, {{count}}"), an order no concatenation can produce. The English pack value and the `FIELD_DEFAULTS` fallback are byte-identical to the literal they replace, so an `en` session and a provider-less embed both render exactly what they did before.
|
|
454
|
+
|
|
455
|
+
Behaviour is unchanged: `aria-live="polite"` and the per-keystroke recompute are deliberately untouched here and tracked separately (#3408).
|
|
456
|
+
|
|
457
|
+
- 789fe3e: `TextAreaField`'s character counter no longer re-announces itself on every keystroke. Measured on `main` in a zh session with `maxLength: 500`, typing a 52-character sentence one character at a time produced 52 distinct screen-reader announcements totalling 979 spoken characters — roughly 19x the text being written, each one cutting off the reader's echo of the letter just typed. The counter element was simultaneously the visible `{n}/{max}` digits, the carrier of the translated sentence and the `aria-live` region itself, so "re-render" and "announce" were the same event; the field also had no `aria-describedby`, so focusing it said nothing about the cap at all (#3408).
|
|
458
|
+
|
|
459
|
+
It is now the three-node shape the GOV.UK Design System character-count component uses: the visible digits are `aria-hidden` and purely decorative; the counter sentence (`fields.textarea.characterCount`, unchanged in all ten packs) has moved onto the textarea's `aria-describedby`, so focus reads "Character count: 12 of 500" once and then stays quiet; and a separate visually-hidden `aria-live="polite"` region carries a new near-limit warning, `fields.textarea.charactersRemaining` (new in all ten packs), which stays silent until the value is inside the last 10% or last 20 characters of the cap — whichever the typist reaches first — and updates only after typing pauses for a second. The same 52-keystroke probe now produces zero announcements; a run that types all the way onto a 500-character cap produces five. Any `aria-describedby` the host already supplied (the form renderer's description and error-message ids) is appended to, never replaced.
|
|
460
|
+
|
|
461
|
+
No metadata change: the counter still renders exactly when the field declares `maxLength` (or the legacy `max_length`), and a widget rendered with no `I18nProvider` still shows the same English sentences.
|
|
462
|
+
|
|
463
|
+
- f789c3b: `TextAreaField`'s fullscreen edit dialog now gives screen reader users the character count it has always shown sighted ones. The dialog's footer counter was a bare `{n}/{max}` span: no accessible name, nothing `aria-live`, and nothing tying it to the dialog's textarea — so browse mode read "5 slash 500" if it happened to sweep the footer, and focusing the input said nothing about the cap at all. The inline surface of the same field has carried a proper three-node counter since #3408, so the fullscreen branch was at zero for the same field, the same cap and the same user (#3417). It is reachable on any phone form with `ObjectFormSchema.mobile.fullscreenLongText` on, for every long-text field that declares a limit.
|
|
464
|
+
|
|
465
|
+
The counting UI is now ONE shared `CharacterCount` component that both surfaces render, instead of two hand-written copies that could only drift. In the dialog it renders `aria-hidden` digits plus a visually-hidden description carrying `fields.textarea.characterCount`, wired to the dialog's textarea through `aria-describedby`, so focus reads "Character count: 12 of 500" once and the count follows the draft as it is edited. The description ids are per surface, because the dialog's draft and the committed value diverge as soon as the user types.
|
|
466
|
+
|
|
467
|
+
The dialog deliberately gets NO live region: it is a modal opened to write at length, the description already delivers the cap on focus, and the inline surface's `aria-live` region stays mounted behind the overlay. The inline surface is unchanged — same DOM, same threshold-gated debounced announcements, same ten locale packs and the same English fallbacks with no `I18nProvider` mounted. No new i18n keys and no metadata change: the counter still renders exactly when the field declares `maxLength` (or the legacy `max_length`).
|
|
468
|
+
|
|
469
|
+
- a321fa4: `TextAreaField`'s mobile fullscreen flag converges on its one real producer
|
|
470
|
+
(objectui#3232).
|
|
471
|
+
|
|
472
|
+
FROM: the widget resolved the "show the expand affordance" decision through a
|
|
473
|
+
four-way `??` chain — a `mobileFullscreen` (camelCase) prop, the field
|
|
474
|
+
metadata's `mobile_fullscreen`, a `mobile_fullscreen` prop, and
|
|
475
|
+
`schema.mobile_fullscreen`. TO: a single read of the field metadata's
|
|
476
|
+
`mobile_fullscreen`, resolved through the `field || schema` carrier pair every
|
|
477
|
+
widget in this package already uses.
|
|
478
|
+
|
|
479
|
+
No runtime behaviour changes, because three of those four reads were
|
|
480
|
+
permanently `undefined`:
|
|
481
|
+
|
|
482
|
+
- `mobileFullscreen` (camelCase) had **no producer anywhere in the repo** — the
|
|
483
|
+
only occurrences of that spelling were the widget's own read and the
|
|
484
|
+
destructure that kept it off the DOM spread. The doc comment nonetheless
|
|
485
|
+
claimed "the host form passes `mobileFullscreen`", so the contract it
|
|
486
|
+
described had never held.
|
|
487
|
+
- `mobile_fullscreen` as a **prop** cannot arrive: the form renderer's
|
|
488
|
+
`stripRegisteredFieldProps` explicitly removes `mobile_fullscreen` and
|
|
489
|
+
`fullscreen` from the props forwarded to registered field widgets.
|
|
490
|
+
- `schema.mobile_fullscreen` was the same object `field || schema` already
|
|
491
|
+
resolves, so it could only ever restate the metadata read.
|
|
492
|
+
|
|
493
|
+
What actually drives the affordance — and is now the only thing that does — is
|
|
494
|
+
the field metadata flag `ObjectForm` stamps onto long-text fields from
|
|
495
|
+
`ObjectFormSchema.mobile.fullscreenLongText`. That path is unchanged and is now
|
|
496
|
+
pinned by tests (button, dialog, and the committed edit), so the cleanup cannot
|
|
497
|
+
have silently removed the working behaviour.
|
|
498
|
+
|
|
499
|
+
Also untouched: the built-in (unregistered) `textarea` branch of the form
|
|
500
|
+
renderer, which reads `mobile_fullscreen || fullscreen` off the form-field
|
|
501
|
+
props and renders its own `FullscreenTextarea`. That is a separate live path.
|
|
502
|
+
|
|
503
|
+
Why this is worth a changeset rather than a silent tidy-up: reads that nobody
|
|
504
|
+
writes are not free. They document a contract that does not exist — the next
|
|
505
|
+
author follows the comment, passes the prop, and is ignored without a word —
|
|
506
|
+
and a `??` chain that accepts four spellings and rejects none is exactly where
|
|
507
|
+
a misspelled key hides. With one source, a wrong spelling has no read path left
|
|
508
|
+
to absorb it. Per AGENTS.md #0.1 and Prime Directive #12, divergence like this
|
|
509
|
+
converges at the producer, not by accumulating tolerance at the consumer. No
|
|
510
|
+
host-override prop was invented in its place: inventing a key with no producer
|
|
511
|
+
is the same mistake in the other direction.
|
|
512
|
+
|
|
513
|
+
- 8d8094a: 20 more registered field widgets now announce a failed validation to assistive
|
|
514
|
+
tech: `multiselect`, `radio`, `checkboxes`, `tags`, `lookup`, `master_detail`,
|
|
515
|
+
`user`, `owner`, `file`, `image`, `location`, `object`, `color`, `rating`,
|
|
516
|
+
`code`, `avatar`, `address`, `geolocation`, `qrcode` and `object-ref` carry
|
|
517
|
+
`aria-invalid="true"` on their real focusable control after a validation
|
|
518
|
+
failure, where before the red message rendered while a screen reader was told
|
|
519
|
+
nothing (objectui#3318, the registry-wide gap objectui#3306's sweep measured).
|
|
520
|
+
|
|
521
|
+
Each widget follows the objectui#3222/#3306 pattern: the `toDomProps(props)`
|
|
522
|
+
whitelist spread goes onto the control the user actually focuses — the input,
|
|
523
|
+
the lookup trigger button, the radiogroup (`role="radiogroup"` is the
|
|
524
|
+
ARIA-designated carrier for a set of radios), every chip/checkbox/star of the
|
|
525
|
+
composite option widgets, the upload dropzone/button — followed by an explicit
|
|
526
|
+
`aria-invalid={!!error}` computed from the published `error` slot. Wrapper
|
|
527
|
+
`<div>`s never carry the state, and `name` is withheld from non-form-control
|
|
528
|
+
elements (the objectui#3291 leak class).
|
|
529
|
+
|
|
530
|
+
`Combobox` (`@object-ui/components`) now accepts standard button attributes
|
|
531
|
+
and forwards them to its focusable `role="combobox"` trigger, giving
|
|
532
|
+
combobox-based widgets an element to deliver `aria-invalid` /
|
|
533
|
+
`aria-describedby` to — the same seam objectui#3306 opened on
|
|
534
|
+
`SelectTrigger`.
|
|
535
|
+
|
|
536
|
+
Nine types remain on the objectui#3318 ratchet ledger with their blockers
|
|
537
|
+
documented there (`formula`/`summary`/`auto_number`/`vector` render no
|
|
538
|
+
focusable control; `grid`, `slider`, `signature` need component-level design;
|
|
539
|
+
`filter-condition`/`recipient-picker` deliver in their editable states but
|
|
540
|
+
render a dependency-gate hint with no control in a fresh form).
|
|
541
|
+
|
|
542
|
+
- Updated dependencies [18cd432]
|
|
543
|
+
- Updated dependencies [532cf8b]
|
|
544
|
+
- Updated dependencies [680080a]
|
|
545
|
+
- Updated dependencies [a7651e6]
|
|
546
|
+
- Updated dependencies [d915c47]
|
|
547
|
+
- Updated dependencies [b71fc92]
|
|
548
|
+
- Updated dependencies [65516ba]
|
|
549
|
+
- Updated dependencies [94c5b7c]
|
|
550
|
+
- Updated dependencies [ca0fa8f]
|
|
551
|
+
- Updated dependencies [34595eb]
|
|
552
|
+
- Updated dependencies [3889ffb]
|
|
553
|
+
- Updated dependencies [5781fb1]
|
|
554
|
+
- Updated dependencies [7e2406a]
|
|
555
|
+
- Updated dependencies [9e9e9a9]
|
|
556
|
+
- Updated dependencies [56409c2]
|
|
557
|
+
- Updated dependencies [042e09d]
|
|
558
|
+
- Updated dependencies [9cbcbf4]
|
|
559
|
+
- Updated dependencies [85c4c9c]
|
|
560
|
+
- Updated dependencies [fd54c3e]
|
|
561
|
+
- Updated dependencies [4eeb932]
|
|
562
|
+
- Updated dependencies [5c856ec]
|
|
563
|
+
- Updated dependencies [23018cc]
|
|
564
|
+
- Updated dependencies [53811d1]
|
|
565
|
+
- Updated dependencies [68b6a28]
|
|
566
|
+
- Updated dependencies [0554e88]
|
|
567
|
+
- Updated dependencies [d915c47]
|
|
568
|
+
- Updated dependencies [f44d872]
|
|
569
|
+
- Updated dependencies [28b2e65]
|
|
570
|
+
- Updated dependencies [509104a]
|
|
571
|
+
- Updated dependencies [825bbe3]
|
|
572
|
+
- Updated dependencies [6195841]
|
|
573
|
+
- Updated dependencies [5dd0127]
|
|
574
|
+
- Updated dependencies [06632e9]
|
|
575
|
+
- Updated dependencies [a415684]
|
|
576
|
+
- Updated dependencies [a4cff5b]
|
|
577
|
+
- Updated dependencies [175bd79]
|
|
578
|
+
- Updated dependencies [5af2852]
|
|
579
|
+
- Updated dependencies [f833d3a]
|
|
580
|
+
- Updated dependencies [a6ec93d]
|
|
581
|
+
- Updated dependencies [2a9513d]
|
|
582
|
+
- Updated dependencies [71be406]
|
|
583
|
+
- Updated dependencies [d22ae31]
|
|
584
|
+
- Updated dependencies [c7ed4c3]
|
|
585
|
+
- Updated dependencies [2409e1d]
|
|
586
|
+
- Updated dependencies [789fe3e]
|
|
587
|
+
- Updated dependencies [8d8094a]
|
|
588
|
+
- @object-ui/core@17.3.0
|
|
589
|
+
- @object-ui/components@17.3.0
|
|
590
|
+
- @object-ui/types@17.3.0
|
|
591
|
+
- @object-ui/i18n@17.3.0
|
|
592
|
+
- @object-ui/react@17.3.0
|
|
593
|
+
- @object-ui/providers@17.3.0
|
|
594
|
+
|
|
3
595
|
## 17.2.0
|
|
4
596
|
|
|
5
597
|
### Minor Changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|