@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.
- package/CHANGELOG.md +1533 -0
- package/README.md +1 -12
- package/dist/FieldEditWidget.d.ts +7 -1
- package/dist/currency.d.ts +46 -0
- package/dist/field-type-alias.d.ts +19 -1
- package/dist/index.cjs +5 -5
- package/dist/index.css +633 -0
- package/dist/index.d.ts +52 -12
- package/dist/index.js +5167 -4759
- package/dist/widgets/AddressField.d.ts +4 -12
- package/dist/widgets/AvatarField.d.ts +1 -1
- package/dist/widgets/CharacterCount.d.ts +56 -0
- package/dist/widgets/CheckboxesField.d.ts +1 -1
- package/dist/widgets/CodeField.d.ts +1 -1
- package/dist/widgets/ColorField.d.ts +1 -1
- package/dist/widgets/CurrencyField.d.ts +1 -1
- package/dist/widgets/EmailField.d.ts +1 -1
- package/dist/widgets/FileField.d.ts +1 -1
- package/dist/widgets/FilterConditionField.d.ts +1 -1
- package/dist/widgets/FullscreenFieldEditor.d.ts +45 -0
- package/dist/widgets/GeolocationField.d.ts +1 -1
- package/dist/widgets/GridField.d.ts +12 -2
- package/dist/widgets/ImageField.d.ts +1 -1
- package/dist/widgets/LocationField.d.ts +1 -1
- package/dist/widgets/LookupField.d.ts +1 -1
- package/dist/widgets/MultiSelectField.d.ts +1 -1
- package/dist/widgets/ObjectField.d.ts +1 -1
- package/dist/widgets/ObjectRefField.d.ts +1 -1
- package/dist/widgets/OptionsEmptyState.d.ts +59 -0
- package/dist/widgets/PercentField.d.ts +1 -1
- package/dist/widgets/PhoneField.d.ts +1 -1
- package/dist/widgets/QRCodeField.d.ts +1 -1
- package/dist/widgets/RadioField.d.ts +1 -1
- package/dist/widgets/RatingField.d.ts +1 -1
- package/dist/widgets/RecipientPickerField.d.ts +1 -1
- package/dist/widgets/RecordPickerDialog.d.ts +49 -7
- package/dist/widgets/RichTextField.d.ts +19 -1
- package/dist/widgets/SelectField.d.ts +25 -5
- package/dist/widgets/TagsField.d.ts +1 -1
- package/dist/widgets/TextAreaField.d.ts +44 -5
- package/dist/widgets/UrlField.d.ts +1 -1
- package/dist/widgets/address-format.d.ts +68 -0
- package/dist/widgets/file-size-guard.d.ts +41 -0
- package/dist/widgets/nativeDateValue.d.ts +37 -0
- package/dist/widgets/toDomProps.d.ts +111 -0
- package/dist/widgets/toHostGroupProps.d.ts +91 -0
- package/dist/widgets/types.d.ts +128 -33
- package/dist/widgets/useRecordQuery.d.ts +7 -1
- package/dist/withFieldCarrier.d.ts +56 -0
- package/package.json +20 -16
- package/src/FieldEditWidget.tsx +17 -1
- package/src/__tests__/AddressCellRenderer.test.tsx +166 -0
- package/src/__tests__/CurrencyField.minorUnits.test.tsx +191 -0
- package/src/__tests__/CurrencyField.symbol.test.tsx +198 -0
- package/src/__tests__/DateCellRenderer.test.tsx +9 -0
- package/src/__tests__/NumberCellRenderer.grouping.test.tsx +146 -0
- package/src/__tests__/PercentCellRenderer.locale.test.tsx +152 -0
- package/src/__tests__/PercentCellRenderer.test.tsx +115 -0
- package/src/__tests__/boolean-label-association-e2e.test.tsx +298 -0
- package/src/__tests__/capability-multiselect-retired.test.ts +109 -0
- package/src/__tests__/cell-truncation.test.tsx +96 -0
- package/src/__tests__/composite-group-label-e2e.test.tsx +436 -0
- package/src/__tests__/composite-group-label-readonly-e2e.test.tsx +403 -0
- package/src/__tests__/date-formatter-residue-4272.test.ts +120 -0
- package/src/__tests__/date-locale-channel.test.tsx +305 -0
- package/src/__tests__/field-carrier-sdui.test.tsx +169 -0
- package/src/__tests__/formatCurrency.centsDigit.test.tsx +158 -0
- package/src/__tests__/formatCurrency.minorUnits.test.tsx +221 -0
- package/src/__tests__/group-labelling-declaration.test.ts +148 -0
- package/src/__tests__/lookupCellRecordLink.test.tsx +171 -0
- package/src/__tests__/percent-cell-vs-measure-4576.test.ts +115 -0
- package/src/__tests__/percent-formatter-locale-4553.test.ts +147 -0
- package/src/__tests__/percent-tie-halfup-4590.test.ts +224 -0
- package/src/__tests__/select-label-association-e2e.test.tsx +199 -0
- package/src/__tests__/spec-symbol-batch7.test.ts +29 -15
- package/src/__tests__/validation-feedback.test.tsx +22 -13
- package/src/__tests__/widget-aria-invalid-e2e.test.tsx +265 -0
- package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +338 -0
- package/src/__tests__/widget-dom-leak-e2e.test.tsx +357 -0
- package/src/__tests__/widget-props-contract.test.tsx +22 -9
- package/src/complex-widgets.test.tsx +22 -6
- package/src/currency.ts +130 -0
- package/src/datetime-widgets.test.tsx +94 -2
- package/src/field-type-alias.multiple.test.ts +81 -0
- package/src/field-type-alias.ts +58 -2
- package/src/index.css +44 -0
- package/src/index.tsx +558 -268
- package/src/standard-widgets.test.tsx +8 -1
- package/src/widgets/AddressField.postalCode.test.tsx +257 -0
- package/src/widgets/AddressField.tsx +107 -38
- package/src/widgets/AddressField.uniqueIds.test.tsx +93 -0
- package/src/widgets/AvatarField.tsx +9 -2
- package/src/widgets/BooleanField.tsx +44 -10
- package/src/widgets/CapabilityMultiSelectField.tsx +17 -5
- package/src/widgets/CharacterCount.tsx +263 -0
- package/src/widgets/CheckboxesField.tsx +66 -16
- package/src/widgets/CodeField.tsx +9 -2
- package/src/widgets/ColorField.tsx +11 -2
- package/src/widgets/CurrencyField.tsx +60 -19
- package/src/widgets/DateField.tsx +13 -4
- package/src/widgets/DateTimeField.tsx +14 -5
- package/src/widgets/EmailField.tsx +5 -5
- package/src/widgets/FileField.tsx +39 -11
- package/src/widgets/FilterConditionField.tsx +11 -0
- package/src/widgets/FormulaField.tsx +7 -2
- package/src/widgets/FullscreenFieldEditor.tsx +60 -0
- package/src/widgets/GeolocationField.tsx +49 -8
- package/src/widgets/GeolocationField.uniqueIds.test.tsx +88 -0
- package/src/widgets/GridField.test.tsx +226 -0
- package/src/widgets/GridField.tsx +115 -11
- package/src/widgets/ImageField.maxSize.test.tsx +154 -0
- package/src/widgets/ImageField.tsx +63 -6
- package/src/widgets/LocationField.tsx +9 -2
- package/src/widgets/LookupField.gateHintLabel.test.tsx +135 -0
- package/src/widgets/LookupField.hydration.test.tsx +1 -1
- package/src/widgets/LookupField.tsx +49 -10
- package/src/widgets/MasterDetailField.tsx +1 -1
- package/src/widgets/MultiSelectField.tsx +86 -16
- package/src/widgets/NumberField.tsx +3 -3
- package/src/widgets/ObjectField.tsx +17 -8
- package/src/widgets/ObjectRefField.tsx +10 -0
- package/src/widgets/OptionsEmptyState.no-provider.test.tsx +88 -0
- package/src/widgets/OptionsEmptyState.test.tsx +169 -0
- package/src/widgets/OptionsEmptyState.tsx +99 -0
- package/src/widgets/PasswordField.tsx +3 -3
- package/src/widgets/PeoplePicker.tsx +7 -1
- package/src/widgets/PercentField.tsx +5 -4
- package/src/widgets/PhoneField.tsx +5 -5
- package/src/widgets/QRCodeField.tsx +9 -2
- package/src/widgets/RadioField.tsx +58 -16
- package/src/widgets/RatingField.tsx +32 -4
- package/src/widgets/RecipientPickerField.tsx +18 -0
- package/src/widgets/RecordPickerDialog.baseFilterShapes.test.tsx +191 -0
- package/src/widgets/RecordPickerDialog.dateLocale.test.tsx +124 -0
- package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +611 -0
- package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +112 -0
- package/src/widgets/RecordPickerDialog.tsx +235 -21
- package/src/widgets/RichTextField.tsx +151 -24
- package/src/widgets/SelectField.tsx +84 -22
- package/src/widgets/SliderField.tsx +1 -1
- package/src/widgets/SummaryField.tsx +1 -1
- package/src/widgets/TagsField.placeholder.no-provider.test.tsx +68 -0
- package/src/widgets/TagsField.placeholder.test.tsx +95 -0
- package/src/widgets/TagsField.tsx +23 -2
- package/src/widgets/TextAreaField.tsx +171 -93
- package/src/widgets/TextField.tsx +3 -3
- package/src/widgets/TimeField.tsx +2 -2
- package/src/widgets/UrlField.tsx +5 -5
- package/src/widgets/UserField.tsx +4 -3
- package/src/widgets/VectorField.tsx +1 -1
- package/src/widgets/__tests__/FilterConditionField.operators.test.ts +25 -1
- package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +248 -0
- package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +116 -0
- package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +244 -0
- package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +195 -0
- package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +417 -0
- package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +287 -0
- package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +160 -0
- package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +161 -0
- package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +346 -0
- package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +246 -0
- package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +175 -0
- package/src/widgets/address-format.ts +120 -0
- package/src/widgets/file-size-guard.ts +55 -0
- package/src/widgets/nativeDateValue.test.ts +115 -0
- package/src/widgets/nativeDateValue.ts +112 -0
- package/src/widgets/optionWidgets.gatedOptions.test.tsx +267 -0
- package/src/widgets/optionWidgets.unconfiguredOptions.test.tsx +150 -0
- package/src/widgets/toDomProps.ts +204 -0
- package/src/widgets/toHostGroupProps.ts +102 -0
- package/src/widgets/types.ts +135 -35
- package/src/widgets/useFieldTranslation.ts +46 -4
- package/src/widgets/useRecordQuery.ts +30 -3
- package/src/withFieldCarrier.tsx +75 -0
- package/dist/FieldEditWidget.test.d.ts +0 -8
- package/dist/__tests__/DateCellRenderer.test.d.ts +0 -0
- package/dist/__tests__/NumberCellRenderer.test.d.ts +0 -0
- package/dist/__tests__/lookupCellDisplayField.test.d.ts +0 -0
- package/dist/__tests__/lookupCellDisplayName.test.d.ts +0 -0
- package/dist/__tests__/lookupCellReferenceKey.test.d.ts +0 -0
- package/dist/__tests__/registry-collision.test.d.ts +0 -0
- package/dist/__tests__/resolveFieldCurrency.test.d.ts +0 -1
- package/dist/__tests__/spec-symbol-batch7.test.d.ts +0 -8
- package/dist/__tests__/validation-feedback.test.d.ts +0 -0
- package/dist/__tests__/widget-props-contract.test.d.ts +0 -8
- package/dist/coerce-safe-value.test.d.ts +0 -1
- package/dist/complex-widgets.test.d.ts +0 -1
- package/dist/datetime-widgets.test.d.ts +0 -1
- package/dist/field-type-coverage.test.d.ts +0 -1
- package/dist/standard-widgets.test.d.ts +0 -0
- package/dist/widgets/CapabilityMultiSelectField.test.d.ts +0 -0
- package/dist/widgets/CheckboxesField.cascade.test.d.ts +0 -0
- package/dist/widgets/GridField.test.d.ts +0 -1
- package/dist/widgets/ImageLightbox.test.d.ts +0 -1
- package/dist/widgets/LookupField.dependsOn.test.d.ts +0 -8
- package/dist/widgets/LookupField.hydration.test.d.ts +0 -1
- package/dist/widgets/LookupField.idField.test.d.ts +0 -1
- package/dist/widgets/MultiSelectField.cascade.test.d.ts +0 -0
- package/dist/widgets/PeoplePicker.test.d.ts +0 -8
- package/dist/widgets/PeoplePickerComponents.test.d.ts +0 -8
- package/dist/widgets/RadioField.cascade.test.d.ts +0 -0
- package/dist/widgets/RecipientPickerField.test.d.ts +0 -0
- package/dist/widgets/SelectField.cascade.test.d.ts +0 -0
- package/dist/widgets/UserFieldPicker.test.d.ts +0 -8
- package/dist/widgets/__tests__/FilterConditionField.matchAll.test.d.ts +0 -8
- package/dist/widgets/__tests__/FilterConditionField.operators.test.d.ts +0 -8
- package/dist/widgets/deriveLookupColumns.test.d.ts +0 -1
- package/dist/widgets/file-value.test.d.ts +0 -1
- package/dist/widgets/personDisplay.test.d.ts +0 -8
- package/dist/widgets/recentLookups.test.d.ts +0 -1
- package/dist/widgets/useRecordQuery.test.d.ts +0 -8
- package/dist/widgets/useUploadingSignal.test.d.ts +0 -1
|
@@ -1,53 +1,85 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Input, EmptyValue } from '@object-ui/components';
|
|
3
3
|
import { FieldWidgetComponentProps } from './types';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { toDomProps } from './toDomProps';
|
|
5
|
+
import { useLocalization, useDisplayLocale, formatDisplayNumber } from '@object-ui/i18n';
|
|
6
|
+
import { resolveFieldCurrency, currencyFractionDigits, currencySymbol } from '../currency';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Format currency value for display. When `currency` is undefined the value
|
|
9
10
|
* is rendered as a plain number with thousands separators (no symbol),
|
|
10
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).
|
|
11
16
|
*/
|
|
12
|
-
function formatAmount(
|
|
17
|
+
function formatAmount(
|
|
18
|
+
value: number,
|
|
19
|
+
currency: string | undefined,
|
|
20
|
+
precision: number,
|
|
21
|
+
locale?: string,
|
|
22
|
+
): string {
|
|
13
23
|
if (currency) {
|
|
14
24
|
try {
|
|
15
|
-
return
|
|
16
|
-
|
|
25
|
+
return formatDisplayNumber(value, {
|
|
26
|
+
locale,
|
|
17
27
|
currency,
|
|
18
28
|
minimumFractionDigits: precision,
|
|
19
29
|
maximumFractionDigits: precision,
|
|
20
|
-
})
|
|
30
|
+
});
|
|
21
31
|
} catch {
|
|
22
32
|
return `${currency} ${value.toFixed(precision)}`;
|
|
23
33
|
}
|
|
24
34
|
}
|
|
25
35
|
try {
|
|
26
|
-
|
|
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,
|
|
27
41
|
minimumFractionDigits: precision,
|
|
28
42
|
maximumFractionDigits: precision,
|
|
29
|
-
})
|
|
43
|
+
});
|
|
30
44
|
} catch {
|
|
31
45
|
return value.toFixed(precision);
|
|
32
46
|
}
|
|
33
47
|
}
|
|
34
48
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export function CurrencyField({ value, onChange, field, readonly, errorMessage, className, ...props }: FieldWidgetComponentProps<number>) {
|
|
40
|
-
const currencyField = (field || (props as any).schema) as any;
|
|
49
|
+
export function CurrencyField({ value, onChange, field, readonly, error, className, ...props }: FieldWidgetComponentProps<number>) {
|
|
50
|
+
const currencyField = field as any;
|
|
41
51
|
// Shared precedence: field currency → currencyConfig → tenant default (ADR-0053).
|
|
42
52
|
const { currency: tenantCurrency } = useLocalization();
|
|
53
|
+
const locale = useDisplayLocale();
|
|
43
54
|
const currency: string | undefined = resolveFieldCurrency(currencyField, tenantCurrency);
|
|
44
|
-
|
|
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);
|
|
45
77
|
|
|
46
78
|
if (readonly) {
|
|
47
79
|
if (value == null) return <EmptyValue />;
|
|
48
80
|
return (
|
|
49
81
|
<span className="text-sm font-medium tabular-nums">
|
|
50
|
-
{formatAmount(Number(value), currency, precision)}
|
|
82
|
+
{formatAmount(Number(value), currency, precision, locale)}
|
|
51
83
|
</span>
|
|
52
84
|
);
|
|
53
85
|
}
|
|
@@ -60,7 +92,16 @@ export function CurrencyField({ value, onChange, field, readonly, errorMessage,
|
|
|
60
92
|
}
|
|
61
93
|
};
|
|
62
94
|
|
|
63
|
-
|
|
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) : '';
|
|
64
105
|
|
|
65
106
|
return (
|
|
66
107
|
<div className="relative">
|
|
@@ -70,7 +111,7 @@ export function CurrencyField({ value, onChange, field, readonly, errorMessage,
|
|
|
70
111
|
</span>
|
|
71
112
|
)}
|
|
72
113
|
<Input
|
|
73
|
-
{...props}
|
|
114
|
+
{...toDomProps(props)}
|
|
74
115
|
type="number"
|
|
75
116
|
value={value ?? ''}
|
|
76
117
|
onChange={(e) => {
|
|
@@ -87,7 +128,7 @@ export function CurrencyField({ value, onChange, field, readonly, errorMessage,
|
|
|
87
128
|
min={typeof currencyField?.min === 'number' ? currencyField.min : undefined}
|
|
88
129
|
max={typeof currencyField?.max === 'number' ? currencyField.max : undefined}
|
|
89
130
|
step={Math.pow(10, -precision).toFixed(precision)}
|
|
90
|
-
aria-invalid={!!
|
|
131
|
+
aria-invalid={!!error}
|
|
91
132
|
/>
|
|
92
133
|
</div>
|
|
93
134
|
);
|
|
@@ -1,25 +1,34 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Input, EmptyValue } from '@object-ui/components';
|
|
3
|
+
import { useDisplayLocale } from '@object-ui/i18n';
|
|
3
4
|
import { FieldWidgetComponentProps } from './types';
|
|
5
|
+
import { toDomProps } from './toDomProps';
|
|
4
6
|
import { openNativePicker } from './openNativePicker';
|
|
7
|
+
import { toDateInputValue } from './nativeDateValue';
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* DateField - Date picker input widget
|
|
8
11
|
* Uses native date input and displays locale-formatted date in readonly mode
|
|
9
12
|
*/
|
|
10
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();
|
|
11
18
|
if (readonly) {
|
|
12
|
-
return value ? <span className="text-sm">{new Date(value).toLocaleDateString()}</span> : <EmptyValue />;
|
|
19
|
+
return value ? <span className="text-sm">{new Date(value).toLocaleDateString(locale)}</span> : <EmptyValue />;
|
|
13
20
|
}
|
|
14
21
|
|
|
15
|
-
|
|
16
|
-
const { inputType, ...domProps } = props as any;
|
|
22
|
+
const domProps = toDomProps(props);
|
|
17
23
|
|
|
18
24
|
return (
|
|
19
25
|
<Input
|
|
20
26
|
{...domProps}
|
|
21
27
|
type="date"
|
|
22
|
-
|
|
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)}
|
|
23
32
|
onChange={(e) => onChange(e.target.value)}
|
|
24
33
|
onClick={(e) => {
|
|
25
34
|
openNativePicker(e.currentTarget);
|
|
@@ -1,32 +1,41 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Input, EmptyValue } from '@object-ui/components';
|
|
3
|
+
import { useDisplayLocale } from '@object-ui/i18n';
|
|
3
4
|
import { FieldWidgetComponentProps } from './types';
|
|
5
|
+
import { toDomProps } from './toDomProps';
|
|
4
6
|
import { openNativePicker } from './openNativePicker';
|
|
7
|
+
import { toDateTimeInputValue, fromDateTimeInputValue } from './nativeDateValue';
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* DateTimeField - Combined date and time picker widget
|
|
8
11
|
* Displays both date and time in locale format when readonly
|
|
9
12
|
*/
|
|
10
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();
|
|
11
18
|
if (readonly) {
|
|
12
19
|
if (!value) return <EmptyValue />;
|
|
13
20
|
const date = new Date(value);
|
|
14
21
|
return (
|
|
15
22
|
<span className="text-sm">
|
|
16
|
-
{date.toLocaleDateString()} {date.toLocaleTimeString()}
|
|
23
|
+
{date.toLocaleDateString(locale)} {date.toLocaleTimeString(locale)}
|
|
17
24
|
</span>
|
|
18
25
|
);
|
|
19
26
|
}
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
const { inputType, ...domProps } = props as any;
|
|
28
|
+
const domProps = toDomProps(props);
|
|
23
29
|
|
|
24
30
|
return (
|
|
25
31
|
<Input
|
|
26
32
|
{...domProps}
|
|
27
33
|
type="datetime-local"
|
|
28
|
-
value
|
|
29
|
-
|
|
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))}
|
|
30
39
|
onClick={(e) => {
|
|
31
40
|
openNativePicker(e.currentTarget);
|
|
32
41
|
domProps.onClick?.(e);
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Input, EmptyValue } from '@object-ui/components';
|
|
3
3
|
import { FieldWidgetComponentProps } from './types';
|
|
4
|
+
import { toDomProps } from './toDomProps';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* EmailField - Email address input with mailto link in readonly mode
|
|
7
8
|
* Renders as a clickable mailto link when readonly, standard email input otherwise
|
|
8
9
|
*/
|
|
9
|
-
export function EmailField({ value, onChange, field, readonly,
|
|
10
|
-
const config = field
|
|
10
|
+
export function EmailField({ value, onChange, field, readonly, error, ...props }: FieldWidgetComponentProps<string>) {
|
|
11
|
+
const config = field;
|
|
11
12
|
if (readonly) {
|
|
12
13
|
if (!value) return <EmptyValue />;
|
|
13
14
|
return (
|
|
@@ -20,8 +21,7 @@ export function EmailField({ value, onChange, field, readonly, errorMessage, ...
|
|
|
20
21
|
);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
const { inputType, ...domProps } = props as any;
|
|
24
|
+
const domProps = toDomProps(props);
|
|
25
25
|
|
|
26
26
|
return (
|
|
27
27
|
<Input
|
|
@@ -31,7 +31,7 @@ export function EmailField({ value, onChange, field, readonly, errorMessage, ...
|
|
|
31
31
|
onChange={(e) => onChange(e.target.value)}
|
|
32
32
|
placeholder={config?.placeholder || 'email@example.com'}
|
|
33
33
|
disabled={readonly || domProps.disabled}
|
|
34
|
-
aria-invalid={!!
|
|
34
|
+
aria-invalid={!!error}
|
|
35
35
|
/>
|
|
36
36
|
);
|
|
37
37
|
}
|
|
@@ -4,7 +4,10 @@ import { useUpload } from '@object-ui/providers';
|
|
|
4
4
|
import { useObjectTranslation } from '@object-ui/i18n';
|
|
5
5
|
import { Upload, X, File as FileIcon, ImageIcon, Camera, Loader2 } from 'lucide-react';
|
|
6
6
|
import { FieldWidgetComponentProps } from './types';
|
|
7
|
+
import { toDomProps } from './toDomProps';
|
|
8
|
+
import { toHostGroupProps } from './toHostGroupProps';
|
|
7
9
|
import { useUploadingSignal } from './useUploadingSignal';
|
|
10
|
+
import { maxSizeError, type TranslateFn } from './file-size-guard';
|
|
8
11
|
import {
|
|
9
12
|
fileValueForSubmit,
|
|
10
13
|
readFileValues,
|
|
@@ -46,13 +49,11 @@ function useFileUploads(opts: {
|
|
|
46
49
|
if (selectedFiles.length === 0) return;
|
|
47
50
|
const newErrors: string[] = [];
|
|
48
51
|
|
|
52
|
+
// Shared with ImageField — see `file-size-guard`.
|
|
49
53
|
const validFiles = selectedFiles.filter(file => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
newErrors.push(
|
|
53
|
-
defaultValue: `"${file.name}" exceeds max size (${maxMB} MB)`,
|
|
54
|
-
name: file.name, max: maxMB,
|
|
55
|
-
}));
|
|
54
|
+
const rejection = maxSizeError(t as TranslateFn, file, maxSize);
|
|
55
|
+
if (rejection) {
|
|
56
|
+
newErrors.push(rejection);
|
|
56
57
|
return false;
|
|
57
58
|
}
|
|
58
59
|
return true;
|
|
@@ -115,11 +116,11 @@ function useFileUploads(opts: {
|
|
|
115
116
|
* Supports single and multiple file uploads with configurable accepted file types.
|
|
116
117
|
* L2: File size validation, per-file progress indicators, error messages.
|
|
117
118
|
*/
|
|
118
|
-
export function FileField({ value, onChange, field, readonly, onUploadingChange, ...props }: FieldWidgetComponentProps<any>) {
|
|
119
|
+
export function FileField({ value, onChange, field, readonly, onUploadingChange, error, ...props }: FieldWidgetComponentProps<any>) {
|
|
119
120
|
const { t } = useObjectTranslation();
|
|
120
121
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
121
122
|
const cameraRef = useRef<HTMLInputElement>(null);
|
|
122
|
-
const fileField =
|
|
123
|
+
const fileField = field as any;
|
|
123
124
|
const multiple = fileField?.multiple || false;
|
|
124
125
|
const accept = fileField?.accept ? fileField.accept.join(',') : undefined;
|
|
125
126
|
const maxSize = fileField?.maxSize as number | undefined; // bytes
|
|
@@ -183,10 +184,18 @@ export function FileField({ value, onChange, field, readonly, onUploadingChange,
|
|
|
183
184
|
}, [accept, processFiles]);
|
|
184
185
|
|
|
185
186
|
if (readonly) {
|
|
186
|
-
|
|
187
|
+
// Readonly there is no dropzone: the field's whole rendered surface is the
|
|
188
|
+
// list of file names, so THAT is what the host's group label names
|
|
189
|
+
// (objectui#3990). The role is therefore `group` here and `button` (the
|
|
190
|
+
// dropzone) while editable — the same widget, two different surfaces. See
|
|
191
|
+
// `toHostGroupProps`; `EmptyValue`'s own `aria-label` ("No value") is
|
|
192
|
+
// outranked by `aria-labelledby` per accname, and on the `generic` role that
|
|
193
|
+
// placeholder span carries it was never exposed as a name anyway.
|
|
194
|
+
const hostGroupProps = toHostGroupProps(props);
|
|
195
|
+
if (!value) return <EmptyValue {...hostGroupProps} />;
|
|
187
196
|
|
|
188
197
|
return (
|
|
189
|
-
<div className="flex flex-wrap gap-2">
|
|
198
|
+
<div {...hostGroupProps} className="flex flex-wrap gap-2">
|
|
190
199
|
{views.map((file, idx) => (
|
|
191
200
|
<span key={idx} className="text-sm truncate max-w-xs">
|
|
192
201
|
{file.name}
|
|
@@ -209,6 +218,8 @@ export function FileField({ value, onChange, field, readonly, onUploadingChange,
|
|
|
209
218
|
}
|
|
210
219
|
};
|
|
211
220
|
|
|
221
|
+
const { name: _domName, ...dropzoneDomProps } = toDomProps(props);
|
|
222
|
+
|
|
212
223
|
return (
|
|
213
224
|
<div className={props.className}>
|
|
214
225
|
<input
|
|
@@ -233,8 +244,23 @@ export function FileField({ value, onChange, field, readonly, onUploadingChange,
|
|
|
233
244
|
)}
|
|
234
245
|
|
|
235
246
|
<div className="space-y-2">
|
|
236
|
-
{/* Drag-and-drop zone
|
|
247
|
+
{/* Drag-and-drop zone — the widget's real focusable control (it is the
|
|
248
|
+
keyboard path to the hidden file input), so the DOM pass-through
|
|
249
|
+
and the validation state land here (objectui#3318). `name` is
|
|
250
|
+
withheld: only DOM-legal on form controls, and on this div it is
|
|
251
|
+
exactly the leak #3291 sweeps for.
|
|
252
|
+
|
|
253
|
+
It also carries the host's label. This widget is declared
|
|
254
|
+
`labelling: 'group'` (objectui#3961) NOT because it is composite — it
|
|
255
|
+
has exactly one control — but because that one control is a
|
|
256
|
+
`div[role="button"]`, which no `<label for>` can reach. So the form
|
|
257
|
+
renderer names it by IDREF instead, and `aria-labelledby` arrives in
|
|
258
|
+
the same whitelist spread (`toDomProps` forwards the whole `aria-`
|
|
259
|
+
family). Deliberately no extra `role="group"` wrapper: there is no
|
|
260
|
+
group here, and adding one would announce a container that holds a
|
|
261
|
+
single button. */}
|
|
237
262
|
<div
|
|
263
|
+
{...dropzoneDomProps}
|
|
238
264
|
onDragOver={handleDragOver}
|
|
239
265
|
onDragLeave={handleDragLeave}
|
|
240
266
|
onDrop={handleDrop}
|
|
@@ -255,6 +281,8 @@ export function FileField({ value, onChange, field, readonly, onUploadingChange,
|
|
|
255
281
|
inputRef.current?.click();
|
|
256
282
|
}
|
|
257
283
|
}}
|
|
284
|
+
// AFTER the spread so this widget's own computation wins (#3222).
|
|
285
|
+
aria-invalid={!!error}
|
|
258
286
|
>
|
|
259
287
|
<Upload className={`size-8 ${isDragOver ? 'text-primary' : 'text-muted-foreground'}`} />
|
|
260
288
|
<div className="text-center">
|
|
@@ -2,6 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { FilterBuilder, cn } from '@object-ui/components';
|
|
3
3
|
import { SchemaRendererContext } from '@object-ui/react';
|
|
4
4
|
import type { FieldWidgetComponentProps } from './types';
|
|
5
|
+
import { toDomProps } from './toDomProps';
|
|
5
6
|
import { useFieldTranslation } from './useFieldTranslation';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -297,6 +298,7 @@ export function FilterConditionField({
|
|
|
297
298
|
onChange,
|
|
298
299
|
readonly,
|
|
299
300
|
className,
|
|
301
|
+
error,
|
|
300
302
|
...props
|
|
301
303
|
}: FieldWidgetComponentProps<string | object>) {
|
|
302
304
|
const ctx = React.useContext(SchemaRendererContext);
|
|
@@ -397,10 +399,19 @@ export function FilterConditionField({
|
|
|
397
399
|
{rawMode ? (
|
|
398
400
|
<>
|
|
399
401
|
<textarea
|
|
402
|
+
// DOM pass-through onto the raw-JSON editing surface
|
|
403
|
+
// (objectui#3318). NOTE this widget stays on the #3318 ledger
|
|
404
|
+
// regardless: its dependency-gated state (no `object_name` chosen
|
|
405
|
+
// yet — the state a fresh form and the registry sweep render) is a
|
|
406
|
+
// plain hint paragraph with no focusable control.
|
|
407
|
+
{...toDomProps(props)}
|
|
400
408
|
className="min-h-[96px] w-full rounded border bg-background px-2 py-1 font-mono text-xs"
|
|
401
409
|
value={rawValue}
|
|
402
410
|
placeholder='{ "type": "customer", "is_active": true }'
|
|
403
411
|
onChange={(e) => onChange(e.target.value as any)}
|
|
412
|
+
// The form's validation slot (#3222) OR this widget's own
|
|
413
|
+
// unparsable-JSON state, which already renders its red message.
|
|
414
|
+
aria-invalid={!!error || !parsed.ok}
|
|
404
415
|
/>
|
|
405
416
|
{!parsed.ok && (
|
|
406
417
|
<span className="text-xs text-destructive">{t('fields.filterCondition.invalidJson')}</span>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { EmptyValue } from '@object-ui/components';
|
|
3
|
+
import { useDisplayLocale } from '@object-ui/i18n';
|
|
3
4
|
import { FieldWidgetComponentProps } from './types';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -7,7 +8,11 @@ import { FieldWidgetComponentProps } from './types';
|
|
|
7
8
|
* Values are computed on the backend and cannot be edited
|
|
8
9
|
*/
|
|
9
10
|
export function FormulaField({ value, field, ...props }: FieldWidgetComponentProps<any>) {
|
|
10
|
-
|
|
11
|
+
// Before the empty-value early return — the hook count must not change when
|
|
12
|
+
// a value flips between null and set. A `date` return type used to format
|
|
13
|
+
// through the MACHINE's locale (objectui#4468).
|
|
14
|
+
const locale = useDisplayLocale();
|
|
15
|
+
const formulaField = field as any;
|
|
11
16
|
const returnType = formulaField?.return_type || 'text';
|
|
12
17
|
|
|
13
18
|
if (value == null) {
|
|
@@ -20,7 +25,7 @@ export function FormulaField({ value, field, ...props }: FieldWidgetComponentPro
|
|
|
20
25
|
} else if (returnType === 'boolean') {
|
|
21
26
|
displayValue = value ? 'Yes' : 'No';
|
|
22
27
|
} else if (returnType === 'date') {
|
|
23
|
-
displayValue = new Date(value).toLocaleDateString();
|
|
28
|
+
displayValue = new Date(value).toLocaleDateString(locale);
|
|
24
29
|
} else {
|
|
25
30
|
displayValue = String(value);
|
|
26
31
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
import React from 'react';
|
|
10
|
+
import { FullscreenEditor, type FullscreenEditorProps } from '@object-ui/components';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The fullscreen-edit affordance shared by every long-text widget in this
|
|
14
|
+
* package (`field:textarea` → `TextAreaField`, `field:markdown` / `field:html`
|
|
15
|
+
* → `RichTextField`).
|
|
16
|
+
*
|
|
17
|
+
* ## This is now a thin wrapper (objectui#3398)
|
|
18
|
+
*
|
|
19
|
+
* The affordance, the dialog, the draft/commit state machine and the
|
|
20
|
+
* `readOnly` / `disabled` semantics all live in `FullscreenEditor`
|
|
21
|
+
* (`@object-ui/components`). They used to live here AND, separately, in the
|
|
22
|
+
* form renderer's built-in `textarea` branch — one form-level promise
|
|
23
|
+
* (`ObjectFormSchema.mobile.fullscreenLongText`, projected onto every long-text
|
|
24
|
+
* field as `mobile_fullscreen`) answered twice, on the two render paths.
|
|
25
|
+
*
|
|
26
|
+
* Two copies of a state machine drift, and these did: objectui#3400 measured a
|
|
27
|
+
* read-only field editable through the built-in branch's dialog, objectui#3402
|
|
28
|
+
* measured the same hole for `disabled` here, and #3393 (label in the dialog
|
|
29
|
+
* title) and #3272 (i18n) each landed on one side before the other. The
|
|
30
|
+
* maintainer's ruling of 2026-08-10 was one implementation hoisted to the
|
|
31
|
+
* dependency-legal side — and the measured import graph allows exactly that
|
|
32
|
+
* direction: this package depends on `@object-ui/components`, which declares no
|
|
33
|
+
* dependency on `@object-ui/fields` in `dependencies` or `peerDependencies`.
|
|
34
|
+
*
|
|
35
|
+
* ## Why the wrapper survives the merge rather than being deleted
|
|
36
|
+
*
|
|
37
|
+
* It carries the one thing that is genuinely about THIS package: `readOnly` is
|
|
38
|
+
* not part of the contract here. Both hosts early-return a read-only DISPLAY
|
|
39
|
+
* before they compute the affordance (`TextAreaField.tsx`, `RichTextField.tsx`),
|
|
40
|
+
* so a `readOnly` reaching this component would be a prop with no producer on
|
|
41
|
+
* this path — the shape this package keeps deleting (objectui#3232/#3233). The
|
|
42
|
+
* `Omit` below is that statement in the type system, and it is why the hoist did
|
|
43
|
+
* not simply re-export the primitive. A future host that renders an editor for
|
|
44
|
+
* read-only fields must widen this type AND pass the prop together; the
|
|
45
|
+
* primitive already answers it (no affordance at all).
|
|
46
|
+
*
|
|
47
|
+
* Everything else — `value` / `onCommit` / `label` / `testIdPrefix` /
|
|
48
|
+
* `disabled` / `children` / `footer` — passes straight through, so the hosts and
|
|
49
|
+
* their pins are unchanged. `testIdPrefix` still namespaces this package's ids
|
|
50
|
+
* (`textarea-`, `richtext-`), distinct from the built-in branch's
|
|
51
|
+
* `form-textarea-`, so a test can still say which render path it found.
|
|
52
|
+
*/
|
|
53
|
+
export type FullscreenFieldEditorProps = Omit<FullscreenEditorProps, 'readOnly'>;
|
|
54
|
+
|
|
55
|
+
export function FullscreenFieldEditor(props: FullscreenFieldEditorProps) {
|
|
56
|
+
// `readOnly` is deliberately not forwarded — it is not in the props type, and
|
|
57
|
+
// the primitive defaults it to false, which is the behaviour both hosts rely
|
|
58
|
+
// on: they have already returned a read-only display before reaching here.
|
|
59
|
+
return <FullscreenEditor {...props} />;
|
|
60
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useId } from 'react';
|
|
2
2
|
import { Input, Button, Label, EmptyValue } from '@object-ui/components';
|
|
3
3
|
import { MapPin, Crosshair } from 'lucide-react';
|
|
4
4
|
import { FieldWidgetComponentProps } from './types';
|
|
5
|
+
import { toDomProps } from './toDomProps';
|
|
6
|
+
import { toHostGroupProps } from './toHostGroupProps';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Geolocation data structure
|
|
@@ -16,9 +18,37 @@ export interface GeolocationValue {
|
|
|
16
18
|
* Geolocation field widget - provides a location picker with coordinates
|
|
17
19
|
* Supports manual entry and browser geolocation API
|
|
18
20
|
*/
|
|
19
|
-
export function GeolocationField({ value, onChange, field, readonly, ...props }: FieldWidgetComponentProps<GeolocationValue>) {
|
|
21
|
+
export function GeolocationField({ value, onChange, field, readonly, error, ...props }: FieldWidgetComponentProps<GeolocationValue>) {
|
|
20
22
|
const [isLoading, setIsLoading] = React.useState(false);
|
|
21
23
|
const location = value || {};
|
|
24
|
+
// DOM pass-through (objectui#3318): the whitelist spread goes onto the FIRST
|
|
25
|
+
// sub-input (latitude); the composite's validation state goes onto BOTH
|
|
26
|
+
// focusable sub-inputs via `aria-invalid={!!error}`.
|
|
27
|
+
//
|
|
28
|
+
// `id` and `aria-labelledby` are held back and land on the group CONTAINER
|
|
29
|
+
// instead (objectui#3961) — they address the whole field, not the latitude box.
|
|
30
|
+
// The host `id` never reached the DOM before: it was overwritten one line later
|
|
31
|
+
// by `id={subId('latitude')}` (objectui#3343), leaving the form's group label
|
|
32
|
+
// pointing at nothing. And `aria-labelledby` on the latitude input would
|
|
33
|
+
// OVERRIDE its own "Latitude" label with the field name. `aria-describedby` and
|
|
34
|
+
// the rest stay on the first sub-input, where focus can reach them.
|
|
35
|
+
const {
|
|
36
|
+
id: _hostId,
|
|
37
|
+
'aria-labelledby': _hostLabelledBy,
|
|
38
|
+
...domProps
|
|
39
|
+
} = toDomProps(props);
|
|
40
|
+
// The pair held back above, in the one spelling every group-labelled widget
|
|
41
|
+
// uses for it — and computed HERE, before the readonly early return below,
|
|
42
|
+
// because that return used to drop both keys on the floor: a published label
|
|
43
|
+
// id with no consumer in the document (objectui#3990). See `toHostGroupProps`.
|
|
44
|
+
const hostGroupProps = toHostGroupProps(props);
|
|
45
|
+
// Sub-input ids (objectui#3343): `useId()` prefix + sub-field name — the
|
|
46
|
+
// `groupId` paradigm of RadioField / CheckboxesField. Hardcoded literals
|
|
47
|
+
// ("latitude" / "longitude") collide as soon as a form renders two
|
|
48
|
+
// geolocation fields, and every label's htmlFor then resolves to the
|
|
49
|
+
// FIRST match.
|
|
50
|
+
const groupId = useId();
|
|
51
|
+
const subId = (name: keyof GeolocationValue) => `${groupId}-${name}`;
|
|
22
52
|
|
|
23
53
|
const handleFieldChange = (fieldName: keyof GeolocationValue, fieldValue: string) => {
|
|
24
54
|
onChange({
|
|
@@ -67,9 +97,13 @@ export function GeolocationField({ value, onChange, field, readonly, ...props }:
|
|
|
67
97
|
};
|
|
68
98
|
|
|
69
99
|
if (readonly) {
|
|
100
|
+
// Readonly the composite collapses to the formatted coordinates (plus the
|
|
101
|
+
// "View on map" link), so THAT row is the surface the host label names
|
|
102
|
+
// (objectui#3990). The `EmptyValue` placeholder sits inside it, so it needs
|
|
103
|
+
// nothing of its own.
|
|
70
104
|
const formatted = formatLocation(location);
|
|
71
105
|
return (
|
|
72
|
-
<div className="flex items-center gap-2">
|
|
106
|
+
<div {...hostGroupProps} className="flex items-center gap-2">
|
|
73
107
|
<MapPin className="w-4 h-4 text-muted-foreground" />
|
|
74
108
|
{formatted ? <span className="text-sm">{formatted}</span> : <EmptyValue />}
|
|
75
109
|
{location.latitude && location.longitude && (
|
|
@@ -88,7 +122,11 @@ export function GeolocationField({ value, onChange, field, readonly, ...props }:
|
|
|
88
122
|
}
|
|
89
123
|
|
|
90
124
|
return (
|
|
91
|
-
|
|
125
|
+
// `role="group"` only when a host actually named this container
|
|
126
|
+
// (objectui#3961); standalone rendering stays exactly as it was. That
|
|
127
|
+
// condition now lives in `toHostGroupProps` so this container and the
|
|
128
|
+
// readonly row above cannot answer differently (objectui#3990).
|
|
129
|
+
<div className="space-y-3" {...hostGroupProps}>
|
|
92
130
|
<div className="flex items-center gap-2">
|
|
93
131
|
<Button
|
|
94
132
|
type="button"
|
|
@@ -115,9 +153,10 @@ export function GeolocationField({ value, onChange, field, readonly, ...props }:
|
|
|
115
153
|
|
|
116
154
|
<div className="grid grid-cols-2 gap-3">
|
|
117
155
|
<div>
|
|
118
|
-
<Label htmlFor=
|
|
156
|
+
<Label htmlFor={subId('latitude')} className="text-xs">Latitude</Label>
|
|
119
157
|
<Input
|
|
120
|
-
|
|
158
|
+
{...domProps}
|
|
159
|
+
id={subId('latitude')}
|
|
121
160
|
type="number"
|
|
122
161
|
value={location.latitude ?? ''}
|
|
123
162
|
onChange={(e) => handleFieldChange('latitude', e.target.value)}
|
|
@@ -125,19 +164,21 @@ export function GeolocationField({ value, onChange, field, readonly, ...props }:
|
|
|
125
164
|
disabled={readonly || props.disabled}
|
|
126
165
|
step="any"
|
|
127
166
|
className={props.className}
|
|
167
|
+
aria-invalid={!!error}
|
|
128
168
|
/>
|
|
129
169
|
</div>
|
|
130
170
|
|
|
131
171
|
<div>
|
|
132
|
-
<Label htmlFor=
|
|
172
|
+
<Label htmlFor={subId('longitude')} className="text-xs">Longitude</Label>
|
|
133
173
|
<Input
|
|
134
|
-
id=
|
|
174
|
+
id={subId('longitude')}
|
|
135
175
|
type="number"
|
|
136
176
|
value={location.longitude ?? ''}
|
|
137
177
|
onChange={(e) => handleFieldChange('longitude', e.target.value)}
|
|
138
178
|
placeholder="-122.4194"
|
|
139
179
|
disabled={readonly || props.disabled}
|
|
140
180
|
step="any"
|
|
181
|
+
aria-invalid={!!error}
|
|
141
182
|
/>
|
|
142
183
|
</div>
|
|
143
184
|
</div>
|