@object-ui/fields 17.5.0 → 17.7.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 (249) hide show
  1. package/CHANGELOG.md +6150 -0
  2. package/README.md +108 -8
  3. package/dist/FieldEditWidget.d.ts +70 -8
  4. package/dist/coerceToSafeValue.d.ts +54 -0
  5. package/dist/currency.d.ts +1 -1
  6. package/dist/field-type-alias.d.ts +16 -0
  7. package/dist/index.cjs +5 -5
  8. package/dist/index.css +53 -11
  9. package/dist/index.d.ts +653 -128
  10. package/dist/index.js +4275 -3171
  11. package/dist/widgets/AddressField.d.ts +3 -3
  12. package/dist/widgets/AutoNumberField.d.ts +1 -1
  13. package/dist/widgets/AvatarField.d.ts +20 -4
  14. package/dist/widgets/BooleanField.d.ts +2 -2
  15. package/dist/widgets/CapabilityMultiSelectField.d.ts +1 -1
  16. package/dist/widgets/CheckboxesField.d.ts +1 -1
  17. package/dist/widgets/CodeField.d.ts +1 -1
  18. package/dist/widgets/ColorField.d.ts +1 -1
  19. package/dist/widgets/CurrencyField.d.ts +1 -1
  20. package/dist/widgets/DateField.d.ts +2 -2
  21. package/dist/widgets/DateTimeField.d.ts +2 -2
  22. package/dist/widgets/EmailField.d.ts +1 -1
  23. package/dist/widgets/FileField.d.ts +18 -2
  24. package/dist/widgets/FilterConditionField.d.ts +43 -2
  25. package/dist/widgets/FormulaField.d.ts +1 -1
  26. package/dist/widgets/FullscreenFieldEditor.d.ts +9 -2
  27. package/dist/widgets/GeolocationField.d.ts +10 -4
  28. package/dist/widgets/GridField.d.ts +102 -86
  29. package/dist/widgets/ImageField.d.ts +1 -1
  30. package/dist/widgets/LocationField.d.ts +31 -4
  31. package/dist/widgets/LookupField.d.ts +9 -1
  32. package/dist/widgets/MasterDetailField.d.ts +1 -1
  33. package/dist/widgets/MultiSelectField.d.ts +1 -1
  34. package/dist/widgets/NumberField.d.ts +2 -2
  35. package/dist/widgets/ObjectField.d.ts +10 -3
  36. package/dist/widgets/ObjectRefField.d.ts +1 -1
  37. package/dist/widgets/OptionsEmptyState.d.ts +38 -8
  38. package/dist/widgets/PasswordField.d.ts +15 -1
  39. package/dist/widgets/PeoplePicker.d.ts +17 -2
  40. package/dist/widgets/PercentField.d.ts +3 -2
  41. package/dist/widgets/PersonRow.d.ts +13 -2
  42. package/dist/widgets/PhoneField.d.ts +1 -1
  43. package/dist/widgets/QRCodeField.d.ts +1 -1
  44. package/dist/widgets/RadioField.d.ts +1 -1
  45. package/dist/widgets/RatingField.d.ts +1 -1
  46. package/dist/widgets/RecipientPickerField.d.ts +44 -1
  47. package/dist/widgets/RecordPickerDialog.d.ts +33 -16
  48. package/dist/widgets/RichTextField.d.ts +37 -1
  49. package/dist/widgets/SelectField.d.ts +1 -1
  50. package/dist/widgets/SelectionTray.d.ts +13 -2
  51. package/dist/widgets/SignatureField.d.ts +32 -2
  52. package/dist/widgets/SliderField.d.ts +37 -2
  53. package/dist/widgets/SummaryField.d.ts +1 -1
  54. package/dist/widgets/TagsField.d.ts +1 -1
  55. package/dist/widgets/TextAreaField.d.ts +6 -2
  56. package/dist/widgets/TextField.d.ts +2 -2
  57. package/dist/widgets/TimeField.d.ts +2 -2
  58. package/dist/widgets/UrlField.d.ts +1 -1
  59. package/dist/widgets/UserField.d.ts +1 -1
  60. package/dist/widgets/VectorField.d.ts +1 -1
  61. package/dist/widgets/_cell-renderer-bridge.d.ts +1 -1
  62. package/dist/widgets/address-format.d.ts +27 -7
  63. package/dist/widgets/file-affordance.d.ts +33 -0
  64. package/dist/widgets/file-size-guard.d.ts +5 -23
  65. package/dist/widgets/file-value.d.ts +38 -9
  66. package/dist/widgets/lookupColumnDisplay.d.ts +110 -0
  67. package/dist/widgets/nativeDateValue.d.ts +9 -27
  68. package/dist/widgets/number-format.d.ts +137 -0
  69. package/dist/widgets/numberBadInput.d.ts +135 -0
  70. package/dist/widgets/personDisplay.d.ts +22 -4
  71. package/dist/widgets/richTextDisplay.d.ts +116 -0
  72. package/dist/widgets/toDomProps.d.ts +10 -1
  73. package/dist/widgets/toHostGroupProps.d.ts +172 -21
  74. package/dist/widgets/toHostProps.d.ts +105 -0
  75. package/dist/widgets/types.d.ts +19 -6
  76. package/dist/widgets/upload-error-message.d.ts +6 -0
  77. package/dist/widgets/uploadingScope.d.ts +142 -0
  78. package/dist/widgets/useCascadingOptions.d.ts +3 -2
  79. package/dist/widgets/useRecordQuery.d.ts +4 -2
  80. package/dist/widgets/useUploadingSignal.d.ts +8 -0
  81. package/package.json +11 -13
  82. package/dist/widgets/CharacterCount.d.ts +0 -56
  83. package/src/FieldEditWidget.test.ts +0 -133
  84. package/src/FieldEditWidget.tsx +0 -207
  85. package/src/__tests__/AddressCellRenderer.test.tsx +0 -166
  86. package/src/__tests__/CurrencyField.minorUnits.test.tsx +0 -191
  87. package/src/__tests__/CurrencyField.symbol.test.tsx +0 -198
  88. package/src/__tests__/DateCellRenderer.test.tsx +0 -82
  89. package/src/__tests__/NumberCellRenderer.grouping.test.tsx +0 -146
  90. package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
  91. package/src/__tests__/PercentCellRenderer.locale.test.tsx +0 -152
  92. package/src/__tests__/PercentCellRenderer.test.tsx +0 -115
  93. package/src/__tests__/boolean-label-association-e2e.test.tsx +0 -298
  94. package/src/__tests__/capability-multiselect-retired.test.ts +0 -109
  95. package/src/__tests__/cell-truncation.test.tsx +0 -96
  96. package/src/__tests__/composite-group-label-e2e.test.tsx +0 -436
  97. package/src/__tests__/composite-group-label-readonly-e2e.test.tsx +0 -403
  98. package/src/__tests__/date-formatter-residue-4272.test.ts +0 -120
  99. package/src/__tests__/date-locale-channel.test.tsx +0 -305
  100. package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
  101. package/src/__tests__/formatCurrency.centsDigit.test.tsx +0 -158
  102. package/src/__tests__/formatCurrency.minorUnits.test.tsx +0 -221
  103. package/src/__tests__/group-labelling-declaration.test.ts +0 -148
  104. package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
  105. package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
  106. package/src/__tests__/lookupCellRecordLink.test.tsx +0 -171
  107. package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
  108. package/src/__tests__/percent-cell-vs-measure-4576.test.ts +0 -115
  109. package/src/__tests__/percent-formatter-locale-4553.test.ts +0 -147
  110. package/src/__tests__/percent-tie-halfup-4590.test.ts +0 -224
  111. package/src/__tests__/registry-collision.test.ts +0 -91
  112. package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
  113. package/src/__tests__/select-label-association-e2e.test.tsx +0 -199
  114. package/src/__tests__/spec-symbol-batch7.test.ts +0 -126
  115. package/src/__tests__/validation-feedback.test.tsx +0 -361
  116. package/src/__tests__/widget-aria-invalid-e2e.test.tsx +0 -265
  117. package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +0 -338
  118. package/src/__tests__/widget-dom-leak-e2e.test.tsx +0 -357
  119. package/src/__tests__/widget-props-contract.test.tsx +0 -142
  120. package/src/coerce-safe-value.test.ts +0 -34
  121. package/src/complex-widgets.test.tsx +0 -924
  122. package/src/currency.ts +0 -144
  123. package/src/datetime-widgets.test.tsx +0 -185
  124. package/src/field-type-alias.multiple.test.ts +0 -81
  125. package/src/field-type-alias.ts +0 -156
  126. package/src/field-type-coverage.test.ts +0 -67
  127. package/src/index.css +0 -44
  128. package/src/index.tsx +0 -2745
  129. package/src/standard-widgets.test.tsx +0 -364
  130. package/src/widgets/AddressField.postalCode.test.tsx +0 -257
  131. package/src/widgets/AddressField.tsx +0 -182
  132. package/src/widgets/AddressField.uniqueIds.test.tsx +0 -93
  133. package/src/widgets/AutoNumberField.tsx +0 -18
  134. package/src/widgets/AvatarField.tsx +0 -117
  135. package/src/widgets/BooleanField.tsx +0 -86
  136. package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
  137. package/src/widgets/CapabilityMultiSelectField.tsx +0 -241
  138. package/src/widgets/CharacterCount.tsx +0 -263
  139. package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
  140. package/src/widgets/CheckboxesField.tsx +0 -169
  141. package/src/widgets/CodeField.tsx +0 -41
  142. package/src/widgets/ColorField.tsx +0 -53
  143. package/src/widgets/CurrencyField.tsx +0 -135
  144. package/src/widgets/DateField.tsx +0 -40
  145. package/src/widgets/DateTimeField.tsx +0 -46
  146. package/src/widgets/EmailField.tsx +0 -37
  147. package/src/widgets/FileField.tsx +0 -512
  148. package/src/widgets/FilterConditionField.tsx +0 -454
  149. package/src/widgets/FormulaField.tsx +0 -38
  150. package/src/widgets/FullscreenFieldEditor.tsx +0 -60
  151. package/src/widgets/GeolocationField.tsx +0 -193
  152. package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
  153. package/src/widgets/GridField.test.tsx +0 -632
  154. package/src/widgets/GridField.tsx +0 -1070
  155. package/src/widgets/ImageCropperDialog.tsx +0 -245
  156. package/src/widgets/ImageField.maxSize.test.tsx +0 -154
  157. package/src/widgets/ImageField.tsx +0 -305
  158. package/src/widgets/ImageLightbox.test.tsx +0 -52
  159. package/src/widgets/ImageLightbox.tsx +0 -108
  160. package/src/widgets/LocationField.tsx +0 -57
  161. package/src/widgets/LookupField.dependsOn.test.tsx +0 -233
  162. package/src/widgets/LookupField.gateHintLabel.test.tsx +0 -135
  163. package/src/widgets/LookupField.hydration.test.tsx +0 -136
  164. package/src/widgets/LookupField.idField.test.tsx +0 -61
  165. package/src/widgets/LookupField.tsx +0 -1295
  166. package/src/widgets/MarkdownContent.tsx +0 -19
  167. package/src/widgets/MasterDetailField.tsx +0 -131
  168. package/src/widgets/MultiSelectField.cascade.test.tsx +0 -143
  169. package/src/widgets/MultiSelectField.tsx +0 -196
  170. package/src/widgets/NumberField.tsx +0 -49
  171. package/src/widgets/ObjectField.tsx +0 -94
  172. package/src/widgets/ObjectRefField.tsx +0 -121
  173. package/src/widgets/OptionsEmptyState.no-provider.test.tsx +0 -88
  174. package/src/widgets/OptionsEmptyState.test.tsx +0 -169
  175. package/src/widgets/OptionsEmptyState.tsx +0 -99
  176. package/src/widgets/PasswordField.tsx +0 -49
  177. package/src/widgets/PeoplePicker.test.tsx +0 -360
  178. package/src/widgets/PeoplePicker.tsx +0 -579
  179. package/src/widgets/PeoplePickerComponents.test.tsx +0 -104
  180. package/src/widgets/PercentField.tsx +0 -95
  181. package/src/widgets/PersonRow.tsx +0 -117
  182. package/src/widgets/PhoneField.tsx +0 -37
  183. package/src/widgets/QRCodeField.tsx +0 -103
  184. package/src/widgets/RadioField.cascade.test.tsx +0 -141
  185. package/src/widgets/RadioField.tsx +0 -143
  186. package/src/widgets/RatingField.tsx +0 -94
  187. package/src/widgets/RecipientPickerField.test.tsx +0 -157
  188. package/src/widgets/RecipientPickerField.tsx +0 -188
  189. package/src/widgets/RecordPickerDialog.baseFilterShapes.test.tsx +0 -191
  190. package/src/widgets/RecordPickerDialog.dateLocale.test.tsx +0 -124
  191. package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
  192. package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
  193. package/src/widgets/RecordPickerDialog.tsx +0 -1365
  194. package/src/widgets/RichTextField.tsx +0 -176
  195. package/src/widgets/SelectField.cascade.test.tsx +0 -138
  196. package/src/widgets/SelectField.tsx +0 -193
  197. package/src/widgets/SelectionTray.tsx +0 -110
  198. package/src/widgets/SignatureField.tsx +0 -140
  199. package/src/widgets/SliderField.tsx +0 -39
  200. package/src/widgets/SummaryField.tsx +0 -31
  201. package/src/widgets/TagsField.placeholder.no-provider.test.tsx +0 -68
  202. package/src/widgets/TagsField.placeholder.test.tsx +0 -95
  203. package/src/widgets/TagsField.tsx +0 -88
  204. package/src/widgets/TextAreaField.tsx +0 -213
  205. package/src/widgets/TextField.tsx +0 -45
  206. package/src/widgets/TimeField.tsx +0 -31
  207. package/src/widgets/UrlField.tsx +0 -46
  208. package/src/widgets/UserField.tsx +0 -59
  209. package/src/widgets/UserFieldPicker.test.tsx +0 -78
  210. package/src/widgets/VectorField.tsx +0 -29
  211. package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +0 -66
  212. package/src/widgets/__tests__/FilterConditionField.operators.test.ts +0 -166
  213. package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +0 -248
  214. package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +0 -116
  215. package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +0 -244
  216. package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +0 -195
  217. package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +0 -417
  218. package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +0 -287
  219. package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +0 -160
  220. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +0 -161
  221. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +0 -346
  222. package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +0 -246
  223. package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +0 -175
  224. package/src/widgets/_cell-renderer-bridge.ts +0 -19
  225. package/src/widgets/address-format.ts +0 -120
  226. package/src/widgets/deriveLookupColumns.test.ts +0 -112
  227. package/src/widgets/deriveLookupColumns.ts +0 -108
  228. package/src/widgets/file-size-guard.ts +0 -55
  229. package/src/widgets/file-value.test.ts +0 -230
  230. package/src/widgets/file-value.ts +0 -246
  231. package/src/widgets/nativeDateValue.test.ts +0 -115
  232. package/src/widgets/nativeDateValue.ts +0 -112
  233. package/src/widgets/openNativePicker.ts +0 -18
  234. package/src/widgets/optionWidgets.gatedOptions.test.tsx +0 -267
  235. package/src/widgets/optionWidgets.unconfiguredOptions.test.tsx +0 -150
  236. package/src/widgets/personDisplay.test.ts +0 -113
  237. package/src/widgets/personDisplay.ts +0 -92
  238. package/src/widgets/recentLookups.test.ts +0 -45
  239. package/src/widgets/recentLookups.ts +0 -47
  240. package/src/widgets/toDomProps.ts +0 -204
  241. package/src/widgets/toHostGroupProps.ts +0 -102
  242. package/src/widgets/types.ts +0 -242
  243. package/src/widgets/useCascadingOptions.ts +0 -46
  244. package/src/widgets/useFieldTranslation.ts +0 -132
  245. package/src/widgets/useRecordQuery.test.ts +0 -174
  246. package/src/widgets/useRecordQuery.ts +0 -291
  247. package/src/widgets/useUploadingSignal.test.tsx +0 -62
  248. package/src/widgets/useUploadingSignal.ts +0 -20
  249. package/src/withFieldCarrier.tsx +0 -75
