@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.
- package/CHANGELOG.md +6150 -0
- package/README.md +108 -8
- package/dist/FieldEditWidget.d.ts +70 -8
- package/dist/coerceToSafeValue.d.ts +54 -0
- package/dist/currency.d.ts +1 -1
- package/dist/field-type-alias.d.ts +16 -0
- package/dist/index.cjs +5 -5
- package/dist/index.css +53 -11
- package/dist/index.d.ts +653 -128
- package/dist/index.js +4275 -3171
- package/dist/widgets/AddressField.d.ts +3 -3
- package/dist/widgets/AutoNumberField.d.ts +1 -1
- package/dist/widgets/AvatarField.d.ts +20 -4
- package/dist/widgets/BooleanField.d.ts +2 -2
- package/dist/widgets/CapabilityMultiSelectField.d.ts +1 -1
- 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/DateField.d.ts +2 -2
- package/dist/widgets/DateTimeField.d.ts +2 -2
- package/dist/widgets/EmailField.d.ts +1 -1
- package/dist/widgets/FileField.d.ts +18 -2
- package/dist/widgets/FilterConditionField.d.ts +43 -2
- package/dist/widgets/FormulaField.d.ts +1 -1
- package/dist/widgets/FullscreenFieldEditor.d.ts +9 -2
- package/dist/widgets/GeolocationField.d.ts +10 -4
- package/dist/widgets/GridField.d.ts +102 -86
- package/dist/widgets/ImageField.d.ts +1 -1
- package/dist/widgets/LocationField.d.ts +31 -4
- package/dist/widgets/LookupField.d.ts +9 -1
- package/dist/widgets/MasterDetailField.d.ts +1 -1
- package/dist/widgets/MultiSelectField.d.ts +1 -1
- package/dist/widgets/NumberField.d.ts +2 -2
- package/dist/widgets/ObjectField.d.ts +10 -3
- package/dist/widgets/ObjectRefField.d.ts +1 -1
- package/dist/widgets/OptionsEmptyState.d.ts +38 -8
- package/dist/widgets/PasswordField.d.ts +15 -1
- package/dist/widgets/PeoplePicker.d.ts +17 -2
- package/dist/widgets/PercentField.d.ts +3 -2
- package/dist/widgets/PersonRow.d.ts +13 -2
- 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 +44 -1
- package/dist/widgets/RecordPickerDialog.d.ts +33 -16
- package/dist/widgets/RichTextField.d.ts +37 -1
- package/dist/widgets/SelectField.d.ts +1 -1
- package/dist/widgets/SelectionTray.d.ts +13 -2
- package/dist/widgets/SignatureField.d.ts +32 -2
- package/dist/widgets/SliderField.d.ts +37 -2
- package/dist/widgets/SummaryField.d.ts +1 -1
- package/dist/widgets/TagsField.d.ts +1 -1
- package/dist/widgets/TextAreaField.d.ts +6 -2
- package/dist/widgets/TextField.d.ts +2 -2
- package/dist/widgets/TimeField.d.ts +2 -2
- package/dist/widgets/UrlField.d.ts +1 -1
- package/dist/widgets/UserField.d.ts +1 -1
- package/dist/widgets/VectorField.d.ts +1 -1
- package/dist/widgets/_cell-renderer-bridge.d.ts +1 -1
- package/dist/widgets/address-format.d.ts +27 -7
- package/dist/widgets/file-affordance.d.ts +33 -0
- package/dist/widgets/file-size-guard.d.ts +5 -23
- package/dist/widgets/file-value.d.ts +38 -9
- package/dist/widgets/lookupColumnDisplay.d.ts +110 -0
- package/dist/widgets/nativeDateValue.d.ts +9 -27
- package/dist/widgets/number-format.d.ts +137 -0
- package/dist/widgets/numberBadInput.d.ts +135 -0
- package/dist/widgets/personDisplay.d.ts +22 -4
- package/dist/widgets/richTextDisplay.d.ts +116 -0
- package/dist/widgets/toDomProps.d.ts +10 -1
- package/dist/widgets/toHostGroupProps.d.ts +172 -21
- package/dist/widgets/toHostProps.d.ts +105 -0
- package/dist/widgets/types.d.ts +19 -6
- package/dist/widgets/upload-error-message.d.ts +6 -0
- package/dist/widgets/uploadingScope.d.ts +142 -0
- package/dist/widgets/useCascadingOptions.d.ts +3 -2
- package/dist/widgets/useRecordQuery.d.ts +4 -2
- package/dist/widgets/useUploadingSignal.d.ts +8 -0
- package/package.json +11 -13
- package/dist/widgets/CharacterCount.d.ts +0 -56
- package/src/FieldEditWidget.test.ts +0 -133
- package/src/FieldEditWidget.tsx +0 -207
- package/src/__tests__/AddressCellRenderer.test.tsx +0 -166
- package/src/__tests__/CurrencyField.minorUnits.test.tsx +0 -191
- package/src/__tests__/CurrencyField.symbol.test.tsx +0 -198
- package/src/__tests__/DateCellRenderer.test.tsx +0 -82
- package/src/__tests__/NumberCellRenderer.grouping.test.tsx +0 -146
- package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
- package/src/__tests__/PercentCellRenderer.locale.test.tsx +0 -152
- package/src/__tests__/PercentCellRenderer.test.tsx +0 -115
- package/src/__tests__/boolean-label-association-e2e.test.tsx +0 -298
- package/src/__tests__/capability-multiselect-retired.test.ts +0 -109
- package/src/__tests__/cell-truncation.test.tsx +0 -96
- package/src/__tests__/composite-group-label-e2e.test.tsx +0 -436
- package/src/__tests__/composite-group-label-readonly-e2e.test.tsx +0 -403
- package/src/__tests__/date-formatter-residue-4272.test.ts +0 -120
- package/src/__tests__/date-locale-channel.test.tsx +0 -305
- package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
- package/src/__tests__/formatCurrency.centsDigit.test.tsx +0 -158
- package/src/__tests__/formatCurrency.minorUnits.test.tsx +0 -221
- package/src/__tests__/group-labelling-declaration.test.ts +0 -148
- package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
- package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
- package/src/__tests__/lookupCellRecordLink.test.tsx +0 -171
- package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
- package/src/__tests__/percent-cell-vs-measure-4576.test.ts +0 -115
- package/src/__tests__/percent-formatter-locale-4553.test.ts +0 -147
- package/src/__tests__/percent-tie-halfup-4590.test.ts +0 -224
- package/src/__tests__/registry-collision.test.ts +0 -91
- package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
- package/src/__tests__/select-label-association-e2e.test.tsx +0 -199
- package/src/__tests__/spec-symbol-batch7.test.ts +0 -126
- package/src/__tests__/validation-feedback.test.tsx +0 -361
- package/src/__tests__/widget-aria-invalid-e2e.test.tsx +0 -265
- package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +0 -338
- package/src/__tests__/widget-dom-leak-e2e.test.tsx +0 -357
- package/src/__tests__/widget-props-contract.test.tsx +0 -142
- package/src/coerce-safe-value.test.ts +0 -34
- package/src/complex-widgets.test.tsx +0 -924
- package/src/currency.ts +0 -144
- package/src/datetime-widgets.test.tsx +0 -185
- package/src/field-type-alias.multiple.test.ts +0 -81
- package/src/field-type-alias.ts +0 -156
- package/src/field-type-coverage.test.ts +0 -67
- package/src/index.css +0 -44
- package/src/index.tsx +0 -2745
- package/src/standard-widgets.test.tsx +0 -364
- package/src/widgets/AddressField.postalCode.test.tsx +0 -257
- package/src/widgets/AddressField.tsx +0 -182
- package/src/widgets/AddressField.uniqueIds.test.tsx +0 -93
- package/src/widgets/AutoNumberField.tsx +0 -18
- package/src/widgets/AvatarField.tsx +0 -117
- package/src/widgets/BooleanField.tsx +0 -86
- package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
- package/src/widgets/CapabilityMultiSelectField.tsx +0 -241
- package/src/widgets/CharacterCount.tsx +0 -263
- package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
- package/src/widgets/CheckboxesField.tsx +0 -169
- package/src/widgets/CodeField.tsx +0 -41
- package/src/widgets/ColorField.tsx +0 -53
- package/src/widgets/CurrencyField.tsx +0 -135
- package/src/widgets/DateField.tsx +0 -40
- package/src/widgets/DateTimeField.tsx +0 -46
- package/src/widgets/EmailField.tsx +0 -37
- package/src/widgets/FileField.tsx +0 -512
- package/src/widgets/FilterConditionField.tsx +0 -454
- package/src/widgets/FormulaField.tsx +0 -38
- package/src/widgets/FullscreenFieldEditor.tsx +0 -60
- package/src/widgets/GeolocationField.tsx +0 -193
- package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
- package/src/widgets/GridField.test.tsx +0 -632
- package/src/widgets/GridField.tsx +0 -1070
- package/src/widgets/ImageCropperDialog.tsx +0 -245
- package/src/widgets/ImageField.maxSize.test.tsx +0 -154
- package/src/widgets/ImageField.tsx +0 -305
- package/src/widgets/ImageLightbox.test.tsx +0 -52
- package/src/widgets/ImageLightbox.tsx +0 -108
- package/src/widgets/LocationField.tsx +0 -57
- package/src/widgets/LookupField.dependsOn.test.tsx +0 -233
- package/src/widgets/LookupField.gateHintLabel.test.tsx +0 -135
- package/src/widgets/LookupField.hydration.test.tsx +0 -136
- package/src/widgets/LookupField.idField.test.tsx +0 -61
- package/src/widgets/LookupField.tsx +0 -1295
- package/src/widgets/MarkdownContent.tsx +0 -19
- package/src/widgets/MasterDetailField.tsx +0 -131
- package/src/widgets/MultiSelectField.cascade.test.tsx +0 -143
- package/src/widgets/MultiSelectField.tsx +0 -196
- package/src/widgets/NumberField.tsx +0 -49
- package/src/widgets/ObjectField.tsx +0 -94
- package/src/widgets/ObjectRefField.tsx +0 -121
- package/src/widgets/OptionsEmptyState.no-provider.test.tsx +0 -88
- package/src/widgets/OptionsEmptyState.test.tsx +0 -169
- package/src/widgets/OptionsEmptyState.tsx +0 -99
- package/src/widgets/PasswordField.tsx +0 -49
- package/src/widgets/PeoplePicker.test.tsx +0 -360
- package/src/widgets/PeoplePicker.tsx +0 -579
- package/src/widgets/PeoplePickerComponents.test.tsx +0 -104
- package/src/widgets/PercentField.tsx +0 -95
- package/src/widgets/PersonRow.tsx +0 -117
- package/src/widgets/PhoneField.tsx +0 -37
- package/src/widgets/QRCodeField.tsx +0 -103
- package/src/widgets/RadioField.cascade.test.tsx +0 -141
- package/src/widgets/RadioField.tsx +0 -143
- package/src/widgets/RatingField.tsx +0 -94
- package/src/widgets/RecipientPickerField.test.tsx +0 -157
- package/src/widgets/RecipientPickerField.tsx +0 -188
- package/src/widgets/RecordPickerDialog.baseFilterShapes.test.tsx +0 -191
- package/src/widgets/RecordPickerDialog.dateLocale.test.tsx +0 -124
- package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
- package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
- package/src/widgets/RecordPickerDialog.tsx +0 -1365
- package/src/widgets/RichTextField.tsx +0 -176
- package/src/widgets/SelectField.cascade.test.tsx +0 -138
- package/src/widgets/SelectField.tsx +0 -193
- package/src/widgets/SelectionTray.tsx +0 -110
- package/src/widgets/SignatureField.tsx +0 -140
- package/src/widgets/SliderField.tsx +0 -39
- package/src/widgets/SummaryField.tsx +0 -31
- package/src/widgets/TagsField.placeholder.no-provider.test.tsx +0 -68
- package/src/widgets/TagsField.placeholder.test.tsx +0 -95
- package/src/widgets/TagsField.tsx +0 -88
- package/src/widgets/TextAreaField.tsx +0 -213
- package/src/widgets/TextField.tsx +0 -45
- package/src/widgets/TimeField.tsx +0 -31
- package/src/widgets/UrlField.tsx +0 -46
- package/src/widgets/UserField.tsx +0 -59
- package/src/widgets/UserFieldPicker.test.tsx +0 -78
- package/src/widgets/VectorField.tsx +0 -29
- package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +0 -66
- package/src/widgets/__tests__/FilterConditionField.operators.test.ts +0 -166
- package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +0 -248
- package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +0 -116
- package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +0 -244
- package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +0 -195
- package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +0 -417
- package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +0 -287
- package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +0 -160
- package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +0 -161
- package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +0 -346
- package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +0 -246
- package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +0 -175
- package/src/widgets/_cell-renderer-bridge.ts +0 -19
- package/src/widgets/address-format.ts +0 -120
- package/src/widgets/deriveLookupColumns.test.ts +0 -112
- package/src/widgets/deriveLookupColumns.ts +0 -108
- package/src/widgets/file-size-guard.ts +0 -55
- package/src/widgets/file-value.test.ts +0 -230
- package/src/widgets/file-value.ts +0 -246
- package/src/widgets/nativeDateValue.test.ts +0 -115
- package/src/widgets/nativeDateValue.ts +0 -112
- package/src/widgets/openNativePicker.ts +0 -18
- package/src/widgets/optionWidgets.gatedOptions.test.tsx +0 -267
- package/src/widgets/optionWidgets.unconfiguredOptions.test.tsx +0 -150
- package/src/widgets/personDisplay.test.ts +0 -113
- package/src/widgets/personDisplay.ts +0 -92
- package/src/widgets/recentLookups.test.ts +0 -45
- package/src/widgets/recentLookups.ts +0 -47
- package/src/widgets/toDomProps.ts +0 -204
- package/src/widgets/toHostGroupProps.ts +0 -102
- package/src/widgets/types.ts +0 -242
- package/src/widgets/useCascadingOptions.ts +0 -46
- package/src/widgets/useFieldTranslation.ts +0 -132
- package/src/widgets/useRecordQuery.test.ts +0 -174
- package/src/widgets/useRecordQuery.ts +0 -291
- package/src/widgets/useUploadingSignal.test.tsx +0 -62
- package/src/widgets/useUploadingSignal.ts +0 -20
- 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
|
-
}
|