@object-ui/fields 17.2.0 → 17.5.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.
Files changed (212) hide show
  1. package/CHANGELOG.md +1533 -0
  2. package/README.md +1 -12
  3. package/dist/FieldEditWidget.d.ts +7 -1
  4. package/dist/currency.d.ts +46 -0
  5. package/dist/field-type-alias.d.ts +19 -1
  6. package/dist/index.cjs +5 -5
  7. package/dist/index.css +633 -0
  8. package/dist/index.d.ts +52 -12
  9. package/dist/index.js +5167 -4759
  10. package/dist/widgets/AddressField.d.ts +4 -12
  11. package/dist/widgets/AvatarField.d.ts +1 -1
  12. package/dist/widgets/CharacterCount.d.ts +56 -0
  13. package/dist/widgets/CheckboxesField.d.ts +1 -1
  14. package/dist/widgets/CodeField.d.ts +1 -1
  15. package/dist/widgets/ColorField.d.ts +1 -1
  16. package/dist/widgets/CurrencyField.d.ts +1 -1
  17. package/dist/widgets/EmailField.d.ts +1 -1
  18. package/dist/widgets/FileField.d.ts +1 -1
  19. package/dist/widgets/FilterConditionField.d.ts +1 -1
  20. package/dist/widgets/FullscreenFieldEditor.d.ts +45 -0
  21. package/dist/widgets/GeolocationField.d.ts +1 -1
  22. package/dist/widgets/GridField.d.ts +12 -2
  23. package/dist/widgets/ImageField.d.ts +1 -1
  24. package/dist/widgets/LocationField.d.ts +1 -1
  25. package/dist/widgets/LookupField.d.ts +1 -1
  26. package/dist/widgets/MultiSelectField.d.ts +1 -1
  27. package/dist/widgets/ObjectField.d.ts +1 -1
  28. package/dist/widgets/ObjectRefField.d.ts +1 -1
  29. package/dist/widgets/OptionsEmptyState.d.ts +59 -0
  30. package/dist/widgets/PercentField.d.ts +1 -1
  31. package/dist/widgets/PhoneField.d.ts +1 -1
  32. package/dist/widgets/QRCodeField.d.ts +1 -1
  33. package/dist/widgets/RadioField.d.ts +1 -1
  34. package/dist/widgets/RatingField.d.ts +1 -1
  35. package/dist/widgets/RecipientPickerField.d.ts +1 -1
  36. package/dist/widgets/RecordPickerDialog.d.ts +49 -7
  37. package/dist/widgets/RichTextField.d.ts +19 -1
  38. package/dist/widgets/SelectField.d.ts +25 -5
  39. package/dist/widgets/TagsField.d.ts +1 -1
  40. package/dist/widgets/TextAreaField.d.ts +44 -5
  41. package/dist/widgets/UrlField.d.ts +1 -1
  42. package/dist/widgets/address-format.d.ts +68 -0
  43. package/dist/widgets/file-size-guard.d.ts +41 -0
  44. package/dist/widgets/nativeDateValue.d.ts +37 -0
  45. package/dist/widgets/toDomProps.d.ts +111 -0
  46. package/dist/widgets/toHostGroupProps.d.ts +91 -0
  47. package/dist/widgets/types.d.ts +128 -33
  48. package/dist/widgets/useRecordQuery.d.ts +7 -1
  49. package/dist/withFieldCarrier.d.ts +56 -0
  50. package/package.json +20 -16
  51. package/src/FieldEditWidget.tsx +17 -1
  52. package/src/__tests__/AddressCellRenderer.test.tsx +166 -0
  53. package/src/__tests__/CurrencyField.minorUnits.test.tsx +191 -0
  54. package/src/__tests__/CurrencyField.symbol.test.tsx +198 -0
  55. package/src/__tests__/DateCellRenderer.test.tsx +9 -0
  56. package/src/__tests__/NumberCellRenderer.grouping.test.tsx +146 -0
  57. package/src/__tests__/PercentCellRenderer.locale.test.tsx +152 -0
  58. package/src/__tests__/PercentCellRenderer.test.tsx +115 -0
  59. package/src/__tests__/boolean-label-association-e2e.test.tsx +298 -0
  60. package/src/__tests__/capability-multiselect-retired.test.ts +109 -0
  61. package/src/__tests__/cell-truncation.test.tsx +96 -0
  62. package/src/__tests__/composite-group-label-e2e.test.tsx +436 -0
  63. package/src/__tests__/composite-group-label-readonly-e2e.test.tsx +403 -0
  64. package/src/__tests__/date-formatter-residue-4272.test.ts +120 -0
  65. package/src/__tests__/date-locale-channel.test.tsx +305 -0
  66. package/src/__tests__/field-carrier-sdui.test.tsx +169 -0
  67. package/src/__tests__/formatCurrency.centsDigit.test.tsx +158 -0
  68. package/src/__tests__/formatCurrency.minorUnits.test.tsx +221 -0
  69. package/src/__tests__/group-labelling-declaration.test.ts +148 -0
  70. package/src/__tests__/lookupCellRecordLink.test.tsx +171 -0
  71. package/src/__tests__/percent-cell-vs-measure-4576.test.ts +115 -0
  72. package/src/__tests__/percent-formatter-locale-4553.test.ts +147 -0
  73. package/src/__tests__/percent-tie-halfup-4590.test.ts +224 -0
  74. package/src/__tests__/select-label-association-e2e.test.tsx +199 -0
  75. package/src/__tests__/spec-symbol-batch7.test.ts +29 -15
  76. package/src/__tests__/validation-feedback.test.tsx +22 -13
  77. package/src/__tests__/widget-aria-invalid-e2e.test.tsx +265 -0
  78. package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +338 -0
  79. package/src/__tests__/widget-dom-leak-e2e.test.tsx +357 -0
  80. package/src/__tests__/widget-props-contract.test.tsx +22 -9
  81. package/src/complex-widgets.test.tsx +22 -6
  82. package/src/currency.ts +130 -0
  83. package/src/datetime-widgets.test.tsx +94 -2
  84. package/src/field-type-alias.multiple.test.ts +81 -0
  85. package/src/field-type-alias.ts +58 -2
  86. package/src/index.css +44 -0
  87. package/src/index.tsx +558 -268
  88. package/src/standard-widgets.test.tsx +8 -1
  89. package/src/widgets/AddressField.postalCode.test.tsx +257 -0
  90. package/src/widgets/AddressField.tsx +107 -38
  91. package/src/widgets/AddressField.uniqueIds.test.tsx +93 -0
  92. package/src/widgets/AvatarField.tsx +9 -2
  93. package/src/widgets/BooleanField.tsx +44 -10
  94. package/src/widgets/CapabilityMultiSelectField.tsx +17 -5
  95. package/src/widgets/CharacterCount.tsx +263 -0
  96. package/src/widgets/CheckboxesField.tsx +66 -16
  97. package/src/widgets/CodeField.tsx +9 -2
  98. package/src/widgets/ColorField.tsx +11 -2
  99. package/src/widgets/CurrencyField.tsx +60 -19
  100. package/src/widgets/DateField.tsx +13 -4
  101. package/src/widgets/DateTimeField.tsx +14 -5
  102. package/src/widgets/EmailField.tsx +5 -5
  103. package/src/widgets/FileField.tsx +39 -11
  104. package/src/widgets/FilterConditionField.tsx +11 -0
  105. package/src/widgets/FormulaField.tsx +7 -2
  106. package/src/widgets/FullscreenFieldEditor.tsx +60 -0
  107. package/src/widgets/GeolocationField.tsx +49 -8
  108. package/src/widgets/GeolocationField.uniqueIds.test.tsx +88 -0
  109. package/src/widgets/GridField.test.tsx +226 -0
  110. package/src/widgets/GridField.tsx +115 -11
  111. package/src/widgets/ImageField.maxSize.test.tsx +154 -0
  112. package/src/widgets/ImageField.tsx +63 -6
  113. package/src/widgets/LocationField.tsx +9 -2
  114. package/src/widgets/LookupField.gateHintLabel.test.tsx +135 -0
  115. package/src/widgets/LookupField.hydration.test.tsx +1 -1
  116. package/src/widgets/LookupField.tsx +49 -10
  117. package/src/widgets/MasterDetailField.tsx +1 -1
  118. package/src/widgets/MultiSelectField.tsx +86 -16
  119. package/src/widgets/NumberField.tsx +3 -3
  120. package/src/widgets/ObjectField.tsx +17 -8
  121. package/src/widgets/ObjectRefField.tsx +10 -0
  122. package/src/widgets/OptionsEmptyState.no-provider.test.tsx +88 -0
  123. package/src/widgets/OptionsEmptyState.test.tsx +169 -0
  124. package/src/widgets/OptionsEmptyState.tsx +99 -0
  125. package/src/widgets/PasswordField.tsx +3 -3
  126. package/src/widgets/PeoplePicker.tsx +7 -1
  127. package/src/widgets/PercentField.tsx +5 -4
  128. package/src/widgets/PhoneField.tsx +5 -5
  129. package/src/widgets/QRCodeField.tsx +9 -2
  130. package/src/widgets/RadioField.tsx +58 -16
  131. package/src/widgets/RatingField.tsx +32 -4
  132. package/src/widgets/RecipientPickerField.tsx +18 -0
  133. package/src/widgets/RecordPickerDialog.baseFilterShapes.test.tsx +191 -0
  134. package/src/widgets/RecordPickerDialog.dateLocale.test.tsx +124 -0
  135. package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +611 -0
  136. package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +112 -0
  137. package/src/widgets/RecordPickerDialog.tsx +235 -21
  138. package/src/widgets/RichTextField.tsx +151 -24
  139. package/src/widgets/SelectField.tsx +84 -22
  140. package/src/widgets/SliderField.tsx +1 -1
  141. package/src/widgets/SummaryField.tsx +1 -1
  142. package/src/widgets/TagsField.placeholder.no-provider.test.tsx +68 -0
  143. package/src/widgets/TagsField.placeholder.test.tsx +95 -0
  144. package/src/widgets/TagsField.tsx +23 -2
  145. package/src/widgets/TextAreaField.tsx +171 -93
  146. package/src/widgets/TextField.tsx +3 -3
  147. package/src/widgets/TimeField.tsx +2 -2
  148. package/src/widgets/UrlField.tsx +5 -5
  149. package/src/widgets/UserField.tsx +4 -3
  150. package/src/widgets/VectorField.tsx +1 -1
  151. package/src/widgets/__tests__/FilterConditionField.operators.test.ts +25 -1
  152. package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +248 -0
  153. package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +116 -0
  154. package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +244 -0
  155. package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +195 -0
  156. package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +417 -0
  157. package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +287 -0
  158. package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +160 -0
  159. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +161 -0
  160. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +346 -0
  161. package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +246 -0
  162. package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +175 -0
  163. package/src/widgets/address-format.ts +120 -0
  164. package/src/widgets/file-size-guard.ts +55 -0
  165. package/src/widgets/nativeDateValue.test.ts +115 -0
  166. package/src/widgets/nativeDateValue.ts +112 -0
  167. package/src/widgets/optionWidgets.gatedOptions.test.tsx +267 -0
  168. package/src/widgets/optionWidgets.unconfiguredOptions.test.tsx +150 -0
  169. package/src/widgets/toDomProps.ts +204 -0
  170. package/src/widgets/toHostGroupProps.ts +102 -0
  171. package/src/widgets/types.ts +135 -35
  172. package/src/widgets/useFieldTranslation.ts +46 -4
  173. package/src/widgets/useRecordQuery.ts +30 -3
  174. package/src/withFieldCarrier.tsx +75 -0
  175. package/dist/FieldEditWidget.test.d.ts +0 -8
  176. package/dist/__tests__/DateCellRenderer.test.d.ts +0 -0
  177. package/dist/__tests__/NumberCellRenderer.test.d.ts +0 -0
  178. package/dist/__tests__/lookupCellDisplayField.test.d.ts +0 -0
  179. package/dist/__tests__/lookupCellDisplayName.test.d.ts +0 -0
  180. package/dist/__tests__/lookupCellReferenceKey.test.d.ts +0 -0
  181. package/dist/__tests__/registry-collision.test.d.ts +0 -0
  182. package/dist/__tests__/resolveFieldCurrency.test.d.ts +0 -1
  183. package/dist/__tests__/spec-symbol-batch7.test.d.ts +0 -8
  184. package/dist/__tests__/validation-feedback.test.d.ts +0 -0
  185. package/dist/__tests__/widget-props-contract.test.d.ts +0 -8
  186. package/dist/coerce-safe-value.test.d.ts +0 -1
  187. package/dist/complex-widgets.test.d.ts +0 -1
  188. package/dist/datetime-widgets.test.d.ts +0 -1
  189. package/dist/field-type-coverage.test.d.ts +0 -1
  190. package/dist/standard-widgets.test.d.ts +0 -0
  191. package/dist/widgets/CapabilityMultiSelectField.test.d.ts +0 -0
  192. package/dist/widgets/CheckboxesField.cascade.test.d.ts +0 -0
  193. package/dist/widgets/GridField.test.d.ts +0 -1
  194. package/dist/widgets/ImageLightbox.test.d.ts +0 -1
  195. package/dist/widgets/LookupField.dependsOn.test.d.ts +0 -8
  196. package/dist/widgets/LookupField.hydration.test.d.ts +0 -1
  197. package/dist/widgets/LookupField.idField.test.d.ts +0 -1
  198. package/dist/widgets/MultiSelectField.cascade.test.d.ts +0 -0
  199. package/dist/widgets/PeoplePicker.test.d.ts +0 -8
  200. package/dist/widgets/PeoplePickerComponents.test.d.ts +0 -8
  201. package/dist/widgets/RadioField.cascade.test.d.ts +0 -0
  202. package/dist/widgets/RecipientPickerField.test.d.ts +0 -0
  203. package/dist/widgets/SelectField.cascade.test.d.ts +0 -0
  204. package/dist/widgets/UserFieldPicker.test.d.ts +0 -8
  205. package/dist/widgets/__tests__/FilterConditionField.matchAll.test.d.ts +0 -8
  206. package/dist/widgets/__tests__/FilterConditionField.operators.test.d.ts +0 -8
  207. package/dist/widgets/deriveLookupColumns.test.d.ts +0 -1
  208. package/dist/widgets/file-value.test.d.ts +0 -1
  209. package/dist/widgets/personDisplay.test.d.ts +0 -8
  210. package/dist/widgets/recentLookups.test.d.ts +0 -1
  211. package/dist/widgets/useRecordQuery.test.d.ts +0 -8
  212. package/dist/widgets/useUploadingSignal.test.d.ts +0 -1
