@object-ui/fields 17.3.0 → 17.6.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 +1655 -0
- package/README.md +1 -12
- package/dist/FieldEditWidget.d.ts +23 -3
- package/dist/currency.d.ts +46 -0
- package/dist/field-type-alias.d.ts +35 -1
- package/dist/index.cjs +4 -4
- package/dist/index.css +651 -0
- package/dist/index.d.ts +474 -85
- package/dist/index.js +5418 -5023
- package/dist/widgets/AddressField.d.ts +4 -12
- package/dist/widgets/AutoNumberField.d.ts +1 -1
- package/dist/widgets/AvatarField.d.ts +1 -1
- package/dist/widgets/BooleanField.d.ts +1 -1
- 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 +1 -1
- package/dist/widgets/DateTimeField.d.ts +1 -1
- package/dist/widgets/EmailField.d.ts +1 -1
- package/dist/widgets/FileField.d.ts +18 -2
- package/dist/widgets/FilterConditionField.d.ts +28 -1
- package/dist/widgets/FormulaField.d.ts +1 -1
- package/dist/widgets/FullscreenFieldEditor.d.ts +49 -152
- package/dist/widgets/GeolocationField.d.ts +1 -1
- package/dist/widgets/GridField.d.ts +68 -6
- 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/MasterDetailField.d.ts +1 -1
- package/dist/widgets/MultiSelectField.d.ts +1 -1
- package/dist/widgets/NumberField.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 +21 -1
- package/dist/widgets/PasswordField.d.ts +1 -1
- 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 +34 -10
- package/dist/widgets/RichTextField.d.ts +37 -1
- package/dist/widgets/SelectField.d.ts +26 -6
- 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 +1 -1
- package/dist/widgets/TimeField.d.ts +1 -1
- 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 +88 -0
- package/dist/widgets/file-size-guard.d.ts +41 -0
- package/dist/widgets/lookupColumnDisplay.d.ts +93 -0
- package/dist/widgets/nativeDateValue.d.ts +37 -0
- package/dist/widgets/richTextDisplay.d.ts +80 -0
- package/dist/widgets/toDomProps.d.ts +30 -10
- package/dist/widgets/toHostGroupProps.d.ts +189 -0
- package/dist/widgets/types.d.ts +8 -3
- package/dist/widgets/useRecordQuery.d.ts +7 -1
- package/package.json +17 -14
- 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__/cell-truncation.test.d.ts +0 -0
- package/dist/__tests__/field-carrier-sdui.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-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 +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/AddressField.uniqueIds.test.d.ts +0 -0
- package/dist/widgets/CapabilityMultiSelectField.test.d.ts +0 -0
- package/dist/widgets/CharacterCount.d.ts +0 -56
- package/dist/widgets/CheckboxesField.cascade.test.d.ts +0 -0
- package/dist/widgets/GeolocationField.uniqueIds.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.gateHintLabel.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/OptionsEmptyState.no-provider.test.d.ts +0 -0
- package/dist/widgets/OptionsEmptyState.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/RecordPickerDialog.filterOptions.test.d.ts +0 -8
- package/dist/widgets/RecordPickerDialog.selectLabel.test.d.ts +0 -8
- package/dist/widgets/SelectField.cascade.test.d.ts +0 -0
- 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/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/__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/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
- package/src/FieldEditWidget.test.ts +0 -133
- package/src/FieldEditWidget.tsx +0 -191
- package/src/__tests__/DateCellRenderer.test.tsx +0 -73
- package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
- package/src/__tests__/cell-truncation.test.tsx +0 -96
- package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
- package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
- package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
- package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
- package/src/__tests__/registry-collision.test.ts +0 -91
- package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
- 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 -598
- 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 -908
- package/src/currency.ts +0 -14
- package/src/datetime-widgets.test.tsx +0 -93
- package/src/field-type-alias.ts +0 -100
- package/src/field-type-coverage.test.ts +0 -67
- package/src/index.tsx +0 -2503
- package/src/standard-widgets.test.tsx +0 -357
- package/src/widgets/AddressField.tsx +0 -132
- package/src/widgets/AddressField.uniqueIds.test.tsx +0 -92
- package/src/widgets/AutoNumberField.tsx +0 -18
- package/src/widgets/AvatarField.tsx +0 -117
- package/src/widgets/BooleanField.tsx +0 -52
- package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
- package/src/widgets/CapabilityMultiSelectField.tsx +0 -229
- package/src/widgets/CharacterCount.tsx +0 -263
- package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
- package/src/widgets/CheckboxesField.tsx +0 -136
- package/src/widgets/CodeField.tsx +0 -41
- package/src/widgets/ColorField.tsx +0 -53
- package/src/widgets/CurrencyField.tsx +0 -95
- package/src/widgets/DateField.tsx +0 -31
- package/src/widgets/DateTimeField.tsx +0 -37
- package/src/widgets/EmailField.tsx +0 -37
- package/src/widgets/FileField.tsx +0 -494
- package/src/widgets/FilterConditionField.tsx +0 -454
- package/src/widgets/FormulaField.tsx +0 -33
- package/src/widgets/FullscreenFieldEditor.tsx +0 -271
- package/src/widgets/GeolocationField.tsx +0 -167
- package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
- package/src/widgets/GridField.test.tsx +0 -406
- package/src/widgets/GridField.tsx +0 -966
- package/src/widgets/ImageCropperDialog.tsx +0 -245
- package/src/widgets/ImageField.tsx +0 -255
- 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 -1291
- 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 -142
- 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 -83
- 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 -111
- package/src/widgets/RatingField.tsx +0 -81
- package/src/widgets/RecipientPickerField.test.tsx +0 -157
- package/src/widgets/RecipientPickerField.tsx +0 -188
- package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
- package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
- package/src/widgets/RecordPickerDialog.tsx +0 -1316
- package/src/widgets/RichTextField.tsx +0 -176
- package/src/widgets/SelectField.cascade.test.tsx +0 -138
- package/src/widgets/SelectField.tsx +0 -162
- 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 -58
- 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 -142
- 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/deriveLookupColumns.test.ts +0 -112
- package/src/widgets/deriveLookupColumns.ts +0 -108
- package/src/widgets/file-value.test.ts +0 -230
- package/src/widgets/file-value.ts +0 -246
- package/src/widgets/openNativePicker.ts +0 -18
- 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 -175
- package/src/widgets/types.ts +0 -242
- package/src/widgets/useCascadingOptions.ts +0 -46
- package/src/widgets/useFieldTranslation.ts +0 -150
- package/src/widgets/useRecordQuery.test.ts +0 -174
- package/src/widgets/useRecordQuery.ts +0 -264
- package/src/widgets/useUploadingSignal.test.tsx +0 -62
- package/src/widgets/useUploadingSignal.ts +0 -20
- package/src/withFieldCarrier.tsx +0 -75
|
@@ -1,357 +0,0 @@
|
|
|
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 { describe, it, expect, vi } from 'vitest';
|
|
10
|
-
import React from 'react';
|
|
11
|
-
import { render, screen, fireEvent } from '@testing-library/react';
|
|
12
|
-
import '@testing-library/jest-dom';
|
|
13
|
-
import { CurrencyField } from './widgets/CurrencyField';
|
|
14
|
-
import { NumberField } from './widgets/NumberField';
|
|
15
|
-
import { TextField } from './widgets/TextField';
|
|
16
|
-
import { SelectField } from './widgets/SelectField';
|
|
17
|
-
import { BooleanField } from './widgets/BooleanField';
|
|
18
|
-
import { DateField } from './widgets/DateField';
|
|
19
|
-
import { EmailField } from './widgets/EmailField';
|
|
20
|
-
import { UrlField } from './widgets/UrlField';
|
|
21
|
-
import { PhoneField } from './widgets/PhoneField';
|
|
22
|
-
import { TextAreaField } from './widgets/TextAreaField';
|
|
23
|
-
|
|
24
|
-
const mockOnChange = vi.fn();
|
|
25
|
-
|
|
26
|
-
describe('Standard Field Widgets', () => {
|
|
27
|
-
describe('CurrencyField', () => {
|
|
28
|
-
const fieldMock = { type: 'currency', currency: 'USD', precision: 2 };
|
|
29
|
-
|
|
30
|
-
it('should render value with currency symbol in input mode', () => {
|
|
31
|
-
render(
|
|
32
|
-
<CurrencyField
|
|
33
|
-
value={123.45}
|
|
34
|
-
onChange={mockOnChange}
|
|
35
|
-
field={fieldMock as any}
|
|
36
|
-
/>
|
|
37
|
-
);
|
|
38
|
-
const input = screen.getByRole('spinbutton'); // input type="number"
|
|
39
|
-
expect(input).toHaveValue(123.45);
|
|
40
|
-
expect(screen.getByText('$')).toBeInTheDocument();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('should format currency in readonly mode', () => {
|
|
44
|
-
const { container } = render(
|
|
45
|
-
<CurrencyField
|
|
46
|
-
value={1234.56}
|
|
47
|
-
onChange={mockOnChange}
|
|
48
|
-
field={fieldMock as any}
|
|
49
|
-
readonly={true}
|
|
50
|
-
/>
|
|
51
|
-
);
|
|
52
|
-
// Expected output format might depend on locale, matching generic USD format
|
|
53
|
-
expect(container.textContent).toMatch(/\$1,234.56/);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('should handle EUR currency', () => {
|
|
57
|
-
render(
|
|
58
|
-
<CurrencyField
|
|
59
|
-
value={100}
|
|
60
|
-
onChange={mockOnChange}
|
|
61
|
-
field={{ ...fieldMock, currency: 'EUR' } as any}
|
|
62
|
-
/>
|
|
63
|
-
);
|
|
64
|
-
expect(screen.getByText('EUR')).toBeInTheDocument();
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
describe('NumberField', () => {
|
|
69
|
-
const fieldMock = { type: 'number', precision: 2 };
|
|
70
|
-
|
|
71
|
-
it('should render numeric input', () => {
|
|
72
|
-
render(
|
|
73
|
-
<NumberField
|
|
74
|
-
value={42}
|
|
75
|
-
onChange={mockOnChange}
|
|
76
|
-
field={fieldMock as any}
|
|
77
|
-
/>
|
|
78
|
-
);
|
|
79
|
-
const input = screen.getByRole('spinbutton');
|
|
80
|
-
expect(input).toHaveValue(42);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('should call onChange with number', () => {
|
|
84
|
-
render(
|
|
85
|
-
<NumberField
|
|
86
|
-
value={1}
|
|
87
|
-
onChange={mockOnChange}
|
|
88
|
-
field={fieldMock as any}
|
|
89
|
-
/>
|
|
90
|
-
);
|
|
91
|
-
const input = screen.getByRole('spinbutton');
|
|
92
|
-
fireEvent.change(input, { target: { value: '10' } });
|
|
93
|
-
expect(mockOnChange).toHaveBeenCalledWith(10);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('should display value in readonly mode', () => {
|
|
97
|
-
const { container } = render(
|
|
98
|
-
<NumberField
|
|
99
|
-
value={99.9}
|
|
100
|
-
onChange={mockOnChange}
|
|
101
|
-
field={fieldMock as any}
|
|
102
|
-
readonly={true}
|
|
103
|
-
/>
|
|
104
|
-
);
|
|
105
|
-
expect(container.textContent).toBe('99.9');
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('should surface metadata min/max on the input (objectui#2572)', () => {
|
|
109
|
-
render(
|
|
110
|
-
<NumberField
|
|
111
|
-
value={5}
|
|
112
|
-
onChange={mockOnChange}
|
|
113
|
-
field={{ type: 'number', min: 0, max: 100 } as any}
|
|
114
|
-
/>
|
|
115
|
-
);
|
|
116
|
-
const input = screen.getByRole('spinbutton');
|
|
117
|
-
expect(input).toHaveAttribute('min', '0');
|
|
118
|
-
expect(input).toHaveAttribute('max', '100');
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('should step by 1 for scale: 0 (whole numbers), not "any"', () => {
|
|
122
|
-
render(
|
|
123
|
-
<NumberField
|
|
124
|
-
value={5}
|
|
125
|
-
onChange={mockOnChange}
|
|
126
|
-
field={{ type: 'number', scale: 0 } as any}
|
|
127
|
-
/>
|
|
128
|
-
);
|
|
129
|
-
expect(screen.getByRole('spinbutton')).toHaveAttribute('step', '1');
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('should honor an explicit metadata step over the scale-derived one', () => {
|
|
133
|
-
render(
|
|
134
|
-
<NumberField
|
|
135
|
-
value={5}
|
|
136
|
-
onChange={mockOnChange}
|
|
137
|
-
field={{ type: 'number', scale: 2, step: 5 } as any}
|
|
138
|
-
/>
|
|
139
|
-
);
|
|
140
|
-
expect(screen.getByRole('spinbutton')).toHaveAttribute('step', '5');
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
describe('TextField', () => {
|
|
145
|
-
const fieldMock = { type: 'text' };
|
|
146
|
-
|
|
147
|
-
it('should render text input', () => {
|
|
148
|
-
render(
|
|
149
|
-
<TextField
|
|
150
|
-
value="Hello"
|
|
151
|
-
onChange={mockOnChange}
|
|
152
|
-
field={fieldMock as any}
|
|
153
|
-
/>
|
|
154
|
-
);
|
|
155
|
-
const input = screen.getByRole('textbox');
|
|
156
|
-
expect(input).toHaveValue('Hello');
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('should render as password when type is password', () => {
|
|
160
|
-
render(
|
|
161
|
-
<TextField
|
|
162
|
-
value="secret"
|
|
163
|
-
onChange={mockOnChange}
|
|
164
|
-
field={{ type: 'password' } as any}
|
|
165
|
-
/>
|
|
166
|
-
);
|
|
167
|
-
// getByRole('textbox') doesn't work for password type usually
|
|
168
|
-
const input = screen.getByDisplayValue('secret');
|
|
169
|
-
expect(input).toHaveAttribute('type', 'password');
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it('should display value in readonly mode', () => {
|
|
173
|
-
const { container } = render(
|
|
174
|
-
<TextField
|
|
175
|
-
value="ReadOnly"
|
|
176
|
-
onChange={mockOnChange}
|
|
177
|
-
field={fieldMock as any}
|
|
178
|
-
readonly={true}
|
|
179
|
-
/>
|
|
180
|
-
);
|
|
181
|
-
expect(container.textContent).toBe('ReadOnly');
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
describe('SelectField', () => {
|
|
186
|
-
const fieldMock = {
|
|
187
|
-
type: 'select',
|
|
188
|
-
options: [
|
|
189
|
-
{ label: 'Option A', value: 'a' },
|
|
190
|
-
{ label: 'Option B', value: 'b' }
|
|
191
|
-
]
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
it('should render select trigger (Combobox/Select)', () => {
|
|
195
|
-
render(
|
|
196
|
-
<SelectField
|
|
197
|
-
value={''}
|
|
198
|
-
onChange={mockOnChange}
|
|
199
|
-
field={fieldMock as any}
|
|
200
|
-
/>
|
|
201
|
-
);
|
|
202
|
-
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it('should display label in readonly mode', () => {
|
|
206
|
-
const { container } = render(
|
|
207
|
-
<SelectField
|
|
208
|
-
value="a"
|
|
209
|
-
onChange={mockOnChange}
|
|
210
|
-
field={fieldMock as any}
|
|
211
|
-
readonly={true}
|
|
212
|
-
/>
|
|
213
|
-
);
|
|
214
|
-
expect(container.textContent).toBe('Option A');
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
// A `select` declared `multiple` is a multi-value picker (spec allows
|
|
218
|
-
// `multiple` on select). SelectField delegates to the chip picker so the
|
|
219
|
-
// form, inline editor, and ActionParamDialog all get multi-select from the
|
|
220
|
-
// one `select` widget — no single-select fallback, no per-surface drift.
|
|
221
|
-
it('renders the multi-value chip picker when field.multiple is set', () => {
|
|
222
|
-
render(
|
|
223
|
-
<SelectField
|
|
224
|
-
value={['a']}
|
|
225
|
-
onChange={mockOnChange}
|
|
226
|
-
field={{ ...fieldMock, multiple: true } as any}
|
|
227
|
-
/>
|
|
228
|
-
);
|
|
229
|
-
// Chip picker uses toggle buttons (aria-pressed), not a Radix combobox.
|
|
230
|
-
expect(screen.queryByRole('combobox')).not.toBeInTheDocument();
|
|
231
|
-
const optionA = screen.getByRole('button', { name: 'Option A' });
|
|
232
|
-
expect(optionA).toHaveAttribute('aria-pressed', 'true');
|
|
233
|
-
expect(screen.getByRole('button', { name: 'Option B' })).toHaveAttribute('aria-pressed', 'false');
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it('multi-select toggles values as an array', () => {
|
|
237
|
-
render(
|
|
238
|
-
<SelectField
|
|
239
|
-
value={['a']}
|
|
240
|
-
onChange={mockOnChange}
|
|
241
|
-
field={{ ...fieldMock, multiple: true } as any}
|
|
242
|
-
/>
|
|
243
|
-
);
|
|
244
|
-
fireEvent.click(screen.getByRole('button', { name: 'Option B' }));
|
|
245
|
-
expect(mockOnChange).toHaveBeenCalledWith(['a', 'b']);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it('multi-select renders selected values as badges in readonly mode', () => {
|
|
249
|
-
const { container } = render(
|
|
250
|
-
<SelectField
|
|
251
|
-
value={['a', 'b']}
|
|
252
|
-
onChange={mockOnChange}
|
|
253
|
-
field={{ ...fieldMock, multiple: true } as any}
|
|
254
|
-
readonly={true}
|
|
255
|
-
/>
|
|
256
|
-
);
|
|
257
|
-
expect(container.textContent).toContain('Option A');
|
|
258
|
-
expect(container.textContent).toContain('Option B');
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
describe('BooleanField', () => {
|
|
263
|
-
const fieldMock = { type: 'boolean' };
|
|
264
|
-
|
|
265
|
-
it('should render checkbox/switch', () => {
|
|
266
|
-
render(
|
|
267
|
-
<BooleanField
|
|
268
|
-
value={true}
|
|
269
|
-
onChange={mockOnChange}
|
|
270
|
-
field={fieldMock as any}
|
|
271
|
-
/>
|
|
272
|
-
);
|
|
273
|
-
// Ensure we can find the switch role (Shadcn switch uses role="switch")
|
|
274
|
-
expect(screen.getByRole('switch')).toBeChecked();
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it('should toggle value', () => {
|
|
278
|
-
render(
|
|
279
|
-
<BooleanField
|
|
280
|
-
value={false}
|
|
281
|
-
onChange={mockOnChange}
|
|
282
|
-
field={fieldMock as any}
|
|
283
|
-
/>
|
|
284
|
-
);
|
|
285
|
-
const toggle = screen.getByRole('switch');
|
|
286
|
-
fireEvent.click(toggle);
|
|
287
|
-
expect(mockOnChange).toHaveBeenCalledWith(true);
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it('should display Yes/No in readonly mode', () => {
|
|
291
|
-
const { container } = render(
|
|
292
|
-
<BooleanField
|
|
293
|
-
value={true}
|
|
294
|
-
onChange={mockOnChange}
|
|
295
|
-
field={fieldMock as any}
|
|
296
|
-
readonly={true}
|
|
297
|
-
/>
|
|
298
|
-
);
|
|
299
|
-
expect(container.textContent).toBe('Yes');
|
|
300
|
-
});
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
describe('EmailField', () => {
|
|
304
|
-
it('should render email input', () => {
|
|
305
|
-
render(
|
|
306
|
-
<EmailField
|
|
307
|
-
value="test@example.com"
|
|
308
|
-
onChange={mockOnChange}
|
|
309
|
-
field={{ type: 'email' } as any}
|
|
310
|
-
/>
|
|
311
|
-
);
|
|
312
|
-
const input = screen.getByDisplayValue('test@example.com');
|
|
313
|
-
expect(input).toHaveAttribute('type', 'email');
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
it('should render as link in readonly mode', () => {
|
|
317
|
-
render(
|
|
318
|
-
<EmailField
|
|
319
|
-
value="test@example.com"
|
|
320
|
-
onChange={mockOnChange}
|
|
321
|
-
field={{ type: 'email' } as any}
|
|
322
|
-
readonly={true}
|
|
323
|
-
/>
|
|
324
|
-
);
|
|
325
|
-
const link = screen.getByRole('link');
|
|
326
|
-
expect(link).toHaveAttribute('href', 'mailto:test@example.com');
|
|
327
|
-
expect(link).toHaveTextContent('test@example.com');
|
|
328
|
-
});
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
describe('UrlField', () => {
|
|
332
|
-
it('should render url input', () => {
|
|
333
|
-
render(
|
|
334
|
-
<UrlField
|
|
335
|
-
value="https://example.com"
|
|
336
|
-
onChange={mockOnChange}
|
|
337
|
-
field={{ type: 'url' } as any}
|
|
338
|
-
/>
|
|
339
|
-
);
|
|
340
|
-
const input = screen.getByDisplayValue('https://example.com');
|
|
341
|
-
expect(input).toHaveAttribute('type', 'url');
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
it('should render clickable link in readonly mode', () => {
|
|
345
|
-
render(
|
|
346
|
-
<UrlField
|
|
347
|
-
value="https://example.com"
|
|
348
|
-
onChange={mockOnChange}
|
|
349
|
-
field={{ type: 'url' } as any}
|
|
350
|
-
readonly={true}
|
|
351
|
-
/>
|
|
352
|
-
);
|
|
353
|
-
const link = screen.getByRole('link');
|
|
354
|
-
expect(link).toHaveAttribute('href', 'https://example.com');
|
|
355
|
-
});
|
|
356
|
-
});
|
|
357
|
-
});
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import React, { useId } from 'react';
|
|
2
|
-
import { Input, Label, EmptyValue } from '@object-ui/components';
|
|
3
|
-
import { FieldWidgetComponentProps } from './types';
|
|
4
|
-
import { toDomProps } from './toDomProps';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Address data structure
|
|
8
|
-
*/
|
|
9
|
-
export interface AddressValue {
|
|
10
|
-
street?: string;
|
|
11
|
-
city?: string;
|
|
12
|
-
state?: string;
|
|
13
|
-
zipCode?: string;
|
|
14
|
-
country?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Address field widget - provides a structured address input
|
|
19
|
-
* Supports street, city, state, zip code, and country
|
|
20
|
-
*/
|
|
21
|
-
export function AddressField({ value, onChange, field, readonly, error, ...props }: FieldWidgetComponentProps<AddressValue>) {
|
|
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}`;
|
|
35
|
-
|
|
36
|
-
const handleFieldChange = (fieldName: keyof AddressValue, fieldValue: string) => {
|
|
37
|
-
onChange({
|
|
38
|
-
...address,
|
|
39
|
-
[fieldName]: fieldValue,
|
|
40
|
-
});
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const formatAddress = (addr: AddressValue): string => {
|
|
44
|
-
const parts = [
|
|
45
|
-
addr.street,
|
|
46
|
-
addr.city,
|
|
47
|
-
[addr.state, addr.zipCode].filter(Boolean).join(' '),
|
|
48
|
-
addr.country,
|
|
49
|
-
].filter(Boolean);
|
|
50
|
-
return parts.join(', ');
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
if (readonly) {
|
|
54
|
-
const formatted = formatAddress(address);
|
|
55
|
-
return formatted ? <span className="text-sm">{formatted}</span> : <EmptyValue />;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return (
|
|
59
|
-
<div className="space-y-3">
|
|
60
|
-
<div>
|
|
61
|
-
<Label htmlFor={subId('street')} className="text-xs">Street Address</Label>
|
|
62
|
-
<Input
|
|
63
|
-
{...domProps}
|
|
64
|
-
id={subId('street')}
|
|
65
|
-
type="text"
|
|
66
|
-
value={address.street || ''}
|
|
67
|
-
onChange={(e) => handleFieldChange('street', e.target.value)}
|
|
68
|
-
placeholder="123 Main St"
|
|
69
|
-
disabled={readonly || props.disabled}
|
|
70
|
-
className={props.className}
|
|
71
|
-
aria-invalid={!!error}
|
|
72
|
-
/>
|
|
73
|
-
</div>
|
|
74
|
-
|
|
75
|
-
<div className="grid grid-cols-2 gap-3">
|
|
76
|
-
<div>
|
|
77
|
-
<Label htmlFor={subId('city')} className="text-xs">City</Label>
|
|
78
|
-
<Input
|
|
79
|
-
id={subId('city')}
|
|
80
|
-
type="text"
|
|
81
|
-
value={address.city || ''}
|
|
82
|
-
onChange={(e) => handleFieldChange('city', e.target.value)}
|
|
83
|
-
placeholder="San Francisco"
|
|
84
|
-
disabled={readonly || props.disabled}
|
|
85
|
-
aria-invalid={!!error}
|
|
86
|
-
/>
|
|
87
|
-
</div>
|
|
88
|
-
|
|
89
|
-
<div>
|
|
90
|
-
<Label htmlFor={subId('state')} className="text-xs">State / Province</Label>
|
|
91
|
-
<Input
|
|
92
|
-
id={subId('state')}
|
|
93
|
-
type="text"
|
|
94
|
-
value={address.state || ''}
|
|
95
|
-
onChange={(e) => handleFieldChange('state', e.target.value)}
|
|
96
|
-
placeholder="CA"
|
|
97
|
-
disabled={readonly || props.disabled}
|
|
98
|
-
aria-invalid={!!error}
|
|
99
|
-
/>
|
|
100
|
-
</div>
|
|
101
|
-
</div>
|
|
102
|
-
|
|
103
|
-
<div className="grid grid-cols-2 gap-3">
|
|
104
|
-
<div>
|
|
105
|
-
<Label htmlFor={subId('zipCode')} className="text-xs">ZIP / Postal Code</Label>
|
|
106
|
-
<Input
|
|
107
|
-
id={subId('zipCode')}
|
|
108
|
-
type="text"
|
|
109
|
-
value={address.zipCode || ''}
|
|
110
|
-
onChange={(e) => handleFieldChange('zipCode', e.target.value)}
|
|
111
|
-
placeholder="94102"
|
|
112
|
-
disabled={readonly || props.disabled}
|
|
113
|
-
aria-invalid={!!error}
|
|
114
|
-
/>
|
|
115
|
-
</div>
|
|
116
|
-
|
|
117
|
-
<div>
|
|
118
|
-
<Label htmlFor={subId('country')} className="text-xs">Country</Label>
|
|
119
|
-
<Input
|
|
120
|
-
id={subId('country')}
|
|
121
|
-
type="text"
|
|
122
|
-
value={address.country || ''}
|
|
123
|
-
onChange={(e) => handleFieldChange('country', e.target.value)}
|
|
124
|
-
placeholder="United States"
|
|
125
|
-
disabled={readonly || props.disabled}
|
|
126
|
-
aria-invalid={!!error}
|
|
127
|
-
/>
|
|
128
|
-
</div>
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
);
|
|
132
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
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,18 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { EmptyValue } from '@object-ui/components';
|
|
3
|
-
import { FieldWidgetComponentProps } from './types';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* AutoNumberField - Read-only auto-generated field
|
|
7
|
-
* Values are automatically generated by the system and cannot be edited
|
|
8
|
-
*/
|
|
9
|
-
export function AutoNumberField({ value, field, ...props }: FieldWidgetComponentProps<string | number>) {
|
|
10
|
-
if (value == null) {
|
|
11
|
-
return <EmptyValue className={props.className} />;
|
|
12
|
-
}
|
|
13
|
-
return (
|
|
14
|
-
<span className={`text-sm font-mono text-gray-700 ${props.className || ''}`}>
|
|
15
|
-
{String(value)}
|
|
16
|
-
</span>
|
|
17
|
-
);
|
|
18
|
-
}
|