@object-ui/fields 17.1.0 → 17.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +786 -0
- package/dist/FieldEditWidget.d.ts +2 -2
- package/dist/__tests__/cell-truncation.test.d.ts +0 -0
- package/dist/__tests__/field-carrier-sdui.test.d.ts +0 -0
- package/dist/__tests__/spec-symbol-batch7.test.d.ts +8 -0
- package/dist/__tests__/widget-aria-invalid-e2e.test.d.ts +0 -0
- package/dist/__tests__/widget-aria-invalid-registry-e2e.test.d.ts +0 -0
- package/dist/__tests__/widget-dom-leak-e2e.test.d.ts +0 -0
- package/dist/__tests__/widget-props-contract.test.d.ts +8 -0
- package/dist/index.cjs +5 -5
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2882 -2569
- package/dist/widgets/AddressField.d.ts +2 -2
- package/dist/widgets/AddressField.uniqueIds.test.d.ts +0 -0
- package/dist/widgets/AutoNumberField.d.ts +2 -2
- package/dist/widgets/AvatarField.d.ts +2 -2
- package/dist/widgets/BooleanField.d.ts +2 -2
- package/dist/widgets/CapabilityMultiSelectField.d.ts +2 -2
- package/dist/widgets/CharacterCount.d.ts +56 -0
- package/dist/widgets/CheckboxesField.d.ts +2 -2
- package/dist/widgets/CodeField.d.ts +2 -2
- package/dist/widgets/ColorField.d.ts +2 -2
- package/dist/widgets/CurrencyField.d.ts +2 -2
- package/dist/widgets/DateField.d.ts +2 -2
- package/dist/widgets/DateTimeField.d.ts +2 -2
- package/dist/widgets/EmailField.d.ts +2 -2
- package/dist/widgets/FileField.d.ts +2 -2
- package/dist/widgets/FilterConditionField.d.ts +2 -2
- package/dist/widgets/FormulaField.d.ts +2 -2
- package/dist/widgets/FullscreenFieldEditor.d.ts +155 -0
- package/dist/widgets/GeolocationField.d.ts +2 -2
- package/dist/widgets/GeolocationField.uniqueIds.test.d.ts +0 -0
- package/dist/widgets/GridField.d.ts +2 -2
- package/dist/widgets/ImageField.d.ts +2 -2
- package/dist/widgets/LocationField.d.ts +2 -2
- package/dist/widgets/LookupField.d.ts +2 -2
- package/dist/widgets/LookupField.gateHintLabel.test.d.ts +8 -0
- package/dist/widgets/MasterDetailField.d.ts +2 -2
- package/dist/widgets/MultiSelectField.d.ts +2 -2
- package/dist/widgets/NumberField.d.ts +2 -2
- package/dist/widgets/ObjectField.d.ts +2 -2
- package/dist/widgets/ObjectRefField.d.ts +2 -2
- package/dist/widgets/OptionsEmptyState.d.ts +45 -0
- package/dist/widgets/OptionsEmptyState.no-provider.test.d.ts +0 -0
- package/dist/widgets/OptionsEmptyState.test.d.ts +0 -0
- package/dist/widgets/PasswordField.d.ts +2 -2
- package/dist/widgets/PercentField.d.ts +2 -2
- package/dist/widgets/PhoneField.d.ts +2 -2
- package/dist/widgets/QRCodeField.d.ts +2 -2
- package/dist/widgets/RadioField.d.ts +2 -2
- package/dist/widgets/RatingField.d.ts +2 -2
- package/dist/widgets/RecipientPickerField.d.ts +2 -2
- package/dist/widgets/RecordPickerDialog.d.ts +17 -1
- package/dist/widgets/RecordPickerDialog.filterOptions.test.d.ts +8 -0
- package/dist/widgets/RecordPickerDialog.selectLabel.test.d.ts +8 -0
- package/dist/widgets/RichTextField.d.ts +20 -2
- package/dist/widgets/SelectField.d.ts +2 -2
- package/dist/widgets/SignatureField.d.ts +2 -2
- package/dist/widgets/SliderField.d.ts +2 -2
- package/dist/widgets/SummaryField.d.ts +2 -2
- package/dist/widgets/TagsField.d.ts +2 -2
- package/dist/widgets/TagsField.placeholder.no-provider.test.d.ts +0 -0
- package/dist/widgets/TagsField.placeholder.test.d.ts +0 -0
- package/dist/widgets/TextAreaField.d.ts +45 -6
- package/dist/widgets/TextField.d.ts +2 -2
- package/dist/widgets/TimeField.d.ts +2 -2
- package/dist/widgets/UrlField.d.ts +2 -2
- package/dist/widgets/UserField.d.ts +2 -2
- package/dist/widgets/VectorField.d.ts +2 -2
- package/dist/widgets/__tests__/FullscreenFieldEditor.i18n.test.d.ts +0 -0
- package/dist/widgets/__tests__/FullscreenFieldEditor.no-provider.test.d.ts +0 -0
- package/dist/widgets/__tests__/RichTextField.fullscreenDisabled.test.d.ts +0 -0
- package/dist/widgets/__tests__/RichTextField.mobileFullscreen.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.characterCount.announcements.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.characterCount.i18n.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.characterCount.no-provider.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.fullscreenDisabled.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.mobileFullscreen.test.d.ts +0 -0
- package/dist/widgets/file-value.d.ts +20 -33
- package/dist/widgets/toDomProps.d.ts +91 -0
- package/dist/widgets/types.d.ts +215 -4
- package/dist/withFieldCarrier.d.ts +56 -0
- package/package.json +13 -13
- package/src/FieldEditWidget.tsx +6 -4
- package/src/__tests__/cell-truncation.test.tsx +96 -0
- package/src/__tests__/field-carrier-sdui.test.tsx +169 -0
- package/src/__tests__/spec-symbol-batch7.test.ts +126 -0
- package/src/__tests__/validation-feedback.test.tsx +22 -13
- package/src/__tests__/widget-aria-invalid-e2e.test.tsx +265 -0
- package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +338 -0
- package/src/__tests__/widget-dom-leak-e2e.test.tsx +598 -0
- package/src/__tests__/widget-props-contract.test.tsx +142 -0
- package/src/complex-widgets.test.tsx +3 -3
- package/src/datetime-widgets.test.tsx +2 -2
- package/src/index.tsx +77 -27
- package/src/widgets/AddressField.tsx +32 -13
- package/src/widgets/AddressField.uniqueIds.test.tsx +92 -0
- package/src/widgets/AutoNumberField.tsx +2 -2
- package/src/widgets/AvatarField.tsx +10 -3
- package/src/widgets/BooleanField.tsx +5 -5
- package/src/widgets/CapabilityMultiSelectField.tsx +4 -4
- package/src/widgets/CharacterCount.tsx +263 -0
- package/src/widgets/CheckboxesField.tsx +35 -18
- package/src/widgets/CodeField.tsx +10 -3
- package/src/widgets/ColorField.tsx +12 -3
- package/src/widgets/CurrencyField.tsx +6 -5
- package/src/widgets/DateField.tsx +4 -4
- package/src/widgets/DateTimeField.tsx +4 -4
- package/src/widgets/EmailField.tsx +6 -6
- package/src/widgets/FileField.tsx +14 -4
- package/src/widgets/FilterConditionField.tsx +14 -3
- package/src/widgets/FormulaField.tsx +3 -3
- package/src/widgets/FullscreenFieldEditor.tsx +271 -0
- package/src/widgets/GeolocationField.tsx +22 -7
- package/src/widgets/GeolocationField.uniqueIds.test.tsx +88 -0
- package/src/widgets/GridField.tsx +4 -4
- package/src/widgets/ImageField.tsx +10 -3
- package/src/widgets/LocationField.tsx +10 -3
- package/src/widgets/LookupField.gateHintLabel.test.tsx +135 -0
- package/src/widgets/LookupField.tsx +50 -15
- package/src/widgets/MasterDetailField.tsx +3 -3
- package/src/widgets/MultiSelectField.tsx +34 -18
- package/src/widgets/NumberField.tsx +5 -5
- package/src/widgets/ObjectField.tsx +18 -9
- package/src/widgets/ObjectRefField.tsx +14 -4
- package/src/widgets/OptionsEmptyState.no-provider.test.tsx +88 -0
- package/src/widgets/OptionsEmptyState.test.tsx +169 -0
- package/src/widgets/OptionsEmptyState.tsx +83 -0
- package/src/widgets/PasswordField.tsx +5 -5
- package/src/widgets/PeoplePicker.tsx +7 -1
- package/src/widgets/PercentField.tsx +6 -5
- package/src/widgets/PhoneField.tsx +6 -6
- package/src/widgets/QRCodeField.tsx +10 -3
- package/src/widgets/RadioField.tsx +28 -18
- package/src/widgets/RatingField.tsx +19 -4
- package/src/widgets/RecipientPickerField.tsx +22 -4
- package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +611 -0
- package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +112 -0
- package/src/widgets/RecordPickerDialog.tsx +173 -8
- package/src/widgets/RichTextField.tsx +152 -25
- package/src/widgets/SelectField.tsx +53 -22
- package/src/widgets/SignatureField.tsx +2 -2
- package/src/widgets/SliderField.tsx +3 -3
- package/src/widgets/SummaryField.tsx +3 -3
- 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 +25 -4
- package/src/widgets/TextAreaField.tsx +172 -94
- package/src/widgets/TextField.tsx +5 -5
- package/src/widgets/TimeField.tsx +4 -4
- package/src/widgets/UrlField.tsx +6 -6
- package/src/widgets/UserField.tsx +3 -3
- package/src/widgets/VectorField.tsx +3 -3
- 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/file-value.ts +21 -5
- package/src/widgets/toDomProps.ts +175 -0
- package/src/widgets/types.ts +230 -7
- package/src/widgets/useFieldTranslation.ts +60 -0
- package/src/withFieldCarrier.tsx +75 -0
|
@@ -0,0 +1,142 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* `FieldWidgetComponentProps` is a CLOSED contract (objectui#3221).
|
|
11
|
+
*
|
|
12
|
+
* It used to end in `[key: string]: any`, which made every check over it
|
|
13
|
+
* vacuous: a type claiming to have every key can never be reported as missing
|
|
14
|
+
* one (objectstack#4075). Three consequences, one test each below:
|
|
15
|
+
*
|
|
16
|
+
* 1. a key the type does not declare is a compile error, not a silent `any`
|
|
17
|
+
* — which is what let objectui#3222 rename the validation slot to the
|
|
18
|
+
* spec's `error` with the compiler, not grep, proving no call site was
|
|
19
|
+
* missed;
|
|
20
|
+
* 2. a MISSPELLED prop (`readOnly` for `readonly`) is rejected;
|
|
21
|
+
* 3. the pass-through keys hosts genuinely forward still type-check, and
|
|
22
|
+
* still reach the rendered control at runtime — closing the type must not
|
|
23
|
+
* have been paid for by dropping real behaviour.
|
|
24
|
+
*
|
|
25
|
+
* (1) and (2) are compile-time: a violation fails `pnpm --filter
|
|
26
|
+
* @object-ui/fields type-check`, not this run. `@ts-expect-error` inverts
|
|
27
|
+
* that — the line fails to compile if the error it expects ever STOPS
|
|
28
|
+
* happening, i.e. if the index signature comes back.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { describe, it, expect } from 'vitest';
|
|
32
|
+
import { render, screen } from '@testing-library/react';
|
|
33
|
+
|
|
34
|
+
import { TextField } from '../widgets/TextField';
|
|
35
|
+
import { BooleanField } from '../widgets/BooleanField';
|
|
36
|
+
import type { FieldWidgetComponentProps } from '../widgets/types';
|
|
37
|
+
import type { FieldMetadata } from '@object-ui/types';
|
|
38
|
+
|
|
39
|
+
type Assert<T extends true> = T;
|
|
40
|
+
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
|
|
41
|
+
type Extends<A, B> = [A] extends [B] ? true : false;
|
|
42
|
+
|
|
43
|
+
const field = { name: 'title', label: 'Title', type: 'text' } as unknown as FieldMetadata;
|
|
44
|
+
|
|
45
|
+
describe('FieldWidgetComponentProps is closed (objectui#3221)', () => {
|
|
46
|
+
it('has no `[key: string]` index signature', () => {
|
|
47
|
+
// The single assertion the whole change turns on. While the index
|
|
48
|
+
// signature existed this was `true`, and every check below was vacuous.
|
|
49
|
+
type _NotOpen = Assert<Equal<Extends<string, keyof FieldWidgetComponentProps>, false>>;
|
|
50
|
+
expect(true).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('rejects keys it does not declare', () => {
|
|
54
|
+
const props = {} as FieldWidgetComponentProps<string>;
|
|
55
|
+
|
|
56
|
+
// `required` stays out on purpose (objectui#3222): the required marker has
|
|
57
|
+
// exactly one author, the form renderer's `<FormLabel>`, and handing the
|
|
58
|
+
// flag to widgets invites a second one. Its sibling `error` went the other
|
|
59
|
+
// way — the spec declares it, so the slot below adopted the spec's name and
|
|
60
|
+
// the form renderer now produces it.
|
|
61
|
+
// @ts-expect-error `required` is not part of this contract
|
|
62
|
+
void props.required;
|
|
63
|
+
const message: string | undefined = props.error;
|
|
64
|
+
void message;
|
|
65
|
+
|
|
66
|
+
// …and the class of bug the index signature hid best: a typo.
|
|
67
|
+
// @ts-expect-error the prop is `readonly`, not React's DOM `readOnly`
|
|
68
|
+
void props.readOnly;
|
|
69
|
+
// @ts-expect-error the prop is `onChange`
|
|
70
|
+
void props.onchange;
|
|
71
|
+
|
|
72
|
+
// The second metadata carrier is gone (objectui#3233). `schema` was a
|
|
73
|
+
// DECLARED key here through #3221 — one concept under two spellings, which
|
|
74
|
+
// is how ~30 widgets ended up resolving `field || schema`. v17 converged it
|
|
75
|
+
// at the producers, so reading it is now a compile error rather than a
|
|
76
|
+
// silent second contract for the next widget author to discover.
|
|
77
|
+
// @ts-expect-error `schema` was retired from the widget contract in v17
|
|
78
|
+
void props.schema;
|
|
79
|
+
|
|
80
|
+
expect(true).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('still accepts the pass-through keys hosts actually forward', () => {
|
|
84
|
+
// Closing the type is only correct if the real call sites still compile.
|
|
85
|
+
// Each key here has a named producer: the form renderer, the inline-edit
|
|
86
|
+
// host, or the line-item grid. Deleting one from the type breaks this.
|
|
87
|
+
const passThrough: FieldWidgetComponentProps<string> = {
|
|
88
|
+
value: '',
|
|
89
|
+
onChange: () => {},
|
|
90
|
+
// The single metadata carrier (objectui#3233) — every host produces this
|
|
91
|
+
// one key: the form renderer (`renderFieldComponent`), the inline-edit
|
|
92
|
+
// hosts, and the registry adapter that maps `SchemaRenderer`'s SDUI node
|
|
93
|
+
// onto it.
|
|
94
|
+
field,
|
|
95
|
+
dataSource: {},
|
|
96
|
+
dependentValues: { country: 'cn' },
|
|
97
|
+
dependsOn: ['country'],
|
|
98
|
+
emptyHint: 'Select country first',
|
|
99
|
+
name: 'title',
|
|
100
|
+
// inline-edit hosts / line-item grid
|
|
101
|
+
compact: true,
|
|
102
|
+
autoFocus: true,
|
|
103
|
+
onSelectRecord: () => {},
|
|
104
|
+
onCreateNew: () => {},
|
|
105
|
+
// DOM / a11y
|
|
106
|
+
id: 'title-input',
|
|
107
|
+
'aria-label': 'Title',
|
|
108
|
+
'data-testid': 'field-title',
|
|
109
|
+
};
|
|
110
|
+
expect(passThrough.compact).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('closing the type kept the pass-through behaviour', () => {
|
|
115
|
+
it('forwards a11y and data attributes to the rendered input', () => {
|
|
116
|
+
render(
|
|
117
|
+
<TextField
|
|
118
|
+
value="hello"
|
|
119
|
+
onChange={() => {}}
|
|
120
|
+
field={field}
|
|
121
|
+
aria-label="Title"
|
|
122
|
+
data-testid="title-input"
|
|
123
|
+
/>,
|
|
124
|
+
);
|
|
125
|
+
const input = screen.getByTestId('title-input');
|
|
126
|
+
expect(input).toHaveAttribute('aria-label', 'Title');
|
|
127
|
+
expect(input).toHaveValue('hello');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('forwards `disabled` — a declared key, not an index-signature accident', () => {
|
|
131
|
+
render(
|
|
132
|
+
<BooleanField
|
|
133
|
+
value={false}
|
|
134
|
+
onChange={() => {}}
|
|
135
|
+
field={{ name: 'active', label: 'Active', type: 'boolean' } as unknown as FieldMetadata}
|
|
136
|
+
disabled
|
|
137
|
+
data-testid="active-switch"
|
|
138
|
+
/>,
|
|
139
|
+
);
|
|
140
|
+
expect(screen.getByTestId('active-switch')).toBeDisabled();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -6,7 +6,7 @@ import { FieldEditWidget } from './FieldEditWidget';
|
|
|
6
6
|
import { MasterDetailField } from './widgets/MasterDetailField';
|
|
7
7
|
import { GridField } from './widgets/GridField';
|
|
8
8
|
import { FileField } from './widgets/FileField';
|
|
9
|
-
import type {
|
|
9
|
+
import type { FieldWidgetComponentProps } from './widgets/types';
|
|
10
10
|
|
|
11
11
|
// ------------- Mocks & Setup -------------
|
|
12
12
|
|
|
@@ -15,7 +15,7 @@ const mockField = {
|
|
|
15
15
|
label: 'Test Field',
|
|
16
16
|
} as any;
|
|
17
17
|
|
|
18
|
-
const baseProps:
|
|
18
|
+
const baseProps: FieldWidgetComponentProps<any> = {
|
|
19
19
|
field: mockField,
|
|
20
20
|
value: undefined,
|
|
21
21
|
onChange: vi.fn(),
|
|
@@ -78,7 +78,7 @@ describe('Complex & Relationship Widgets', () => {
|
|
|
78
78
|
reference_field: 'name',
|
|
79
79
|
} as any;
|
|
80
80
|
|
|
81
|
-
const dynamicProps:
|
|
81
|
+
const dynamicProps: FieldWidgetComponentProps<any> = {
|
|
82
82
|
...baseProps,
|
|
83
83
|
field: dynamicField,
|
|
84
84
|
dataSource: mockDataSource,
|
|
@@ -3,7 +3,7 @@ import { describe, it, expect, vi } from 'vitest';
|
|
|
3
3
|
import { DateField } from './widgets/DateField';
|
|
4
4
|
import { DateTimeField } from './widgets/DateTimeField';
|
|
5
5
|
import { TimeField } from './widgets/TimeField';
|
|
6
|
-
import type {
|
|
6
|
+
import type { FieldWidgetComponentProps } from './widgets/types';
|
|
7
7
|
|
|
8
8
|
const mockField = {
|
|
9
9
|
name: 'test_field',
|
|
@@ -11,7 +11,7 @@ const mockField = {
|
|
|
11
11
|
type: 'date',
|
|
12
12
|
} as any;
|
|
13
13
|
|
|
14
|
-
const baseProps:
|
|
14
|
+
const baseProps: FieldWidgetComponentProps<string> = {
|
|
15
15
|
field: mockField,
|
|
16
16
|
value: '',
|
|
17
17
|
onChange: vi.fn(),
|
package/src/index.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import { Badge, Avatar, AvatarImage, AvatarFallback, Button, Checkbox, EmptyValu
|
|
|
14
14
|
import { Check, X, Copy, Phone as PhoneIcon, MapPin } from 'lucide-react';
|
|
15
15
|
import { useObjectTranslation } from '@object-ui/react';
|
|
16
16
|
import { SchemaRendererContext as _SchemaRendererContext } from '@object-ui/react';
|
|
17
|
+
import { withFieldCarrier } from './withFieldCarrier';
|
|
17
18
|
|
|
18
19
|
// Module-level cache so multiple renderers fetching the same lookup ID
|
|
19
20
|
// only trigger one network call. Keyed by `${objectName}:${id}`.
|
|
@@ -589,13 +590,42 @@ export function formatDateTime(value: string | Date | number): string {
|
|
|
589
590
|
});
|
|
590
591
|
}
|
|
591
592
|
|
|
593
|
+
/**
|
|
594
|
+
* Single-line cell value with a working ellipsis and a full-text fallback.
|
|
595
|
+
*
|
|
596
|
+
* `truncate` on a bare inline `<span>` never clips — an inline box has no
|
|
597
|
+
* width box for `overflow:hidden` / `text-overflow:ellipsis` to act on, so
|
|
598
|
+
* the value renders at full content width and the tail is silently cut by
|
|
599
|
+
* whatever ancestor happens to clip (the detail card edge), with no ellipsis
|
|
600
|
+
* (objectui#3466; same mechanism as the JSON cell in objectui#2578).
|
|
601
|
+
* Block-level + `max-w-full` gives the span its parent's width to truncate
|
|
602
|
+
* against, and as a flex item its `overflow:hidden` zeroes the automatic
|
|
603
|
+
* min-size so it can shrink below the text width. The `title` keeps the full
|
|
604
|
+
* text reachable on hover — host rows (detail sections) put their inline-edit
|
|
605
|
+
* hint in *their* `title`, so the value's full text must live on the value
|
|
606
|
+
* element itself, where it takes precedence under the cursor.
|
|
607
|
+
*/
|
|
608
|
+
function TruncatedText({
|
|
609
|
+
text,
|
|
610
|
+
className,
|
|
611
|
+
}: {
|
|
612
|
+
text: string;
|
|
613
|
+
className?: string;
|
|
614
|
+
}): React.ReactElement {
|
|
615
|
+
return (
|
|
616
|
+
<span className={cn('block max-w-full truncate', className)} title={text}>
|
|
617
|
+
{text}
|
|
618
|
+
</span>
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
592
622
|
/**
|
|
593
623
|
* Text field cell renderer
|
|
594
624
|
*/
|
|
595
625
|
export function TextCellRenderer({ value }: CellRendererProps): React.ReactElement {
|
|
596
626
|
const safe = coerceToSafeValue(value);
|
|
597
627
|
if (safe == null || safe === '') return <EmptyValue />;
|
|
598
|
-
return <
|
|
628
|
+
return <TruncatedText text={String(safe)} />;
|
|
599
629
|
}
|
|
600
630
|
|
|
601
631
|
/**
|
|
@@ -1089,10 +1119,13 @@ export function SelectCellRenderer({ value, field }: CellRendererProps): React.R
|
|
|
1089
1119
|
|| SEMANTIC_COLOR_MAP[String(val).toLowerCase().replace(/[\s-]/g, '_')]
|
|
1090
1120
|
|| hashToColor(String(val).toLowerCase().replace(/[\s-]/g, '_'));
|
|
1091
1121
|
const dotClass = DOT_COLOR_MAP[colorName] || DOT_COLOR_MAP.gray;
|
|
1122
|
+
// max-w-full bounds the (otherwise content-sized) inline-flex box so the
|
|
1123
|
+
// inner truncate can engage; title keeps the full label on hover
|
|
1124
|
+
// (objectui#3466, same class of bug as the badge branch below).
|
|
1092
1125
|
return (
|
|
1093
|
-
<span key={key} className="inline-flex items-center gap-1.5 text-sm">
|
|
1126
|
+
<span key={key} className="inline-flex max-w-full items-center gap-1.5 text-sm" title={label}>
|
|
1094
1127
|
<span className={cn('h-1.5 w-1.5 rounded-full shrink-0', dotClass)} aria-hidden="true" />
|
|
1095
|
-
<span className="truncate">{label}</span>
|
|
1128
|
+
<span className="min-w-0 truncate">{label}</span>
|
|
1096
1129
|
</span>
|
|
1097
1130
|
);
|
|
1098
1131
|
}
|
|
@@ -1266,7 +1299,7 @@ export function FileCellRenderer({ value, field }: CellRendererProps): React.Rea
|
|
|
1266
1299
|
}
|
|
1267
1300
|
|
|
1268
1301
|
const fileName = value.name || value.original_name || 'File';
|
|
1269
|
-
return <
|
|
1302
|
+
return <TruncatedText text={String(fileName)} className="text-sm" />;
|
|
1270
1303
|
}
|
|
1271
1304
|
|
|
1272
1305
|
/**
|
|
@@ -1435,7 +1468,7 @@ export function LookupCellRenderer({ value, field }: CellRendererProps): React.R
|
|
|
1435
1468
|
String(parsed.externalId ?? parsed.id ?? parsed._id ?? '');
|
|
1436
1469
|
}
|
|
1437
1470
|
} catch { /* not JSON — fall through to normal resolution */ }
|
|
1438
|
-
if (parsedDisplay) return <
|
|
1471
|
+
if (parsedDisplay) return <TruncatedText text={parsedDisplay} />;
|
|
1439
1472
|
}
|
|
1440
1473
|
}
|
|
1441
1474
|
|
|
@@ -1446,7 +1479,7 @@ export function LookupCellRenderer({ value, field }: CellRendererProps): React.R
|
|
|
1446
1479
|
const display =
|
|
1447
1480
|
resolveLookupRecordName(obj, refSchema, displayField) || String(obj.id || obj._id || '');
|
|
1448
1481
|
if (display) {
|
|
1449
|
-
return <
|
|
1482
|
+
return <TruncatedText text={display} />;
|
|
1450
1483
|
}
|
|
1451
1484
|
}
|
|
1452
1485
|
|
|
@@ -1506,12 +1539,12 @@ export function LookupCellRenderer({ value, field }: CellRendererProps): React.R
|
|
|
1506
1539
|
const label =
|
|
1507
1540
|
resolveLookupRecordName(value as Record<string, unknown>, refSchema, displayField) ||
|
|
1508
1541
|
String((value as any).id || (value as any)._id || '[Object]');
|
|
1509
|
-
return <
|
|
1542
|
+
return <TruncatedText text={label} />;
|
|
1510
1543
|
}
|
|
1511
1544
|
|
|
1512
1545
|
// Primitive value (e.g. raw ID): try options → resolver → opaque-ID placeholder → raw
|
|
1513
1546
|
const { text, muted } = resolveLabel(value);
|
|
1514
|
-
return <
|
|
1547
|
+
return <TruncatedText text={text} className={muted ? 'text-muted-foreground' : undefined} />;
|
|
1515
1548
|
}
|
|
1516
1549
|
|
|
1517
1550
|
/**
|
|
@@ -1535,7 +1568,7 @@ export function UserCellRenderer({ value }: CellRendererProps): React.ReactEleme
|
|
|
1535
1568
|
|
|
1536
1569
|
// Primitive value: just display the ID/username as text
|
|
1537
1570
|
if (typeof value !== 'object') {
|
|
1538
|
-
return <
|
|
1571
|
+
return <TruncatedText text={String(value)} />;
|
|
1539
1572
|
}
|
|
1540
1573
|
|
|
1541
1574
|
if (Array.isArray(value)) {
|
|
@@ -1544,11 +1577,7 @@ export function UserCellRenderer({ value }: CellRendererProps): React.ReactEleme
|
|
|
1544
1577
|
{value.slice(0, 3).map((user, idx) => {
|
|
1545
1578
|
// Primitive user in array
|
|
1546
1579
|
if (typeof user !== 'object' || user === null) {
|
|
1547
|
-
return (
|
|
1548
|
-
<span key={idx} className="truncate text-sm">
|
|
1549
|
-
{String(user)}
|
|
1550
|
-
</span>
|
|
1551
|
-
);
|
|
1580
|
+
return <TruncatedText key={idx} text={String(user)} className="text-sm" />;
|
|
1552
1581
|
}
|
|
1553
1582
|
const name = user.name || user.username || 'User';
|
|
1554
1583
|
const initials = name.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2);
|
|
@@ -1588,7 +1617,7 @@ export function UserCellRenderer({ value }: CellRendererProps): React.ReactEleme
|
|
|
1588
1617
|
{initials}
|
|
1589
1618
|
</AvatarFallback>
|
|
1590
1619
|
</Avatar>
|
|
1591
|
-
<
|
|
1620
|
+
<TruncatedText text={String(name)} />
|
|
1592
1621
|
</div>
|
|
1593
1622
|
);
|
|
1594
1623
|
}
|
|
@@ -1670,13 +1699,10 @@ export function JsonCellRenderer({ value }: CellRendererProps): React.ReactEleme
|
|
|
1670
1699
|
} else {
|
|
1671
1700
|
text = String(value);
|
|
1672
1701
|
}
|
|
1673
|
-
//
|
|
1674
|
-
//
|
|
1675
|
-
//
|
|
1676
|
-
|
|
1677
|
-
// spills into the neighbouring column (objectui#2578). The title keeps the
|
|
1678
|
-
// full value on hover.
|
|
1679
|
-
return <span className="block max-w-full font-mono text-xs text-gray-600 truncate" title={text}>{text}</span>;
|
|
1702
|
+
// The original site of the block-level+max-w-full+title pattern
|
|
1703
|
+
// (objectui#2578) — now shared with every single-line value renderer via
|
|
1704
|
+
// TruncatedText (objectui#3466).
|
|
1705
|
+
return <TruncatedText text={text} className="font-mono text-xs text-gray-600" />;
|
|
1680
1706
|
}
|
|
1681
1707
|
|
|
1682
1708
|
/**
|
|
@@ -2048,7 +2074,9 @@ function createFieldRenderer(FieldWidget: React.ComponentType<any>) {
|
|
|
2048
2074
|
readonly: schema?.readonly || schema?.readOnly,
|
|
2049
2075
|
help: schema?.help,
|
|
2050
2076
|
description: schema?.description,
|
|
2051
|
-
|
|
2077
|
+
// `??`, not `||` — an authored default of `false` / `0` / `''` is a value,
|
|
2078
|
+
// and `||` silently threw it away (same falsy-as-empty class as cloud#972).
|
|
2079
|
+
defaultValue: schema?.defaultValue ?? schema?.value,
|
|
2052
2080
|
...schema,
|
|
2053
2081
|
};
|
|
2054
2082
|
|
|
@@ -2275,9 +2303,12 @@ export function registerField(fieldType: string): void {
|
|
|
2275
2303
|
|
|
2276
2304
|
// Create lazy component
|
|
2277
2305
|
const LazyFieldWidget = React.lazy(loader);
|
|
2278
|
-
|
|
2279
|
-
// Register with field namespace
|
|
2280
|
-
|
|
2306
|
+
|
|
2307
|
+
// Register with field namespace. No LAYOUT wrapper — the form renderer owns
|
|
2308
|
+
// label/description/spacing. The only thing wrapped is the metadata carrier:
|
|
2309
|
+
// `withFieldCarrier` maps `SchemaRenderer`'s `schema` node onto `field`
|
|
2310
|
+
// (objectui#3233) and renders nothing of its own.
|
|
2311
|
+
ComponentRegistry.register(fieldType, withFieldCarrier(LazyFieldWidget), {
|
|
2281
2312
|
namespace: 'field',
|
|
2282
2313
|
skipFallback: FIELD_TYPES_SKIP_FALLBACK.has(fieldType),
|
|
2283
2314
|
});
|
|
@@ -2351,7 +2382,7 @@ export function registerFields() {
|
|
|
2351
2382
|
// field type. Registered WITHOUT createFieldRenderer: it is a fully-controlled
|
|
2352
2383
|
// widget (value/onChange from RHF), so the wrapper's extra label + local value
|
|
2353
2384
|
// buffer would only duplicate the form's FormLabel and desync the value.
|
|
2354
|
-
ComponentRegistry.register('capability-multiselect', CapabilityMultiSelectField, { namespace: 'field', skipFallback: true });
|
|
2385
|
+
ComponentRegistry.register('capability-multiselect', withFieldCarrier(CapabilityMultiSelectField), { namespace: 'field', skipFallback: true });
|
|
2355
2386
|
// master_detail = child-side FK lookup (single value, typically required). See
|
|
2356
2387
|
// fieldWidgetMap above for rationale.
|
|
2357
2388
|
ComponentRegistry.register('master_detail', createFieldRenderer(LookupField), { namespace: 'field' });
|
|
@@ -2421,6 +2452,11 @@ export * from './widgets/ObjectRefField';
|
|
|
2421
2452
|
export * from './widgets/FilterConditionField';
|
|
2422
2453
|
export * from './widgets/RecipientPickerField';
|
|
2423
2454
|
export * from './widgets/RecordPickerDialog';
|
|
2455
|
+
// Shared picker-column derivation (ADR-0085 highlightFields → displayFields →
|
|
2456
|
+
// schema walk) — consumed by LookupField AND by RelatedList's Add-picker so a
|
|
2457
|
+
// lookup picker and a related-list Add dialog of the same object agree on
|
|
2458
|
+
// columns (#3365).
|
|
2459
|
+
export * from './widgets/deriveLookupColumns';
|
|
2424
2460
|
export * from './widgets/FileField';
|
|
2425
2461
|
export * from './widgets/ImageField';
|
|
2426
2462
|
export { ImageCropperDialog } from './widgets/ImageCropperDialog';
|
|
@@ -2449,5 +2485,19 @@ export * from './widgets/RadioField';
|
|
|
2449
2485
|
export * from './widgets/CheckboxesField';
|
|
2450
2486
|
export * from './widgets/TagsField';
|
|
2451
2487
|
|
|
2488
|
+
// The SDUI-node → `field` adapter every registration here goes through
|
|
2489
|
+
// (objectui#3233). Exported so a widget registered OUTSIDE this repo can reach
|
|
2490
|
+
// the same single-carrier guarantee instead of re-growing a `field || schema`
|
|
2491
|
+
// read of its own.
|
|
2492
|
+
export { withFieldCarrier } from './withFieldCarrier';
|
|
2493
|
+
|
|
2494
|
+
// The whitelist that decides what a widget's `...props` spread may put on a
|
|
2495
|
+
// DOM element (objectui#3291) — the runtime executor of the "DOM pass-through"
|
|
2496
|
+
// block of `FieldWidgetComponentProps`. Exported for the same reason as
|
|
2497
|
+
// `withFieldCarrier` above: a widget authored outside this repo needs to reach
|
|
2498
|
+
// it, or it re-grows the bare spread this closed.
|
|
2499
|
+
export { toDomProps } from './widgets/toDomProps';
|
|
2500
|
+
export type { DomProps } from './widgets/toDomProps';
|
|
2501
|
+
|
|
2452
2502
|
// Initialize registry
|
|
2453
2503
|
registerAllFields();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useId } from 'react';
|
|
2
2
|
import { Input, Label, EmptyValue } from '@object-ui/components';
|
|
3
|
-
import {
|
|
3
|
+
import { FieldWidgetComponentProps } from './types';
|
|
4
|
+
import { toDomProps } from './toDomProps';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Address data structure
|
|
@@ -17,8 +18,20 @@ export interface AddressValue {
|
|
|
17
18
|
* Address field widget - provides a structured address input
|
|
18
19
|
* Supports street, city, state, zip code, and country
|
|
19
20
|
*/
|
|
20
|
-
export function AddressField({ value, onChange, field, readonly, ...props }:
|
|
21
|
+
export function AddressField({ value, onChange, field, readonly, error, ...props }: FieldWidgetComponentProps<AddressValue>) {
|
|
21
22
|
const address = value || {};
|
|
23
|
+
// DOM pass-through (objectui#3318): the whitelist spread goes onto the FIRST
|
|
24
|
+
// sub-input (street) — one carrier for the form renderer's aria-describedby;
|
|
25
|
+
// the composite's validation state goes onto EVERY focusable sub-input via
|
|
26
|
+
// `aria-invalid={!!error}` (a form-level failure means the whole address is
|
|
27
|
+
// missing/invalid, and whichever sub-input the user reaches must announce it).
|
|
28
|
+
const domProps = toDomProps(props);
|
|
29
|
+
// Sub-input ids (objectui#3343): `useId()` prefix + sub-field name — the
|
|
30
|
+
// `groupId` paradigm of RadioField / CheckboxesField. Hardcoded literals
|
|
31
|
+
// ("street", "city", …) collide as soon as a form renders two address
|
|
32
|
+
// fields, and every label's htmlFor then resolves to the FIRST match.
|
|
33
|
+
const groupId = useId();
|
|
34
|
+
const subId = (name: keyof AddressValue) => `${groupId}-${name}`;
|
|
22
35
|
|
|
23
36
|
const handleFieldChange = (fieldName: keyof AddressValue, fieldValue: string) => {
|
|
24
37
|
onChange({
|
|
@@ -45,66 +58,72 @@ export function AddressField({ value, onChange, field, readonly, ...props }: Fie
|
|
|
45
58
|
return (
|
|
46
59
|
<div className="space-y-3">
|
|
47
60
|
<div>
|
|
48
|
-
<Label htmlFor=
|
|
61
|
+
<Label htmlFor={subId('street')} className="text-xs">Street Address</Label>
|
|
49
62
|
<Input
|
|
50
|
-
|
|
63
|
+
{...domProps}
|
|
64
|
+
id={subId('street')}
|
|
51
65
|
type="text"
|
|
52
66
|
value={address.street || ''}
|
|
53
67
|
onChange={(e) => handleFieldChange('street', e.target.value)}
|
|
54
68
|
placeholder="123 Main St"
|
|
55
69
|
disabled={readonly || props.disabled}
|
|
56
70
|
className={props.className}
|
|
71
|
+
aria-invalid={!!error}
|
|
57
72
|
/>
|
|
58
73
|
</div>
|
|
59
74
|
|
|
60
75
|
<div className="grid grid-cols-2 gap-3">
|
|
61
76
|
<div>
|
|
62
|
-
<Label htmlFor=
|
|
77
|
+
<Label htmlFor={subId('city')} className="text-xs">City</Label>
|
|
63
78
|
<Input
|
|
64
|
-
id=
|
|
79
|
+
id={subId('city')}
|
|
65
80
|
type="text"
|
|
66
81
|
value={address.city || ''}
|
|
67
82
|
onChange={(e) => handleFieldChange('city', e.target.value)}
|
|
68
83
|
placeholder="San Francisco"
|
|
69
84
|
disabled={readonly || props.disabled}
|
|
85
|
+
aria-invalid={!!error}
|
|
70
86
|
/>
|
|
71
87
|
</div>
|
|
72
88
|
|
|
73
89
|
<div>
|
|
74
|
-
<Label htmlFor=
|
|
90
|
+
<Label htmlFor={subId('state')} className="text-xs">State / Province</Label>
|
|
75
91
|
<Input
|
|
76
|
-
id=
|
|
92
|
+
id={subId('state')}
|
|
77
93
|
type="text"
|
|
78
94
|
value={address.state || ''}
|
|
79
95
|
onChange={(e) => handleFieldChange('state', e.target.value)}
|
|
80
96
|
placeholder="CA"
|
|
81
97
|
disabled={readonly || props.disabled}
|
|
98
|
+
aria-invalid={!!error}
|
|
82
99
|
/>
|
|
83
100
|
</div>
|
|
84
101
|
</div>
|
|
85
102
|
|
|
86
103
|
<div className="grid grid-cols-2 gap-3">
|
|
87
104
|
<div>
|
|
88
|
-
<Label htmlFor=
|
|
105
|
+
<Label htmlFor={subId('zipCode')} className="text-xs">ZIP / Postal Code</Label>
|
|
89
106
|
<Input
|
|
90
|
-
id=
|
|
107
|
+
id={subId('zipCode')}
|
|
91
108
|
type="text"
|
|
92
109
|
value={address.zipCode || ''}
|
|
93
110
|
onChange={(e) => handleFieldChange('zipCode', e.target.value)}
|
|
94
111
|
placeholder="94102"
|
|
95
112
|
disabled={readonly || props.disabled}
|
|
113
|
+
aria-invalid={!!error}
|
|
96
114
|
/>
|
|
97
115
|
</div>
|
|
98
116
|
|
|
99
117
|
<div>
|
|
100
|
-
<Label htmlFor=
|
|
118
|
+
<Label htmlFor={subId('country')} className="text-xs">Country</Label>
|
|
101
119
|
<Input
|
|
102
|
-
id=
|
|
120
|
+
id={subId('country')}
|
|
103
121
|
type="text"
|
|
104
122
|
value={address.country || ''}
|
|
105
123
|
onChange={(e) => handleFieldChange('country', e.target.value)}
|
|
106
124
|
placeholder="United States"
|
|
107
125
|
disabled={readonly || props.disabled}
|
|
126
|
+
aria-invalid={!!error}
|
|
108
127
|
/>
|
|
109
128
|
</div>
|
|
110
129
|
</div>
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* Unique sub-input ids for the composite `address` widget (objectui#3343).
|
|
11
|
+
*
|
|
12
|
+
* The widget used to hardcode literal ids ("street", "city", "state",
|
|
13
|
+
* "zipCode", "country") on its sub-inputs. Two address fields in one form —
|
|
14
|
+
* e.g. billing + shipping — then produced duplicate DOM ids, and every
|
|
15
|
+
* `Label htmlFor` resolved to the FIRST match in the document: each sub-label
|
|
16
|
+
* of the second field clicked/announced the first field's input.
|
|
17
|
+
*
|
|
18
|
+
* The fix follows the `groupId` paradigm of RadioField / CheckboxesField:
|
|
19
|
+
* a `useId()` prefix + the sub-field name. These tests pin it by rendering
|
|
20
|
+
* TWO instances in one form:
|
|
21
|
+
*
|
|
22
|
+
* 1. every `[id]` in the document is globally unique (red again if anyone
|
|
23
|
+
* reverts to hardcoded literals — both instances would emit id="street");
|
|
24
|
+
* 2. each sub-label of the SECOND instance resolves — via the exact
|
|
25
|
+
* mechanism browsers use for label activation, `for` → getElementById —
|
|
26
|
+
* to an input inside its OWN instance, and focusing it lands there.
|
|
27
|
+
*/
|
|
28
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
29
|
+
import React from 'react';
|
|
30
|
+
import { render, within } from '@testing-library/react';
|
|
31
|
+
import '@testing-library/jest-dom';
|
|
32
|
+
import { AddressField } from './AddressField';
|
|
33
|
+
|
|
34
|
+
const field = { name: 'address', type: 'address' } as any;
|
|
35
|
+
|
|
36
|
+
const SUB_LABELS = [
|
|
37
|
+
'Street Address',
|
|
38
|
+
'City',
|
|
39
|
+
'State / Province',
|
|
40
|
+
'ZIP / Postal Code',
|
|
41
|
+
'Country',
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
function renderTwoInOneForm() {
|
|
45
|
+
return render(
|
|
46
|
+
<form>
|
|
47
|
+
<div data-testid="address-a">
|
|
48
|
+
<AddressField value={{}} onChange={vi.fn()} field={field} />
|
|
49
|
+
</div>
|
|
50
|
+
<div data-testid="address-b">
|
|
51
|
+
<AddressField value={{}} onChange={vi.fn()} field={field} />
|
|
52
|
+
</div>
|
|
53
|
+
</form>,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('AddressField — unique sub-input ids (objectui#3343)', () => {
|
|
58
|
+
it('two address fields in one form produce globally unique DOM ids', () => {
|
|
59
|
+
const { baseElement } = renderTwoInOneForm();
|
|
60
|
+
const ids = Array.from(baseElement.querySelectorAll('[id]')).map((el) => el.id);
|
|
61
|
+
// 5 sub-inputs per instance — both instances must actually render ids.
|
|
62
|
+
expect(ids.length).toBeGreaterThanOrEqual(10);
|
|
63
|
+
const duplicates = ids.filter((id, i) => ids.indexOf(id) !== i);
|
|
64
|
+
expect(duplicates).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("each sub-label of the SECOND field focuses that field's own input", () => {
|
|
68
|
+
const { getByTestId } = renderTwoInOneForm();
|
|
69
|
+
const first = getByTestId('address-a');
|
|
70
|
+
const second = getByTestId('address-b');
|
|
71
|
+
|
|
72
|
+
for (const text of SUB_LABELS) {
|
|
73
|
+
const label = within(second).getByText(text);
|
|
74
|
+
const forId = label.getAttribute('for');
|
|
75
|
+
expect(forId, `label "${text}" must carry htmlFor`).toBeTruthy();
|
|
76
|
+
|
|
77
|
+
// Exactly what the browser does on label click / SR announcement:
|
|
78
|
+
// resolve `for` against the document.
|
|
79
|
+
const control = document.getElementById(forId!);
|
|
80
|
+
expect(control, `htmlFor of "${text}" must resolve`).not.toBeNull();
|
|
81
|
+
expect(control!.tagName).toBe('INPUT');
|
|
82
|
+
expect(
|
|
83
|
+
second.contains(control),
|
|
84
|
+
`"${text}" must resolve into its OWN field, not the first one`,
|
|
85
|
+
).toBe(true);
|
|
86
|
+
expect(first.contains(control)).toBe(false);
|
|
87
|
+
|
|
88
|
+
(control as HTMLInputElement).focus();
|
|
89
|
+
expect(control).toHaveFocus();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { EmptyValue } from '@object-ui/components';
|
|
3
|
-
import {
|
|
3
|
+
import { FieldWidgetComponentProps } from './types';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* AutoNumberField - Read-only auto-generated field
|
|
7
7
|
* Values are automatically generated by the system and cannot be edited
|
|
8
8
|
*/
|
|
9
|
-
export function AutoNumberField({ value, field, ...props }:
|
|
9
|
+
export function AutoNumberField({ value, field, ...props }: FieldWidgetComponentProps<string | number>) {
|
|
10
10
|
if (value == null) {
|
|
11
11
|
return <EmptyValue className={props.className} />;
|
|
12
12
|
}
|