@@ -0,0 +1,68 @@
1
+ import { AddressValue } from '@objectstack/spec/data';
2
+ /**
3
+ * Address data structure — `z.input` of `@objectstack/spec`'s
4
+ * `AddressValueSchema`, which is what the platform stores and what
5
+ * `/api/v1/data/**` serves back.
6
+ *
7
+ * The postal code is spelled `postalCode` (objectstack#5143). `AddressField`
8
+ * used to read and write `zipCode`, a key that appears nowhere in the spec, so
9
+ * the stored postal code never reached the input — and whatever the user then
10
+ * typed into that apparently-empty box was written under a key
11
+ * `AddressValueSchema` strips: lost outright on a new record, and on an
12
+ * existing one silently discarded while the stale stored value survived. (A
13
+ * postal code nobody touched did survive an unrelated edit, because the write
14
+ * spread the whole stored object through; the issue's account of that half is
15
+ * corrected in `AddressField.postalCode.test.tsx`.) One name, on both sides:
16
+ * the contract's.
17
+ *
18
+ * IMPORTED rather than declared since objectui#4167. rc.6 publishes
19
+ * `AddressValue` under this exact name, and the local copy — whose own comment
20
+ * already claimed to be "the part names of `AddressSchema`" — declared five of
21
+ * the seven parts. `countryCode` and `formatted` were missing, which is
22
+ * objectstack#4115's failure class precisely: a canonical CLAIM over a narrower
23
+ * shape, believed by the next reader. The claim is now the code.
24
+ *
25
+ * `AddressField` still renders five inputs and {@link formatAddress} still
26
+ * composes five parts, and that is a deliberate split rather than an omission:
27
+ * `formatted` is a derived one-line rendering of the parts (what this module
28
+ * computes) and `countryCode` is the ISO pair for `country`, neither of which a
29
+ * human types into a part box. Binding the type does not publish them as
30
+ * inputs — it stops the type from asserting the platform cannot store them, and
31
+ * it means a write from the widget no longer looks type-clean while dropping a
32
+ * key the contract carries: the `{ ...address }` spread in `handleFieldChange`
33
+ * preserves both, and now says so.
34
+ *
35
+ * The declaration moved here (objectui#4037) so the display cell renderer and
36
+ * the widget share one definition; the SOURCE of that definition is the spec.
37
+ */
38
+ export type { AddressValue };
39
+ /**
40
+ * The shape written by builds up to and including 17.0.0-rc.1, whose postal
41
+ * code landed under `zipCode` (objectstack#5143). Read-time compatibility ONLY,
42
+ * and deliberately not part of {@link AddressValue}: authoring code must not be
43
+ * able to spell `zipCode`, and nothing here ever writes it back — the first
44
+ * edit of any part normalizes the record onto `postalCode` (see
45
+ * `AddressField`'s `handleFieldChange`). This is not a producer alias to be
46
+ * honoured forever; it exists to carry data THIS widget mis-wrote, and can go
47
+ * once no such data remains.
48
+ */
49
+ export type LegacyAddressValue = AddressValue & {
50
+ zipCode?: string;
51
+ };
52
+ /** Postal code of a stored address, preferring the canonical key. */
53
+ export declare function readPostalCode(addr: AddressValue): string | undefined;
54
+ /**
55
+ * Formats a stored address as a single line: `Street, City, State ZIP, Country`.
56
+ *
57
+ * Ordering and separators are NOT invented here — they are the rule
58
+ * `AddressField` already applied in its readonly branch, and the sub-field set
59
+ * its inputs expose (street / city / state / postalCode / country). Missing
60
+ * parts are dropped rather than spaced over, so a street-only address is
61
+ * `"中策路 1 号"` and never `", , ,"`.
62
+ *
63
+ * Returns `''` when no part is usable — callers decide what an empty address
64
+ * looks like (an `EmptyValue` placeholder for the cell renderer), and a value
65
+ * carrying no recognized part at all is NOT silently blanked: see
66
+ * `AddressCellRenderer`, which falls back to JSON so unknown data stays visible.
67
+ */
68
+ export declare function formatAddress(addr: AddressValue): string;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The client-side `maxSize` guard shared by the file and image widgets
3
+ * (objectui#4141).
4
+ *
5
+ * Why this is shared rather than mirrored per widget: the guard is the only
6
+ * thing standing between a declared `maxSize` and a real `presigned → PUT →
7
+ * complete` round trip, and `paramToField` copies `maxSize` onto the field
8
+ * config for **every** param type regardless of which widget ends up rendering
9
+ * it. So a widget that re-derives the check drifts silently — the image param
10
+ * shipped receiving the constraint and never reading it, while the file param
11
+ * rejected the identical pick, and nothing failed until QA drove the two side by
12
+ * side. One exported function makes the difference visible: a widget either
13
+ * calls it or conspicuously does not.
14
+ *
15
+ * Both widgets deliberately share the `fields.file.exceedsMaxSize` key too. The
16
+ * sentence names a file and a limit and says nothing file-widget-specific, it is
17
+ * already translated in all 11 built-in locales, and a parallel `fields.image.*`
18
+ * twin would be the same sentence maintained twice — drifting the moment one
19
+ * translator rewords one of them.
20
+ */
21
+ /**
22
+ * Minimal shape of the i18next `t` this module needs, matching the established
23
+ * `TranslateFn` pattern in `app-shell/src/providers/writeWarningToast`.
24
+ */
25
+ export type TranslateFn = (key: string, options?: Record<string, unknown>) => string;
26
+ /**
27
+ * The rejection message for a pick that exceeds `maxSize`, or `null` when the
28
+ * pick is within the limit — so a caller reads it as `if (err) reject`.
29
+ *
30
+ * A falsy `maxSize` (undeclared, or an explicit `0`) means **unrestricted**,
31
+ * preserving the `if (maxSize && …)` semantics the file widget has always
32
+ * applied: an undeclared limit must never turn into a 0-byte one.
33
+ *
34
+ * Takes `name` and `size` separately instead of a `File` because the image
35
+ * widget's crop path validates a re-encoded `Blob`, which carries no name of its
36
+ * own — that name comes from the cropper's output.
37
+ */
38
+ export declare function maxSizeError(t: TranslateFn, pick: {
39
+ name: string;
40
+ size: number;
41
+ }, maxSize: number | undefined): string | null;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * Stored value → `<input type="date">` value (`YYYY-MM-DD`).
10
+ *
11
+ * A leading `YYYY-MM-DD` is passed through VERBATIM rather than re-parsed: a
12
+ * date-only string is parsed as UTC midnight by `new Date()`, so reading local
13
+ * calendar components back out of it shifts the day by one everywhere west of
14
+ * Greenwich. `2026-06-17` must stay June 17th in every zone.
15
+ */
16
+ export declare function toDateInputValue(value: unknown): string;
17
+ /**
18
+ * Stored value → `<input type="datetime-local">` value (`YYYY-MM-DDTHH:mm`),
19
+ * expressed in the viewer's local wall clock — the same instant the detail
20
+ * page shows, so opening the editor never appears to move the time.
21
+ *
22
+ * A value that is ALREADY a zone-less local datetime is truncated to minutes
23
+ * in place instead of being round-tripped through `Date`, which would
24
+ * reinterpret it and then re-render it, twice risking an off-by-a-zone shift.
25
+ */
26
+ export declare function toDateTimeInputValue(value: unknown): string;
27
+ /**
28
+ * `<input type="datetime-local">` value → stored value (ISO-8601, UTC).
29
+ *
30
+ * The control hands us a zone-less local wall clock; `new Date()` reads that
31
+ * form as local time, which is the inverse of what `toDateTimeInputValue`
32
+ * wrote — the pair is what keeps read and write on one basis.
33
+ *
34
+ * An unparseable string is returned untouched rather than blanked: dropping a
35
+ * value the user can see in the box would be a data loss dressed up as a fix.
36
+ */
37
+ export declare function fromDateTimeInputValue(value: string): string;
@@ -0,0 +1,111 @@
1
+ import { DomProps as CoreDomProps } from '@object-ui/core';
2
+ /**
3
+ * The RUNTIME EXECUTOR of the "DOM pass-through" section of
4
+ * {@link FieldWidgetComponentProps} (objectui#3291).
5
+ *
6
+ * ## The defect
7
+ *
8
+ * Field widgets forwarded their leftover props to a control with a bare
9
+ * spread — `const { inputType, ...domProps } = props as any; <Input
10
+ * {...domProps} />`. Whatever a host handed the widget therefore became a DOM
11
+ * attribute. Measured on `origin/main`, a real form + a real widget:
12
+ *
13
+ * ```
14
+ * <input placeholder="PH-f" zzcanary="CANARY-STR" zzcanaryobj="[object Object]"
15
+ * zzcanarynum="42" id="…" type="text" value="" name="f">
16
+ * ```
17
+ *
18
+ * The `[object Object]` this issue is named for is an authored field-config
19
+ * key being `String()`-ed onto an attribute. React 19 does not warn: an
20
+ * all-lowercase unknown attribute is passed through in complete silence, which
21
+ * is why this survived so long.
22
+ *
23
+ * ## Why a whitelist, and not a list of keys to drop
24
+ *
25
+ * The biggest leak source is not any NAMED renderer prop — it is the open tail
26
+ * of author-supplied keys. The form renderer destructures a fixed set of known
27
+ * keys and forwards `...fieldProps` verbatim
28
+ * (`components/src/renderers/form/form.tsx`), and `SchemaRenderer` is wider
29
+ * still: it spreads the whole authored node as props and has no strip layer at
30
+ * all, so on the SDUI path a widget's own spread is the ONLY line of defence.
31
+ *
32
+ * A blacklist enumerating today's renderer-only keys (`error`, `emptyHint`,
33
+ * `dataSource`, `dependentValues`, `dependsOn`, `options`, `inputType`, …)
34
+ * would pass every one of the canaries above and would not stop the next
35
+ * authored key either. Only "keep the known-safe set, drop the rest" closes
36
+ * it, which is what this function does.
37
+ *
38
+ * ## Declared = enforced
39
+ *
40
+ * {@link FieldWidgetComponentProps} already DECLARES the closed set of keys a
41
+ * widget may receive, and {@link FieldWidgetDomProps} names the subset that may
42
+ * legitimately reach a DOM element (objectui#3221). Until now that was a
43
+ * type-level claim a widget could violate at runtime simply by spreading. This
44
+ * function is that declaration's executable form, and TWO compile-time
45
+ * assertions below bind them in both directions — forwarding an undeclared key
46
+ * and declaring an unforwarded DOM key are each a compile error. Neither
47
+ * direction can drift silently.
48
+ *
49
+ * ## Deliberately NOT forwarded
50
+ *
51
+ * `role` and other HTML global attributes are absent because the contract does
52
+ * not declare them. They reached the DOM before this change only through the
53
+ * open spread. If a field node should be able to author one, DECLARE it on
54
+ * `FieldWidgetComponentProps` first and add it here — do not reopen the spread
55
+ * (AGENTS.md #0.1: fix the contract, never widen the consumer).
56
+ *
57
+ * ## Where the mechanism lives now, and why the key list stayed here
58
+ *
59
+ * objectui#4425 phase 2 promoted this whitelist to the SDUI widget contract
60
+ * generally, so the MECHANISM — filter by a declared key list plus the `aria-*`
61
+ * / `data-*` open families — moved to `@object-ui/core`
62
+ * (`utils/dom-props.ts`, `pickDomProps`), where every plugin package can reach
63
+ * it without depending on `@object-ui/fields` (the objectui#4409
64
+ * dependency-direction method: `plugin-chatbot` declares `@object-ui/core` and
65
+ * does not declare this package).
66
+ *
67
+ * The KEY LIST did not move, because it is not the same list. Measured on
68
+ * objectui#4431:
69
+ *
70
+ * - `name` and `disabled` are here and NOT in the SDUI set: both are legal
71
+ * only on form controls, which is what every widget in this package renders
72
+ * and what `FieldWidgetComponentProps` declares. `name` on the `<div>` an
73
+ * SDUI container widget renders is a leak — one of the 14 attributes #4431
74
+ * closed — and two widgets here already hand-strip it when they spread onto
75
+ * something that is not a control (`ObjectRefField`'s trigger,
76
+ * `FileField`'s dropzone).
77
+ * - `role` is in the SDUI set and NOT here: `SchemaRenderer` resolves it for
78
+ * every node from `@objectstack/spec`'s `AriaPropsSchema`, while this
79
+ * contract deliberately does not declare it (see "Deliberately NOT
80
+ * forwarded" above).
81
+ *
82
+ * So: one mechanism, two declarations, each bound by the assertions below to
83
+ * the contract it executes. Not two judges.
84
+ */
85
+ declare const DOM_PASS_THROUGH_KEYS: readonly ["id", "name", "autoFocus", "tabIndex", "onBlur", "onFocus", "onClick", "className", "disabled"];
86
+ type DomPassThroughKey = (typeof DOM_PASS_THROUGH_KEYS)[number];
87
+ /** The subset of `P` this helper forwards, with each key's declared type. */
88
+ export type DomProps<P> = CoreDomProps<P, DomPassThroughKey>;
89
+ /**
90
+ * Keep only what may legitimately become a DOM attribute, and drop everything
91
+ * else — renderer plumbing, and any extra key an author put on the field
92
+ * config or SDUI node.
93
+ *
94
+ * Replaces the bare `{...props}` spread in every field widget that renders a
95
+ * host element:
96
+ *
97
+ * ```tsx
98
+ * // before — forwards whatever it was handed
99
+ * const { inputType, ...domProps } = props as any;
100
+ * return <Input {...domProps} … />;
101
+ *
102
+ * // after
103
+ * return <Input {...toDomProps(props)} … />;
104
+ * ```
105
+ *
106
+ * Semantic props a widget INTERPRETS (`value`, `onChange`, `field`,
107
+ * `readonly`, `error`, …) are read from `props` as before; this function only
108
+ * governs what gets spread.
109
+ */
110
+ export declare function toDomProps<P extends object>(props: P): DomProps<P>;
111
+ export {};
@@ -0,0 +1,91 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * The props that make a group-labelled widget's rendered surface the thing its
10
+ * host label actually NAMES, on the surfaces that are not controls
11
+ * (objectui#3990).
12
+ *
13
+ * ## What it is for
14
+ *
15
+ * A widget declared `labelling: 'group'` (objectui#3961) is told by the form
16
+ * renderer: your label publishes an `id` and drops its `for`, so name yourself
17
+ * by IDREF. The renderer hands down exactly two facts that address the field as
18
+ * a WHOLE:
19
+ *
20
+ * - `id` — the host's control id (`…-form-item`), the id the label's `for`
21
+ * used to point at;
22
+ * - `aria-labelledby` — the IDREF of that visible label.
23
+ *
24
+ * Every editable branch already consumes them inside its wider `toDomProps`
25
+ * spread. The branches that returned EARLY did not — a field-level
26
+ * `readonly: true`, or an option list with zero offered options — so the label
27
+ * published an id that no element in the document referenced. Measured on
28
+ * `origin/main`, one field per row, reading the host label against the DOM:
29
+ *
30
+ * ```
31
+ * multiselect readonly+value consumers=0 byLabelText=0 namedByRole=[]
32
+ * multiselect readonly+empty consumers=0 byLabelText=0 namedByRole=[]
33
+ * multiselect zeroOptions consumers=0 byLabelText=0 namedByRole=[]
34
+ * multiselect editable consumers=1 byLabelText=1 namedByRole=[group:1]
35
+ * ```
36
+ *
37
+ * All seven group-labelled types measured the same, in every readonly state.
38
+ * This is quieter than the dangling `for` #3961 replaced: a `for` that resolves
39
+ * to nothing can at least be swept for, while "a label published an id and
40
+ * nobody consumed it" looks like healthy markup.
41
+ *
42
+ * ## Why this narrow pair, and not the whole `toDomProps` result
43
+ *
44
+ * Everything else in the DOM pass-through addresses a CONTROL, and a readonly
45
+ * display has none: `aria-describedby` / `aria-required` are announced when
46
+ * focus lands on something focusable, `disabled` / `tabIndex` / the focus
47
+ * handlers only mean something on an interactive element, and `name` is
48
+ * DOM-legal on form controls only — on a `div` it is exactly the leak
49
+ * objectui#3291 sweeps for. Spreading the full result would also put the host's
50
+ * `className` on top of the widget's own, because the widgets whose readonly
51
+ * surface is a placeholder or a plain container (`EmptyValue`, `FileField`,
52
+ * `AddressField`) keep `className` inside `props`.
53
+ *
54
+ * ## Why `role` travels with the two keys
55
+ *
56
+ * `aria-labelledby` on a role-less `div` / `span` names NOTHING — `generic`
57
+ * prohibits an author name — so the pair is inert without a role that supports
58
+ * naming. `group` is the one that fits a readonly surface: it names a set of
59
+ * values (chips, checked labels, stars, file names, a formatted address)
60
+ * without promising the interactivity `textbox` or `radiogroup` would. It is
61
+ * deliberately NOT the role the same widget answers with while editable:
62
+ * `RadioField`'s readonly branch renders the chosen label as text with no radios
63
+ * left in it, and `FileField`'s renders file names with no dropzone button.
64
+ *
65
+ * The role is emitted ONLY when a host actually named the field, mirroring
66
+ * every editable branch's `isLabelledGroup` test: standalone rendering (the
67
+ * grid's inline cell editor, a bare SDUI node) hands down neither key, so every
68
+ * value here is `undefined`, React emits no attribute, and that markup stays
69
+ * byte-identical to what it was.
70
+ */
71
+ export interface HostGroupProps {
72
+ /** The host's control id — the id its label would have pointed `for` at. */
73
+ id?: string;
74
+ /** IDREF of the host's visible label. */
75
+ 'aria-labelledby'?: string;
76
+ /** `'group'` when — and only when — a host named this surface. */
77
+ role?: 'group';
78
+ }
79
+ /**
80
+ * Read the two whole-field keys out of a widget's leftover props, plus the role
81
+ * that makes them mean something. See {@link HostGroupProps}.
82
+ *
83
+ * Routed through {@link toDomProps} rather than reading `props` directly so the
84
+ * pair keeps ONE gate: those keys reach an element only if the DOM pass-through
85
+ * whitelist still forwards them, and its two compile-time assertions bind that
86
+ * whitelist to the props contract in both directions.
87
+ */
88
+ export declare function toHostGroupProps(props: {
89
+ id?: string;
90
+ 'aria-labelledby'?: string;
91
+ }): HostGroupProps;
@@ -1,5 +1,47 @@
1
1
  import { AriaAttributes, FocusEventHandler, MouseEventHandler } from 'react';
2
2
  import { DependsOnInput, FieldMetadata } from '@object-ui/types';
3
+ /**
4
+ * DOM pass-through: what a widget's `...props` spread may legitimately put on
5
+ * the element it renders.
6
+ *
7
+ * A NAMED type rather than an inline block of {@link FieldWidgetComponentProps}
8
+ * so the compiler can bind it to its runtime executor in BOTH directions
9
+ * (objectui#3291). `toDomProps` asserts:
10
+ *
11
+ * - every key it forwards is declared on the widget contract — deleting one
12
+ * here without deleting it there is a compile error;
13
+ * - every key of THIS type is one it forwards — adding one here without
14
+ * adding it to `DOM_PASS_THROUGH_KEYS` is a compile error too.
15
+ *
16
+ * The second direction is the one that matters for the failure mode this repo
17
+ * treats as first-class: DECLARED BUT NOT DELIVERED (objectui#3290's
18
+ * `aria-required` that never reached a control, objectui#3222's validation slot
19
+ * nobody produced). Without it, a key added here would type-check, read as
20
+ * supported, and silently never reach the DOM — and the leak test cannot see
21
+ * that class of bug, because it looks for attributes that ARRIVE, not for ones
22
+ * that go missing.
23
+ *
24
+ * `className` and `disabled` are deliberately NOT here. They are DOM-legal and
25
+ * `toDomProps` does forward them, but they are declared on the controlled-input
26
+ * block of {@link FieldWidgetComponentProps} because widgets also INTERPRET
27
+ * them (className is composed with the widget's own classes; disabled is OR-ed
28
+ * with readonly). They are therefore bound in the forward direction only.
29
+ *
30
+ * `AriaAttributes` and the `data-${string}` family are matched by prefix at
31
+ * runtime rather than key-by-key, so they are intersected in separately.
32
+ *
33
+ * Adding a key here is a contract change: say who produces it and who reads it.
34
+ */
35
+ export type FieldWidgetDomProps = {
36
+ id?: string;
37
+ /** react-hook-form's field name, spread in by the form renderer. */
38
+ name?: string;
39
+ autoFocus?: boolean;
40
+ tabIndex?: number;
41
+ onBlur?: FocusEventHandler<HTMLElement>;
42
+ onFocus?: FocusEventHandler<HTMLElement>;
43
+ onClick?: MouseEventHandler<HTMLElement>;
44
+ };
3
45
  /**
4
46
  * Props every field widget in this package receives at RUNTIME.
5
47
  *
@@ -13,13 +55,27 @@ import { DependsOnInput, FieldMetadata } from '@object-ui/types';
13
55
  * the widgets in this directory actually implement, against
14
56
  * `@object-ui/types`'s much richer `FieldMetadata`.
15
57
  *
16
- * The two are NOT interchangeable, and the divergence is not cosmetic: the
17
- * spec's contract names the error slot `error`, this one names it
18
- * `errorMessage`, so a widget written to the spec's declared props renders no
19
- * validation message here. That divergence is tracked in objectui#3222 and is
20
- * deliberately NOT resolved here — but it is now *visible*: reading
21
- * `props.error` or `props.required` off this type is a compile error rather
22
- * than a silent `any`.
58
+ * The two are NOT interchangeable, but they no longer disagree on the
59
+ * validation slot: this type used to call it `errorMessage` while the spec
60
+ * calls it `error`, so a widget written to the published contract read
61
+ * `undefined` forever. objectui#3222 resolved that in the direction the
62
+ * contract points — the slot below IS the spec's `error`, and the form
63
+ * renderer now produces it (see `error`'s own doc comment). Reading
64
+ * `props.required` off this type is still a compile error rather than a
65
+ * silent `any`; that key is deliberately NOT lowered here (see below).
66
+ *
67
+ * ## Why `required` is still absent, though the spec declares it
68
+ *
69
+ * The required MARKER is drawn exactly once, by the form renderer's
70
+ * `<FormLabel>` (the `*` with `aria-label="required"`, which the label
71
+ * association folds into the control's accessible name). Handing `required`
72
+ * to widgets would give that marker a second possible author, and the very
73
+ * next AI-written widget draws its own asterisk — the same double-display
74
+ * failure that keeps the validation TEXT out of the widget below. The a11y
75
+ * state a widget genuinely could carry is `aria-required` on the input, and
76
+ * that needs no new key at all: `AriaAttributes` is already part of this type
77
+ * and every widget already forwards it to its control. Tracked separately;
78
+ * do not add `required` here to "align" without that decision.
23
79
  *
24
80
  * ## Why there is no `[key: string]: any` (objectui#3221)
25
81
  *
@@ -39,37 +95,66 @@ import { DependsOnInput, FieldMetadata } from '@object-ui/types';
39
95
  * is expressed as a template-literal index signature, which — unlike
40
96
  * `[key: string]` — leaves `keyof` finite, so an undeclared prop still fails.
41
97
  *
98
+ * ## Why there is no `schema` (objectui#3233)
99
+ *
100
+ * `schema` used to be declared here as a SECOND carrier for what `field`
101
+ * already means — `SchemaRenderer` handed the SDUI node down as `schema`, the
102
+ * form renderer forwarded `schema={props.field || props.schema || props}`
103
+ * *alongside* `field`, and ~30 widgets resolved their config as
104
+ * `field || schema`. One concept, two keys, two producers: exactly the
105
+ * consumer-side tolerance AGENTS.md #0.1 forbids, and exactly the shape that
106
+ * lets an AI-written widget pick the wrong spelling and still "work" under one
107
+ * host while reading `undefined` under another.
108
+ *
109
+ * v17 converged it at the PRODUCERS. `field` is the only carrier a widget ever
110
+ * sees; the SDUI node → `field` translation happens once, in this package's
111
+ * registry adapter (`withFieldCarrier`), not ~30 times in the widgets. Reading
112
+ * `props.schema` is now a compile error rather than a silent second contract.
113
+ *
42
114
  * Adding a key here is a contract change: say who produces it and who reads it.
43
115
  */
44
116
  export type FieldWidgetComponentProps<T = any> = {
45
117
  value: T;
46
118
  onChange: (val: T) => void;
47
119
  /**
48
- * The field's metadata. Deliberately the looser `@object-ui/types` shape
49
- * rather than the spec's, to avoid a circular dependency for now.
120
+ * The field's metadata, and the SINGLE carrier for it (objectui#3233).
121
+ *
122
+ * Deliberately the looser `@object-ui/types` shape rather than the spec's,
123
+ * to avoid a circular dependency for now.
124
+ *
125
+ * Every host produces it: the form renderer's `renderFieldComponent`, the
126
+ * inline-edit hosts (`FieldEditWidget`), `ActionParamDialog` /
127
+ * `BulkActionDialog`, and — since #3233 — the registry adapter that adapts
128
+ * `SchemaRenderer`'s SDUI node onto this contract (`withFieldCarrier` in
129
+ * `packages/fields/src/withFieldCarrier.tsx`). A widget reads `props.field`
130
+ * and nothing else; there is no second key to check.
50
131
  */
51
132
  field: FieldMetadata;
52
133
  readonly?: boolean;
53
134
  disabled?: boolean;
54
135
  className?: string;
55
- errorMessage?: string;
136
+ /**
137
+ * The active validation message for this field, named as
138
+ * `@objectstack/spec/ui`'s `FieldWidgetPropsSchema` names it (objectui#3222).
139
+ *
140
+ * **Producer**: the form renderer, from react-hook-form's
141
+ * `fieldState.error?.message` (`packages/components/src/renderers/form/
142
+ * form.tsx`). Before #3222 nothing in the repo produced it under EITHER
143
+ * spelling, so the seven widgets computing `aria-invalid={!!errorMessage}`
144
+ * were computing it from a permanent `undefined` — `aria-invalid` was never
145
+ * once set and a screen reader was never told the field had failed.
146
+ *
147
+ * **Consumer**: a widget reads this ONLY to drive `aria-invalid` on the
148
+ * control it renders. The message TEXT stays with the form renderer's
149
+ * `<FormMessage/>`; a widget that also renders it double-displays it.
150
+ */
151
+ error?: string;
56
152
  /**
57
153
  * Upload widgets (`file`/`image`) fire this when their in-progress state
58
154
  * flips, so a host can block submit until a presigned upload settles. Other
59
155
  * widgets ignore it.
60
156
  */
61
157
  onUploadingChange?: (uploading: boolean) => void;
62
- /**
63
- * The raw authored node the widget was rendered from. Two hosts supply it:
64
- * `SchemaRenderer` (`<Component schema={schema} {...schema} />`) and the
65
- * form renderer's `renderFieldComponent` (`schema={props.field || ...}`),
66
- * which is why ~25 widgets read `field || schema` for their config.
67
- *
68
- * This is a second carrier for what `field` already means, i.e. a de-facto
69
- * second contract (AGENTS.md #0.1) — declared here so it is at least
70
- * visible; converging the two is tracked separately.
71
- */
72
- schema?: FieldMetadata | Record<string, unknown>;
73
158
  /**
74
159
  * DataSource for widgets that query records (lookup / user / object-ref /
75
160
  * recipient-picker / grid). Injected by the form renderer for the field
@@ -93,9 +178,27 @@ export type FieldWidgetComponentProps<T = any> = {
93
178
  */
94
179
  dependsOn?: DependsOnInput;
95
180
  /**
96
- * Hint shown when a dependency-gated option list is still waiting on its
97
- * controlling field. Forwarded by the form renderer; the option widgets in
98
- * this package currently discard it and render their own message instead.
181
+ * Controlling-field name → its human LABEL, for widgets that have to NAME a
182
+ * sibling field in user-visible copy (the dependent-lookup gate hint).
183
+ *
184
+ * Only the form renderer knows a field's label — the widget sees its own
185
+ * metadata and a `depends_on` list of API names. Without this map the gate
186
+ * sentence interpolated the raw API name into every locale, `en` included
187
+ * ("Select crm_account first"), which is an internal identifier leaking into
188
+ * the UI, not merely an untranslated word (objectstack#5407). This is the
189
+ * lookup-side counterpart of `emptyHint`, which the form already resolves to
190
+ * labels for the fixed-option widgets. A name with no entry falls back to
191
+ * itself, so a host that passes nothing renders exactly what it did before.
192
+ */
193
+ dependsOnLabels?: Record<string, string>;
194
+ /**
195
+ * Hint shown when an option list cannot be filled — typically a
196
+ * dependency-gated list still waiting on its controlling field (#2284).
197
+ * Forwarded by the form renderer, which resolves the controlling fields to
198
+ * their human LABELS. **When supplied it wins**; the option widgets fall back
199
+ * to their own translated copy only when a host computed none (objectui#3231
200
+ * — they used to discard this prop and always render their own hardcoded
201
+ * English). See `OptionsEmptyState`, the single consumer.
99
202
  */
100
203
  emptyHint?: string;
101
204
  /**
@@ -114,15 +217,7 @@ export type FieldWidgetComponentProps<T = any> = {
114
217
  * user had typed. Also declarable on the field metadata.
115
218
  */
116
219
  onCreateNew?: (searchQuery: string) => void;
117
- id?: string;
118
- /** react-hook-form's field name, spread in by the form renderer. */
119
- name?: string;
120
- autoFocus?: boolean;
121
- tabIndex?: number;
122
- onBlur?: FocusEventHandler<HTMLElement>;
123
- onFocus?: FocusEventHandler<HTMLElement>;
124
- onClick?: MouseEventHandler<HTMLElement>;
125
- } & AriaAttributes & {
220
+ } & FieldWidgetDomProps & AriaAttributes & {
126
221
  /**
127
222
  * Arbitrary `data-*` attributes (test ids, analytics hooks). Open by design,
128
223
  * but a template-literal key — `keyof` stays finite, so this does NOT
@@ -46,8 +46,14 @@ export interface UseRecordQueryOptions {
46
46
  * `$filter` — already merged by the caller (base `lookup_filters`, dependent
47
47
  * lookup chain, candidate hygiene like `banned != true`, …). Compared by
48
48
  * value, so a referentially-new-but-equal object each render will not loop.
49
+ *
50
+ * Either the `QueryParams.$filter` record form or a lowered ObjectQL AST node
51
+ * (an array), since the picker's merge now produces both (#3831). Typed
52
+ * `unknown` rather than `Record< string, any >` so an array cannot slip
53
+ * through a type that silently accepts it; emptiness is decided by
54
+ * {@link hasFilter}, not by `Object.keys`.
49
55
  */
50
- filter?: Record<string, any>;
56
+ filter?: unknown;
51
57
  /** `$expand` — related entities to include (e.g. `['primary_business_unit_id']`). */
52
58
  expand?: string[];
53
59
  /** `$searchFields` — narrow the server searchable set (ADR-0061). */
@@ -0,0 +1,56 @@
1
+ import { default as React } from 'react';
2
+ /**
3
+ * Adapt `SchemaRenderer`'s SDUI carrier onto the field-widget contract —
4
+ * objectui#3233, the producer-side half of retiring `schema` from
5
+ * {@link import('./widgets/types').FieldWidgetComponentProps}.
6
+ *
7
+ * ## Why this exists
8
+ *
9
+ * Two kinds of host render a registered field widget, and they name the
10
+ * metadata differently *for good reason*:
11
+ *
12
+ * - the **form renderer** (`renderFieldComponent` in
13
+ * `@object-ui/components`), the inline-edit hosts (`FieldEditWidget`) and
14
+ * the action dialogs pass **`field`** — the field metadata they already
15
+ * hold;
16
+ * - **`SchemaRenderer`** (`@object-ui/react`) passes **`schema`** — the
17
+ * authored node. That key is the UNIVERSAL SDUI contract *every* registered
18
+ * component receives (`element:*`, `page:*`, `object-grid`,
19
+ * `ReportRenderer` …). It is not being retired and must not be: it is what
20
+ * the renderer means by "the node you were rendered from".
21
+ *
22
+ * What WAS wrong is that both keys reached the widget, so ~30 widgets resolved
23
+ * their config as `field || schema` — one concept under two spellings, a second
24
+ * de-facto contract on the widget side (AGENTS.md #0.1) and precisely the shape
25
+ * that lets an AI-written widget pick one spelling and appear to work under one
26
+ * host while reading `undefined` under the other.
27
+ *
28
+ * The translation belongs at the seam, exactly once: here. Downstream `field`
29
+ * is the only carrier, and a widget reading `props.schema` no longer compiles.
30
+ *
31
+ * ## Guarantee
32
+ *
33
+ * Payload-preserving by construction: the node is forwarded **by reference**,
34
+ * so a widget's `field` is the very object it used to read through `schema`.
35
+ * `field` wins when a host supplies both (the form path always does, and it
36
+ * holds the resolved metadata rather than the raw node).
37
+ *
38
+ * `schema` is *consumed* here rather than forwarded — with the key gone from
39
+ * the contract, passing it on would only reach the DOM as a stray attribute
40
+ * through a widget's `...props` spread.
41
+ *
42
+ * ## For widget authors outside this repo
43
+ *
44
+ * Registering a widget raw means `SchemaRenderer` hands it `schema` and no
45
+ * `field`. Wrap it once at registration and the widget only ever implements
46
+ * `FieldWidgetComponentProps`:
47
+ *
48
+ * ```tsx
49
+ * import { withFieldCarrier } from '@object-ui/fields';
50
+ *
51
+ * ComponentRegistry.register('color', withFieldCarrier(ColorPickerField), {
52
+ * namespace: 'field',
53
+ * });
54
+ * ```
55
+ */
56
+ export declare function withFieldCarrier<P extends object>(FieldWidget: React.ComponentType<P>): React.FC<Record<string, any>>;