@@ -1,169 +0,0 @@
1
- import React, { useId, useEffect } from 'react';
2
- import { Checkbox, Label, EmptyValue, Badge } from '@object-ui/components';
3
- import type { OptionLike } from '@object-ui/core';
4
- import { FieldWidgetComponentProps } from './types';
5
- import { toDomProps } from './toDomProps';
6
- import { toHostGroupProps } from './toHostGroupProps';
7
- import { OptionsEmptyState } from './OptionsEmptyState';
8
- import { useCascadingOptions } from './useCascadingOptions';
9
-
10
- type Option = OptionLike;
11
-
12
- /**
13
- * CheckboxesField - select zero or more values from a fixed option set, rendered
14
- * as a list of checkboxes. The stored value is a string[]. Used for the
15
- * `checkboxes` field type.
16
- *
17
- * Options support the same per-option `visibleWhen` cascading + `dependsOn`
18
- * gating as `MultiSelectField` (ADR-0058 / #2715), resolved through the shared
19
- * {@link useCascadingOptions} hook: the offered boxes narrow against the live
20
- * form record + `current_user`, the control is gated behind a "select the parent
21
- * first" hint while a dependency is empty, and selections no longer offered
22
- * (parent changed / predicate flipped) are pruned from the array.
23
- */
24
- export function CheckboxesField({
25
- value,
26
- onChange,
27
- field,
28
- readonly,
29
- className,
30
- dependentValues,
31
- dependsOn: dependsOnProp,
32
- emptyHint,
33
- dataSource: _dataSource,
34
- error,
35
- ...props
36
- }: FieldWidgetComponentProps<string[]>) {
37
- const config = field as any;
38
- const rawOptions: Option[] = config?.options || [];
39
- const selected: string[] = Array.isArray(value) ? value : value == null ? [] : [value as unknown as string];
40
- const groupId = useId();
41
- const fieldName = props.name || config?.name || props.id || '';
42
-
43
- const dependsOn = config?.dependsOn ?? dependsOnProp;
44
- const { options, gated, dependsOnFields } = useCascadingOptions<Option>(
45
- rawOptions,
46
- dependsOn,
47
- dependentValues,
48
- );
49
-
50
- // Cascade clear: prune selected values the offered set no longer includes
51
- // (parent changed / predicate flipped), keeping the ones still valid — the
52
- // per-element clear the multi-value case needs (cf. the scalar select/radio).
53
- useEffect(() => {
54
- if (readonly) return;
55
- // Never configured → nothing to prune against; see `MultiSelectField`'s
56
- // copy of this guard for the measured failure (objectui#4220).
57
- if (rawOptions.length === 0) return;
58
- // Gated → the authored list is withheld until the `dependsOn` parent is
59
- // chosen, so the empty offered set is missing information, not a verdict on
60
- // the stored value; clearing here fired on mount (objectui#4247). Same
61
- // reasoning, at length, in `MultiSelectField`.
62
- if (gated) return;
63
- if (selected.length === 0) return;
64
- const stillOffered = selected.filter((v) => options.some((o) => o.value === v));
65
- if (stillOffered.length !== selected.length) onChange(stillOffered);
66
- // eslint-disable-next-line react-hooks/exhaustive-deps
67
- }, [options, gated]);
68
-
69
- // The host's group label has to be consumed on EVERY surface this widget can
70
- // render, not only the editable one (objectui#3990): both branches below
71
- // return before the editable container's `groupDomProps` spread, which is how
72
- // a field-level `readonly: true` and a zero-option list ended up with a
73
- // published label id and no consumer at all. See `toHostGroupProps`.
74
- const hostGroupProps = toHostGroupProps(props);
75
-
76
- if (readonly) {
77
- // The readonly display of a checkbox set is the set of CHECKED labels — a
78
- // set of values, so the same `group` answer as the editable list.
79
- // `EmptyValue` is that surface with nothing in it; its own `aria-label`
80
- // ("No value") is outranked by `aria-labelledby` per accname, and on the
81
- // `generic` role that span carries it was never exposed anyway.
82
- if (selected.length === 0) return <EmptyValue {...hostGroupProps} />;
83
- return (
84
- <div {...hostGroupProps} className="flex flex-wrap gap-1">
85
- {selected.map((v) => {
86
- // Label from the raw set so a stored value hidden by `visibleWhen`
87
- // still renders its label rather than a bare id.
88
- const opt = rawOptions.find((o) => o.value === v);
89
- return <Badge key={v} variant="outline">{opt?.label || v}</Badge>;
90
- })}
91
- </div>
92
- );
93
- }
94
-
95
- // No offered options is unfillable — surface a legible state instead of an
96
- // empty checkbox list: the host's `emptyHint` when it computed one, else this
97
- // widget's own translated copy. Shared with the select / multiselect / radio
98
- // so the four cannot drift again (objectui#3231).
99
- if (options.length === 0) {
100
- return (
101
- <OptionsEmptyState
102
- emptyHint={emptyHint}
103
- gated={gated}
104
- dependsOnFields={dependsOnFields}
105
- testId={fieldName ? `checkboxes-empty-${fieldName}` : undefined}
106
- className="min-h-9"
107
- // This box IS the field in this state, so it is what the host label
108
- // names (objectui#3990).
109
- hostGroupProps={hostGroupProps}
110
- />
111
- );
112
- }
113
-
114
- const toggle = (v: string, checked: boolean) => {
115
- const next = checked ? [...selected.filter((x) => x !== v), v] : selected.filter((x) => x !== v);
116
- onChange(next);
117
- };
118
-
119
- // DOM pass-through (objectui#3318): the container carries the form
120
- // renderer's id / aria-describedby, but NOT its `aria-invalid` — a plain
121
- // wrapper div is not where assistive tech reads the invalid state. That
122
- // state goes onto every focusable checkbox below (`checkbox` is an
123
- // aria-invalid-supporting role), computed from the published `error` slot
124
- // (#3222). The per-item ids below stay authoritative for their labels.
125
- // `name` is withheld too: it is only DOM-legal on form controls, and on
126
- // this div it is exactly the leak #3291 sweeps for.
127
- const {
128
- 'aria-invalid': _hostAriaInvalid,
129
- name: _domName,
130
- ...groupDomProps
131
- } = toDomProps(props);
132
- // When the host associated its visible label with this container by IDREF
133
- // (`aria-labelledby`, objectui#3961) the container IS the labelled group, so it
134
- // answers with the matching role — without one, the name sits on a generic
135
- // `div` and contributes nothing: `label for` pointing here was already inert
136
- // (`HTMLLabelElement.control` is null for a div), which is the whole defect.
137
- // Absent (standalone: the inline grid editor, a bare SDUI node) nothing is
138
- // emitted and the markup is unchanged.
139
- const isLabelledGroup = groupDomProps['aria-labelledby'] != null;
140
-
141
- return (
142
- <div
143
- {...groupDomProps}
144
- role={isLabelledGroup ? 'group' : undefined}
145
- className={className}
146
- data-testid={fieldName ? `checkboxes-${fieldName}` : undefined}
147
- >
148
- {options.map((opt) => {
149
- // Multi-value fields store string arrays — stringify the (possibly
150
- // numeric, #3090-widened) authored value at this boundary.
151
- const value = String(opt.value);
152
- const id = `${groupId}-${value}`;
153
- return (
154
- <div key={value} className="flex items-center space-x-2 py-0.5">
155
- <Checkbox
156
- id={id}
157
- checked={selected.includes(value)}
158
- onCheckedChange={(checked) => toggle(value, !!checked)}
159
- disabled={props.disabled}
160
- aria-invalid={!!error}
161
- data-testid={`checkboxes-option-${value}`}
162
- />
163
- <Label htmlFor={id} className="font-normal">{opt.label}</Label>
164
- </div>
165
- );
166
- })}
167
- </div>
168
- );
169
- }
@@ -1,41 +0,0 @@
1
- import React from 'react';
2
- import { Textarea, cn, EmptyValue } from '@object-ui/components';
3
- import { FieldWidgetComponentProps } from './types';
4
- import { toDomProps } from './toDomProps';
5
-
6
- /**
7
- * Code field widget - provides a code editor with syntax highlighting
8
- * Uses a simple textarea with monospace font
9
- * For advanced code editing, use the @object-ui/plugin-editor component
10
- */
11
- export function CodeField({ value, onChange, field, readonly, error, ...props }: FieldWidgetComponentProps<string>) {
12
- const config = field;
13
- // Get code-specific configuration from field metadata
14
- const language = (config as any)?.language ?? 'javascript';
15
-
16
- if (readonly) {
17
- return (
18
- <pre className={cn("text-sm bg-muted p-2 rounded overflow-x-auto border", props.className)}>
19
- <code>{value || <EmptyValue />}</code>
20
- </pre>
21
- );
22
- }
23
-
24
- return (
25
- <Textarea
26
- // DOM pass-through onto the real focusable control (objectui#3318).
27
- {...toDomProps(props)}
28
- value={value || ''}
29
- onChange={(e) => onChange(e.target.value)}
30
- placeholder={config?.placeholder || `// Write ${language} code here...`}
31
- disabled={readonly || props.disabled}
32
- className={cn("font-mono text-sm", props.className)}
33
- rows={12}
34
- spellCheck={false}
35
- // AFTER the spread so this widget's own computation wins: `error` is
36
- // the published validation slot (#3222), `!!undefined` → explicit
37
- // "false".
38
- aria-invalid={!!error}
39
- />
40
- );
41
- }
@@ -1,53 +0,0 @@
1
- import React from 'react';
2
- import { Input, EmptyValue } from '@object-ui/components';
3
- import { FieldWidgetComponentProps } from './types';
4
- import { toDomProps } from './toDomProps';
5
-
6
- /**
7
- * Color field widget - provides a color picker input
8
- * Supports hex color values (e.g., #ff0000)
9
- */
10
- export function ColorField({ value, onChange, field, readonly, error, ...props }: FieldWidgetComponentProps<string>) {
11
- const colorField = field as any;
12
-
13
- if (readonly) {
14
- return (
15
- <div className="flex items-center gap-2">
16
- <div
17
- className="w-6 h-6 rounded border border-input"
18
- style={{ backgroundColor: value || '#000000' }}
19
- />
20
- <span className="text-sm">{value || <EmptyValue />}</span>
21
- </div>
22
- );
23
- }
24
-
25
- return (
26
- <div className="flex items-center gap-2">
27
- <input
28
- type="color"
29
- value={value || '#000000'}
30
- onChange={(e) => onChange(e.target.value)}
31
- disabled={readonly || props.disabled}
32
- className="w-10 h-10 rounded border border-input cursor-pointer"
33
- // Both focusable halves of this widget announce the same validation
34
- // state (objectui#3318).
35
- aria-invalid={!!error}
36
- />
37
- <Input
38
- // DOM pass-through onto the primary text control (objectui#3318): the
39
- // whitelist spread carries the form renderer's id / aria-describedby.
40
- {...toDomProps(props)}
41
- type="text"
42
- value={value || ''}
43
- onChange={(e) => onChange(e.target.value)}
44
- placeholder={colorField?.placeholder || '#000000'}
45
- disabled={readonly || props.disabled}
46
- className={props.className}
47
- pattern="^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"
48
- // AFTER the spread so this widget's own computation wins (#3222).
49
- aria-invalid={!!error}
50
- />
51
- </div>
52
- );
53
- }
@@ -1,135 +0,0 @@
1
- import React from 'react';
2
- import { Input, EmptyValue } from '@object-ui/components';
3
- import { FieldWidgetComponentProps } from './types';
4
- import { toDomProps } from './toDomProps';
5
- import { useLocalization, useDisplayLocale, formatDisplayNumber } from '@object-ui/i18n';
6
- import { resolveFieldCurrency, currencyFractionDigits, currencySymbol } from '../currency';
7
-
8
- /**
9
- * Format currency value for display. When `currency` is undefined the value
10
- * is rendered as a plain number with thousands separators (no symbol),
11
- * because silently assuming USD is misleading for non-USD businesses.
12
- *
13
- * `precision` is a display width the caller resolved — either the field's
14
- * authored `precision` or, when it declared none, the currency's own ISO 4217
15
- * minor-unit count (see the derivation at the call site, objectui#4361).
16
- */
17
- function formatAmount(
18
- value: number,
19
- currency: string | undefined,
20
- precision: number,
21
- locale?: string,
22
- ): string {
23
- if (currency) {
24
- try {
25
- return formatDisplayNumber(value, {
26
- locale,
27
- currency,
28
- minimumFractionDigits: precision,
29
- maximumFractionDigits: precision,
30
- });
31
- } catch {
32
- return `${currency} ${value.toFixed(precision)}`;
33
- }
34
- }
35
- try {
36
- // No `scale` passed: this is a currency widget whose `precision` is a
37
- // display width, so a `precision: 0` amount keeps its separators rather
38
- // than being read as an ordinal (objectui#4033).
39
- return formatDisplayNumber(value, {
40
- locale,
41
- minimumFractionDigits: precision,
42
- maximumFractionDigits: precision,
43
- });
44
- } catch {
45
- return value.toFixed(precision);
46
- }
47
- }
48
-
49
- export function CurrencyField({ value, onChange, field, readonly, error, className, ...props }: FieldWidgetComponentProps<number>) {
50
- const currencyField = field as any;
51
- // Shared precedence: field currency → currencyConfig → tenant default (ADR-0053).
52
- const { currency: tenantCurrency } = useLocalization();
53
- const locale = useDisplayLocale();
54
- const currency: string | undefined = resolveFieldCurrency(currencyField, tenantCurrency);
55
- // An AUTHORED `precision` wins: it is authored metadata, and this repo's
56
- // convention is that authored metadata keeps priority. Whether a declared
57
- // `precision` that contradicts the currency's ISO 4217 digits (say
58
- // `precision: 2` on a JPY field) should be REJECTED at publish time is a
59
- // contract question, so it is filed upstream in `@objectstack/spec` rather
60
- // than answered here by overriding the author (objectui#4361).
61
- //
62
- // An ABSENT `precision` derives from the currency instead of defaulting to a
63
- // literal 2, which rendered `¥1,234.50` for a currency with no minor unit.
64
- // "Absent" is genuinely distinguishable from "authored 2" here — MEASURED,
65
- // not assumed: `CurrencyFieldMetadata.precision` is optional in
66
- // `@object-ui/types` and `z.ZodOptional<z.ZodNumber>` (no `.default()`) in
67
- // `@objectstack/spec`, so a parsed field carries no materialized 2. The only
68
- // `.default(2)` on the currency surface is `CurrencyConfigSchema.precision`,
69
- // a different key on the `currencyConfig` block that this widget never reads.
70
- //
71
- // This is the widget's ONE precision, so the derivation also reaches the edit
72
- // affordances below. Deliberate: leaving `step`/blur-rounding at 2 would give
73
- // a JPY field that displays whole yen while offering a 0.01 spinner step and
74
- // rounding typed input to 1234.56 yen.
75
- const precision =
76
- currencyField?.precision ?? (currency ? currencyFractionDigits(currency) : 2);
77
-
78
- if (readonly) {
79
- if (value == null) return <EmptyValue />;
80
- return (
81
- <span className="text-sm font-medium tabular-nums">
82
- {formatAmount(Number(value), currency, precision, locale)}
83
- </span>
84
- );
85
- }
86
-
87
- // Parse and format on blur to ensure valid currency format
88
- const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
89
- const val = parseFloat(e.target.value);
90
- if (!isNaN(val)) {
91
- onChange(parseFloat(val.toFixed(precision)));
92
- }
93
- };
94
-
95
- // ONE channel for the symbol (objectui#4414). This used to be a hand-written
96
- // `currency === 'USD' ? '$' : currency` ternary, with a dead one-entry
97
- // `CURRENCY_SYMBOLS` map sitting two lines above it restating the same fact —
98
- // a table that looked like the place to add a currency but that nothing read.
99
- // Both were hand copies of knowledge `Intl` already carries, and both are
100
- // gone: `currencySymbol` reads the `currency` part of the SAME format the
101
- // readonly branch above renders amounts with, so the two modes of this widget
102
- // can no longer disagree (they did: `JPY` in the adornment, `¥1,235` in the
103
- // readonly span). USD at the display-locale default is `$` either way.
104
- const symbol = currency ? currencySymbol(currency, locale) : '';
105
-
106
- return (
107
- <div className="relative">
108
- {symbol && (
109
- <span className="absolute left-3 top-1/2 -translate-y-1/2 text-sm text-gray-500">
110
- {symbol}
111
- </span>
112
- )}
113
- <Input
114
- {...toDomProps(props)}
115
- type="number"
116
- value={value ?? ''}
117
- onChange={(e) => {
118
- const val = e.target.value === '' ? null : parseFloat(e.target.value);
119
- onChange(val as any);
120
- }}
121
- onBlur={handleBlur}
122
- placeholder={currencyField?.placeholder || '0.00'}
123
- disabled={readonly || props.disabled}
124
- className={`${symbol ? 'pl-8' : ''} ${className || ''}`}
125
- // Surface the field's declared range (e.g. `min: 0` on a budget) so the
126
- // browser's spinner/keyboard affordances respect it (objectui#2572);
127
- // server-side validation still owns enforcement.
128
- min={typeof currencyField?.min === 'number' ? currencyField.min : undefined}
129
- max={typeof currencyField?.max === 'number' ? currencyField.max : undefined}
130
- step={Math.pow(10, -precision).toFixed(precision)}
131
- aria-invalid={!!error}
132
- />
133
- </div>
134
- );
135
- }
@@ -1,40 +0,0 @@
1
- import React from 'react';
2
- import { Input, EmptyValue } from '@object-ui/components';
3
- import { useDisplayLocale } from '@object-ui/i18n';
4
- import { FieldWidgetComponentProps } from './types';
5
- import { toDomProps } from './toDomProps';
6
- import { openNativePicker } from './openNativePicker';
7
- import { toDateInputValue } from './nativeDateValue';
8
-
9
- /**
10
- * DateField - Date picker input widget
11
- * Uses native date input and displays locale-formatted date in readonly mode
12
- */
13
- export function DateField({ value, onChange, field, readonly, ...props }: FieldWidgetComponentProps<string>) {
14
- // Before the readonly early return: the hook count must not depend on a prop
15
- // (objectui#4468). A bare `toLocaleDateString()` reads the MACHINE's locale,
16
- // which is how a Chinese form ended up with an `8/11/2026` value in it.
17
- const locale = useDisplayLocale();
18
- if (readonly) {
19
- return value ? <span className="text-sm">{new Date(value).toLocaleDateString(locale)}</span> : <EmptyValue />;
20
- }
21
-
22
- const domProps = toDomProps(props);
23
-
24
- return (
25
- <Input
26
- {...domProps}
27
- type="date"
28
- // An API that hands back `2026-06-17T00:00:00.000Z` for a `date` field
29
- // would leave this control empty too (objectui#3127). The written-back
30
- // shape is unchanged: the control's own plain `YYYY-MM-DD`.
31
- value={toDateInputValue(value)}
32
- onChange={(e) => onChange(e.target.value)}
33
- onClick={(e) => {
34
- openNativePicker(e.currentTarget);
35
- domProps.onClick?.(e);
36
- }}
37
- disabled={readonly || domProps.disabled}
38
- />
39
- );
40
- }
@@ -1,46 +0,0 @@
1
- import React from 'react';
2
- import { Input, EmptyValue } from '@object-ui/components';
3
- import { useDisplayLocale } from '@object-ui/i18n';
4
- import { FieldWidgetComponentProps } from './types';
5
- import { toDomProps } from './toDomProps';
6
- import { openNativePicker } from './openNativePicker';
7
- import { toDateTimeInputValue, fromDateTimeInputValue } from './nativeDateValue';
8
-
9
- /**
10
- * DateTimeField - Combined date and time picker widget
11
- * Displays both date and time in locale format when readonly
12
- */
13
- export function DateTimeField({ value, onChange, field, readonly, ...props }: FieldWidgetComponentProps<string>) {
14
- // Before the readonly early return — the hook count must not depend on a
15
- // prop. See DateField for why the bare `toLocale*` calls were wrong
16
- // (objectui#4468).
17
- const locale = useDisplayLocale();
18
- if (readonly) {
19
- if (!value) return <EmptyValue />;
20
- const date = new Date(value);
21
- return (
22
- <span className="text-sm">
23
- {date.toLocaleDateString(locale)} {date.toLocaleTimeString(locale)}
24
- </span>
25
- );
26
- }
27
-
28
- const domProps = toDomProps(props);
29
-
30
- return (
31
- <Input
32
- {...domProps}
33
- type="datetime-local"
34
- // The record's value is ISO-8601 (`…T14:30:00.000Z`), which this control
35
- // rejects outright — it renders empty and the user reads that as a lost
36
- // value (objectui#3127). Convert in, and back out on the same basis.
37
- value={toDateTimeInputValue(value)}
38
- onChange={(e) => onChange(fromDateTimeInputValue(e.target.value))}
39
- onClick={(e) => {
40
- openNativePicker(e.currentTarget);
41
- domProps.onClick?.(e);
42
- }}
43
- disabled={readonly || domProps.disabled}
44
- />
45
- );
46
- }
@@ -1,37 +0,0 @@
1
- import React from 'react';
2
- import { Input, EmptyValue } from '@object-ui/components';
3
- import { FieldWidgetComponentProps } from './types';
4
- import { toDomProps } from './toDomProps';
5
-
6
- /**
7
- * EmailField - Email address input with mailto link in readonly mode
8
- * Renders as a clickable mailto link when readonly, standard email input otherwise
9
- */
10
- export function EmailField({ value, onChange, field, readonly, error, ...props }: FieldWidgetComponentProps<string>) {
11
- const config = field;
12
- if (readonly) {
13
- if (!value) return <EmptyValue />;
14
- return (
15
- <a
16
- href={`mailto:${value}`}
17
- className="text-sm text-blue-600 hover:text-blue-800 hover:underline"
18
- >
19
- {value}
20
- </a>
21
- );
22
- }
23
-
24
- const domProps = toDomProps(props);
25
-
26
- return (
27
- <Input
28
- {...domProps}
29
- type="email"
30
- value={value || ''}
31
- onChange={(e) => onChange(e.target.value)}
32
- placeholder={config?.placeholder || 'email@example.com'}
33
- disabled={readonly || domProps.disabled}
34
- aria-invalid={!!error}
35
- />
36
- );
37
- }