@object-ui/fields 17.5.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 +714 -0
- package/dist/FieldEditWidget.d.ts +16 -2
- package/dist/field-type-alias.d.ts +16 -0
- package/dist/index.cjs +4 -4
- package/dist/index.css +27 -9
- package/dist/index.d.ts +425 -72
- package/dist/index.js +2530 -2228
- package/dist/widgets/AddressField.d.ts +3 -3
- 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 +9 -2
- package/dist/widgets/GeolocationField.d.ts +1 -1
- package/dist/widgets/GridField.d.ts +56 -4
- 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 +13 -7
- 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 +2 -4
- package/dist/widgets/RichTextField.d.ts +37 -1
- package/dist/widgets/SelectField.d.ts +1 -1
- 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 +27 -7
- package/dist/widgets/lookupColumnDisplay.d.ts +93 -0
- package/dist/widgets/richTextDisplay.d.ts +80 -0
- package/dist/widgets/toHostGroupProps.d.ts +119 -21
- package/dist/widgets/types.d.ts +8 -3
- package/package.json +9 -10
- package/dist/widgets/CharacterCount.d.ts +0 -56
- package/src/FieldEditWidget.test.ts +0 -133
- package/src/FieldEditWidget.tsx +0 -207
- package/src/__tests__/AddressCellRenderer.test.tsx +0 -166
- package/src/__tests__/CurrencyField.minorUnits.test.tsx +0 -191
- package/src/__tests__/CurrencyField.symbol.test.tsx +0 -198
- package/src/__tests__/DateCellRenderer.test.tsx +0 -82
- package/src/__tests__/NumberCellRenderer.grouping.test.tsx +0 -146
- package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
- package/src/__tests__/PercentCellRenderer.locale.test.tsx +0 -152
- package/src/__tests__/PercentCellRenderer.test.tsx +0 -115
- package/src/__tests__/boolean-label-association-e2e.test.tsx +0 -298
- package/src/__tests__/capability-multiselect-retired.test.ts +0 -109
- package/src/__tests__/cell-truncation.test.tsx +0 -96
- package/src/__tests__/composite-group-label-e2e.test.tsx +0 -436
- package/src/__tests__/composite-group-label-readonly-e2e.test.tsx +0 -403
- package/src/__tests__/date-formatter-residue-4272.test.ts +0 -120
- package/src/__tests__/date-locale-channel.test.tsx +0 -305
- package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
- package/src/__tests__/formatCurrency.centsDigit.test.tsx +0 -158
- package/src/__tests__/formatCurrency.minorUnits.test.tsx +0 -221
- package/src/__tests__/group-labelling-declaration.test.ts +0 -148
- package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
- package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
- package/src/__tests__/lookupCellRecordLink.test.tsx +0 -171
- package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
- package/src/__tests__/percent-cell-vs-measure-4576.test.ts +0 -115
- package/src/__tests__/percent-formatter-locale-4553.test.ts +0 -147
- package/src/__tests__/percent-tie-halfup-4590.test.ts +0 -224
- package/src/__tests__/registry-collision.test.ts +0 -91
- package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
- package/src/__tests__/select-label-association-e2e.test.tsx +0 -199
- package/src/__tests__/spec-symbol-batch7.test.ts +0 -126
- package/src/__tests__/validation-feedback.test.tsx +0 -361
- package/src/__tests__/widget-aria-invalid-e2e.test.tsx +0 -265
- package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +0 -338
- package/src/__tests__/widget-dom-leak-e2e.test.tsx +0 -357
- package/src/__tests__/widget-props-contract.test.tsx +0 -142
- package/src/coerce-safe-value.test.ts +0 -34
- package/src/complex-widgets.test.tsx +0 -924
- package/src/currency.ts +0 -144
- package/src/datetime-widgets.test.tsx +0 -185
- package/src/field-type-alias.multiple.test.ts +0 -81
- package/src/field-type-alias.ts +0 -156
- package/src/field-type-coverage.test.ts +0 -67
- package/src/index.css +0 -44
- package/src/index.tsx +0 -2745
- package/src/standard-widgets.test.tsx +0 -364
- package/src/widgets/AddressField.postalCode.test.tsx +0 -257
- package/src/widgets/AddressField.tsx +0 -182
- package/src/widgets/AddressField.uniqueIds.test.tsx +0 -93
- package/src/widgets/AutoNumberField.tsx +0 -18
- package/src/widgets/AvatarField.tsx +0 -117
- package/src/widgets/BooleanField.tsx +0 -86
- package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
- package/src/widgets/CapabilityMultiSelectField.tsx +0 -241
- package/src/widgets/CharacterCount.tsx +0 -263
- package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
- package/src/widgets/CheckboxesField.tsx +0 -169
- package/src/widgets/CodeField.tsx +0 -41
- package/src/widgets/ColorField.tsx +0 -53
- package/src/widgets/CurrencyField.tsx +0 -135
- package/src/widgets/DateField.tsx +0 -40
- package/src/widgets/DateTimeField.tsx +0 -46
- package/src/widgets/EmailField.tsx +0 -37
- package/src/widgets/FileField.tsx +0 -512
- package/src/widgets/FilterConditionField.tsx +0 -454
- package/src/widgets/FormulaField.tsx +0 -38
- package/src/widgets/FullscreenFieldEditor.tsx +0 -60
- package/src/widgets/GeolocationField.tsx +0 -193
- package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
- package/src/widgets/GridField.test.tsx +0 -632
- package/src/widgets/GridField.tsx +0 -1070
- package/src/widgets/ImageCropperDialog.tsx +0 -245
- package/src/widgets/ImageField.maxSize.test.tsx +0 -154
- package/src/widgets/ImageField.tsx +0 -305
- package/src/widgets/ImageLightbox.test.tsx +0 -52
- package/src/widgets/ImageLightbox.tsx +0 -108
- package/src/widgets/LocationField.tsx +0 -57
- package/src/widgets/LookupField.dependsOn.test.tsx +0 -233
- package/src/widgets/LookupField.gateHintLabel.test.tsx +0 -135
- package/src/widgets/LookupField.hydration.test.tsx +0 -136
- package/src/widgets/LookupField.idField.test.tsx +0 -61
- package/src/widgets/LookupField.tsx +0 -1295
- package/src/widgets/MarkdownContent.tsx +0 -19
- package/src/widgets/MasterDetailField.tsx +0 -131
- package/src/widgets/MultiSelectField.cascade.test.tsx +0 -143
- package/src/widgets/MultiSelectField.tsx +0 -196
- package/src/widgets/NumberField.tsx +0 -49
- package/src/widgets/ObjectField.tsx +0 -94
- package/src/widgets/ObjectRefField.tsx +0 -121
- package/src/widgets/OptionsEmptyState.no-provider.test.tsx +0 -88
- package/src/widgets/OptionsEmptyState.test.tsx +0 -169
- package/src/widgets/OptionsEmptyState.tsx +0 -99
- package/src/widgets/PasswordField.tsx +0 -49
- package/src/widgets/PeoplePicker.test.tsx +0 -360
- package/src/widgets/PeoplePicker.tsx +0 -579
- package/src/widgets/PeoplePickerComponents.test.tsx +0 -104
- package/src/widgets/PercentField.tsx +0 -95
- package/src/widgets/PersonRow.tsx +0 -117
- package/src/widgets/PhoneField.tsx +0 -37
- package/src/widgets/QRCodeField.tsx +0 -103
- package/src/widgets/RadioField.cascade.test.tsx +0 -141
- package/src/widgets/RadioField.tsx +0 -143
- package/src/widgets/RatingField.tsx +0 -94
- package/src/widgets/RecipientPickerField.test.tsx +0 -157
- package/src/widgets/RecipientPickerField.tsx +0 -188
- package/src/widgets/RecordPickerDialog.baseFilterShapes.test.tsx +0 -191
- package/src/widgets/RecordPickerDialog.dateLocale.test.tsx +0 -124
- package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
- package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
- package/src/widgets/RecordPickerDialog.tsx +0 -1365
- package/src/widgets/RichTextField.tsx +0 -176
- package/src/widgets/SelectField.cascade.test.tsx +0 -138
- package/src/widgets/SelectField.tsx +0 -193
- package/src/widgets/SelectionTray.tsx +0 -110
- package/src/widgets/SignatureField.tsx +0 -140
- package/src/widgets/SliderField.tsx +0 -39
- package/src/widgets/SummaryField.tsx +0 -31
- package/src/widgets/TagsField.placeholder.no-provider.test.tsx +0 -68
- package/src/widgets/TagsField.placeholder.test.tsx +0 -95
- package/src/widgets/TagsField.tsx +0 -88
- package/src/widgets/TextAreaField.tsx +0 -213
- package/src/widgets/TextField.tsx +0 -45
- package/src/widgets/TimeField.tsx +0 -31
- package/src/widgets/UrlField.tsx +0 -46
- package/src/widgets/UserField.tsx +0 -59
- package/src/widgets/UserFieldPicker.test.tsx +0 -78
- package/src/widgets/VectorField.tsx +0 -29
- package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +0 -66
- package/src/widgets/__tests__/FilterConditionField.operators.test.ts +0 -166
- package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +0 -248
- package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +0 -116
- package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +0 -244
- package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +0 -195
- package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +0 -417
- package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +0 -287
- package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +0 -160
- package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +0 -161
- package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +0 -346
- package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +0 -246
- package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +0 -175
- package/src/widgets/_cell-renderer-bridge.ts +0 -19
- package/src/widgets/address-format.ts +0 -120
- package/src/widgets/deriveLookupColumns.test.ts +0 -112
- package/src/widgets/deriveLookupColumns.ts +0 -108
- package/src/widgets/file-size-guard.ts +0 -55
- package/src/widgets/file-value.test.ts +0 -230
- package/src/widgets/file-value.ts +0 -246
- package/src/widgets/nativeDateValue.test.ts +0 -115
- package/src/widgets/nativeDateValue.ts +0 -112
- package/src/widgets/openNativePicker.ts +0 -18
- package/src/widgets/optionWidgets.gatedOptions.test.tsx +0 -267
- package/src/widgets/optionWidgets.unconfiguredOptions.test.tsx +0 -150
- package/src/widgets/personDisplay.test.ts +0 -113
- package/src/widgets/personDisplay.ts +0 -92
- package/src/widgets/recentLookups.test.ts +0 -45
- package/src/widgets/recentLookups.ts +0 -47
- package/src/widgets/toDomProps.ts +0 -204
- package/src/widgets/toHostGroupProps.ts +0 -102
- package/src/widgets/types.ts +0 -242
- package/src/widgets/useCascadingOptions.ts +0 -46
- package/src/widgets/useFieldTranslation.ts +0 -132
- package/src/widgets/useRecordQuery.test.ts +0 -174
- package/src/widgets/useRecordQuery.ts +0 -291
- package/src/widgets/useUploadingSignal.test.tsx +0 -62
- package/src/widgets/useUploadingSignal.ts +0 -20
- package/src/withFieldCarrier.tsx +0 -75
|
@@ -1,403 +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
|
-
* End-to-end: a group-labelled field's visible label names the widget's surface
|
|
11
|
-
* in EVERY state it renders — not only the editable one (objectui#3990).
|
|
12
|
-
*
|
|
13
|
-
* The residual of objectui#3961 / #3975. Those two moved the association from an
|
|
14
|
-
* inert `<label for>` to the WAI-ARIA group pattern (the label publishes its own
|
|
15
|
-
* `id`, the widget answers with `aria-labelledby` + a role that can carry a
|
|
16
|
-
* name), and pinned it in `composite-group-label-e2e.test.tsx` — entirely on
|
|
17
|
-
* EDITABLE samples. Every one of these widgets also returns EARLY, before the
|
|
18
|
-
* container that consumes that IDREF, for a field-level `readonly: true` and for
|
|
19
|
-
* an option list that resolved to zero offered options. Measured on
|
|
20
|
-
* `origin/main` with a real form + the same bare registration this file uses,
|
|
21
|
-
* counting the elements that reference the host label's published id:
|
|
22
|
-
*
|
|
23
|
-
* ```
|
|
24
|
-
* consumers byLabelText namedByRole
|
|
25
|
-
* multiselect readonly+value 0 0 []
|
|
26
|
-
* multiselect readonly+empty 0 0 []
|
|
27
|
-
* multiselect zeroOptions 0 0 []
|
|
28
|
-
* multiselect editable 1 1 [group:1] ← #3975
|
|
29
|
-
* ```
|
|
30
|
-
*
|
|
31
|
-
* All seven types measured identically, in every readonly state. Both `for`-less
|
|
32
|
-
* halves of the association were dropped by those returns, so the visible label
|
|
33
|
-
* was the accessible name of NOTHING — the same user-visible outcome #3961
|
|
34
|
-
* fixed, in the states its probe never sampled. It is not a regression of either
|
|
35
|
-
* PR: before #3961 the same states rendered a `for` pointing at an id no element
|
|
36
|
-
* carried, which named nothing either. The shape changed; the defect did not.
|
|
37
|
-
*
|
|
38
|
-
* ## Why the role is `group` here even where the editable branch says otherwise
|
|
39
|
-
*
|
|
40
|
-
* `aria-labelledby` on a role-less `div` / `span` names nothing (`generic`
|
|
41
|
-
* prohibits an author name), so the readonly surface needs a role that supports
|
|
42
|
-
* naming, and `group` is the only one that does not lie about interactivity:
|
|
43
|
-
*
|
|
44
|
-
* - `radio` answers `radiogroup` while editable, but its readonly branch renders
|
|
45
|
-
* the CHOSEN option's label as text — there is not one radio left in it;
|
|
46
|
-
* - `file` answers `button` while editable (the dropzone is its single control),
|
|
47
|
-
* but readonly there is no dropzone, only the file names.
|
|
48
|
-
*
|
|
49
|
-
* So the roles this file expects deliberately differ from `HOSTED` in the
|
|
50
|
-
* editable e2e for exactly those two, and the last describe block pins that the
|
|
51
|
-
* editable answers are untouched.
|
|
52
|
-
*
|
|
53
|
-
* The widgets are registered raw rather than through `registerAllFields()`, whose
|
|
54
|
-
* `React.lazy` loaders put an unbounded module load inside a bounded `findBy`
|
|
55
|
-
* window — this repo's known flake generator (AGENTS.md 测试纪律, objectui#3010).
|
|
56
|
-
*/
|
|
57
|
-
|
|
58
|
-
import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest';
|
|
59
|
-
import { render, screen, cleanup } from '@testing-library/react';
|
|
60
|
-
import '@testing-library/jest-dom';
|
|
61
|
-
import { ComponentRegistry } from '@object-ui/core';
|
|
62
|
-
// Module scope: pulls in the form renderer's registration side effect.
|
|
63
|
-
import '@object-ui/components';
|
|
64
|
-
|
|
65
|
-
import { AddressField } from '../widgets/AddressField';
|
|
66
|
-
import { GeolocationField } from '../widgets/GeolocationField';
|
|
67
|
-
import { CheckboxesField } from '../widgets/CheckboxesField';
|
|
68
|
-
import { RadioField } from '../widgets/RadioField';
|
|
69
|
-
import { RatingField } from '../widgets/RatingField';
|
|
70
|
-
import { FileField } from '../widgets/FileField';
|
|
71
|
-
import { MultiSelectField } from '../widgets/MultiSelectField';
|
|
72
|
-
import { SelectField } from '../widgets/SelectField';
|
|
73
|
-
|
|
74
|
-
const WIDGETS: Record<string, any> = {
|
|
75
|
-
address: AddressField,
|
|
76
|
-
geolocation: GeolocationField,
|
|
77
|
-
checkboxes: CheckboxesField,
|
|
78
|
-
radio: RadioField,
|
|
79
|
-
rating: RatingField,
|
|
80
|
-
file: FileField,
|
|
81
|
-
multiselect: MultiSelectField,
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
/** The option widgets — the only ones with a zero-option state to measure. */
|
|
85
|
-
const OPTION_TYPES = ['checkboxes', 'radio', 'multiselect'] as const;
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Where the placeholder sits when a readonly field has no value. Three real
|
|
89
|
-
* shapes, so the assertion is per-widget rather than one presumed form:
|
|
90
|
-
*
|
|
91
|
-
* - `'is-placeholder'`: the whole surface IS `EmptyValue`, which therefore
|
|
92
|
-
* carries the group props itself;
|
|
93
|
-
* - `'contains-placeholder'`: `geolocation` keeps its icon row and renders the
|
|
94
|
-
* placeholder inside it, so the row is the named group;
|
|
95
|
-
* - `'no-placeholder'`: `rating` has no empty state at all — an unset rating
|
|
96
|
-
* renders the same "0 / max" star row.
|
|
97
|
-
*/
|
|
98
|
-
const EMPTY_SHAPE: Record<string, 'is-placeholder' | 'contains-placeholder' | 'no-placeholder'> = {
|
|
99
|
-
address: 'is-placeholder',
|
|
100
|
-
checkboxes: 'is-placeholder',
|
|
101
|
-
radio: 'is-placeholder',
|
|
102
|
-
file: 'is-placeholder',
|
|
103
|
-
multiselect: 'is-placeholder',
|
|
104
|
-
geolocation: 'contains-placeholder',
|
|
105
|
-
rating: 'no-placeholder',
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
const OPTIONS = [
|
|
109
|
-
{ label: 'Alpha', value: 'a' },
|
|
110
|
-
{ label: 'Beta', value: 'b' },
|
|
111
|
-
];
|
|
112
|
-
|
|
113
|
-
/** A stored value per type, so the readonly branch renders its filled shape. */
|
|
114
|
-
const VALUES: Record<string, unknown> = {
|
|
115
|
-
address: { street: '1 Main St', city: 'Springfield' },
|
|
116
|
-
geolocation: { latitude: 1.5, longitude: 2.5 },
|
|
117
|
-
checkboxes: ['a'],
|
|
118
|
-
radio: 'a',
|
|
119
|
-
rating: 3,
|
|
120
|
-
file: { id: 'f1', name: 'report.pdf' },
|
|
121
|
-
multiselect: ['a'],
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
/** The role the EDITABLE branch answers with — unchanged by this issue. */
|
|
125
|
-
const EDITABLE_ROLE: Record<string, string> = {
|
|
126
|
-
address: 'group',
|
|
127
|
-
geolocation: 'group',
|
|
128
|
-
checkboxes: 'group',
|
|
129
|
-
radio: 'radiogroup',
|
|
130
|
-
rating: 'group',
|
|
131
|
-
file: 'button',
|
|
132
|
-
multiselect: 'group',
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const TYPES = Object.keys(WIDGETS);
|
|
136
|
-
|
|
137
|
-
beforeAll(() => {
|
|
138
|
-
for (const [type, Component] of Object.entries(WIDGETS)) {
|
|
139
|
-
ComponentRegistry.register(type, Component as any, {
|
|
140
|
-
namespace: 'field',
|
|
141
|
-
skipFallback: true,
|
|
142
|
-
// The declaration under test, mirroring `FIELD_TYPES_GROUP_LABELLED`.
|
|
143
|
-
labelling: 'group',
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
}, 30000);
|
|
147
|
-
|
|
148
|
-
beforeEach(() => {
|
|
149
|
-
if (!(Element.prototype as any).scrollIntoView) {
|
|
150
|
-
(Element.prototype as any).scrollIntoView = () => {};
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
afterEach(() => {
|
|
155
|
-
cleanup();
|
|
156
|
-
vi.restoreAllMocks();
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
/** HTML's labelable elements — the only ones a `<label for>` can address. */
|
|
160
|
-
const LABELABLE = new Set(['button', 'input', 'meter', 'output', 'progress', 'select', 'textarea']);
|
|
161
|
-
|
|
162
|
-
function labelTargets(): Array<{ forId: string; resolves: boolean; labelable: boolean }> {
|
|
163
|
-
return Array.from(document.querySelectorAll('label'))
|
|
164
|
-
.map((el) => {
|
|
165
|
-
const forId = el.getAttribute('for') ?? '';
|
|
166
|
-
const target = forId ? document.getElementById(forId) : null;
|
|
167
|
-
return {
|
|
168
|
-
forId,
|
|
169
|
-
resolves: !!target,
|
|
170
|
-
labelable: !!target && LABELABLE.has(target.tagName.toLowerCase()),
|
|
171
|
-
};
|
|
172
|
-
})
|
|
173
|
-
.filter((l) => l.forId !== '');
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function fieldConfig(
|
|
177
|
-
type: string,
|
|
178
|
-
opts: { readonly?: boolean; zeroOptions?: boolean } = {},
|
|
179
|
-
): Record<string, unknown> {
|
|
180
|
-
const config: any = { name: `f_${type}`, label: `Group Label ${type}`, type };
|
|
181
|
-
if ((OPTION_TYPES as readonly string[]).includes(type)) {
|
|
182
|
-
config.options = opts.zeroOptions ? [] : OPTIONS;
|
|
183
|
-
}
|
|
184
|
-
// Field-level readonly — the state this issue is about. NOT the form-level
|
|
185
|
-
// `mode: 'view'`, which renders a different (detail) path and was already
|
|
186
|
-
// naming its group correctly.
|
|
187
|
-
if (opts.readonly) config.readonly = true;
|
|
188
|
-
return config;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/** The real form renderer hosting one field — the #3990 reproduction. */
|
|
192
|
-
function renderForm(fields: any[], defaultValues: Record<string, unknown> = {}) {
|
|
193
|
-
const Form = ComponentRegistry.get('form')!;
|
|
194
|
-
return render(
|
|
195
|
-
<Form
|
|
196
|
-
schema={{
|
|
197
|
-
type: 'form',
|
|
198
|
-
mode: 'create',
|
|
199
|
-
showSubmit: false,
|
|
200
|
-
showCancel: false,
|
|
201
|
-
defaultValues,
|
|
202
|
-
fields,
|
|
203
|
-
}}
|
|
204
|
-
/>,
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const hostLabelOf = (name: string): HTMLLabelElement => {
|
|
209
|
-
const el = document.querySelector(`[data-field="${name}"] > label`);
|
|
210
|
-
if (!el) throw new Error(`no host label rendered for field "${name}"`);
|
|
211
|
-
return el as HTMLLabelElement;
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
describe('a READONLY group-labelled field is named by its host label (objectui#3990)', () => {
|
|
215
|
-
it.each(TYPES)('%s: the host label names the readonly surface', (type) => {
|
|
216
|
-
renderForm([fieldConfig(type, { readonly: true })], { [`f_${type}`]: VALUES[type] });
|
|
217
|
-
|
|
218
|
-
// 0 before this fix, 1 after — for all seven, whatever their readonly shape.
|
|
219
|
-
const named = screen.getAllByRole('group', { name: `Group Label ${type}` });
|
|
220
|
-
expect(named).toHaveLength(1);
|
|
221
|
-
// The same association read from the other side.
|
|
222
|
-
expect(screen.getAllByLabelText(`Group Label ${type}`)).toEqual(named);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it.each(TYPES)('%s: the readonly surface is the element the host handed its id to', (type) => {
|
|
226
|
-
// The name and the host id must land on the SAME element, as in the editable
|
|
227
|
-
// path: the readonly branches dropped BOTH, so the `…-form-item` id existed
|
|
228
|
-
// on no element in the document.
|
|
229
|
-
renderForm([fieldConfig(type, { readonly: true })], { [`f_${type}`]: VALUES[type] });
|
|
230
|
-
|
|
231
|
-
const named = screen.getByRole('group', { name: `Group Label ${type}` });
|
|
232
|
-
expect(named.getAttribute('id')).toMatch(/-form-item$/);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it.each(TYPES)('%s: readonly with NO value names the placeholder surface', (type) => {
|
|
236
|
-
renderForm([fieldConfig(type, { readonly: true })]);
|
|
237
|
-
|
|
238
|
-
const named = screen.getByRole('group', { name: `Group Label ${type}` });
|
|
239
|
-
const shape = EMPTY_SHAPE[type];
|
|
240
|
-
if (shape === 'is-placeholder') {
|
|
241
|
-
// `EmptyValue` itself is the field's whole surface here. Its own
|
|
242
|
-
// `aria-label` ("No value") stays in the markup and is outranked by
|
|
243
|
-
// `aria-labelledby` per accname — on the `generic` role it carried before
|
|
244
|
-
// this change, that author name was not exposed at all.
|
|
245
|
-
expect(named).toHaveAttribute('data-slot', 'empty-value');
|
|
246
|
-
expect(named).toHaveAccessibleName(`Group Label ${type}`);
|
|
247
|
-
} else if (shape === 'contains-placeholder') {
|
|
248
|
-
expect(named.querySelector('[data-slot="empty-value"]')).not.toBeNull();
|
|
249
|
-
} else {
|
|
250
|
-
// `rating` has no empty state: an unset rating is the same star row.
|
|
251
|
-
expect(named.querySelector('[data-slot="empty-value"]')).toBeNull();
|
|
252
|
-
expect(named.textContent).toContain('0 / 5');
|
|
253
|
-
}
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it.each(TYPES)('%s: the host label still emits an id and no `for` when readonly', (type) => {
|
|
257
|
-
renderForm([fieldConfig(type, { readonly: true })], { [`f_${type}`]: VALUES[type] });
|
|
258
|
-
|
|
259
|
-
const label = hostLabelOf(`f_${type}`);
|
|
260
|
-
expect(label).not.toHaveAttribute('for');
|
|
261
|
-
expect(label.id).not.toBe('');
|
|
262
|
-
// A dangling IDREF fails silently, which reads exactly like success.
|
|
263
|
-
expect(document.getElementById(label.id)).toBe(label);
|
|
264
|
-
// And no OTHER label in the field is left with an inert/dangling `for`.
|
|
265
|
-
expect(labelTargets().filter((l) => !l.resolves || !l.labelable)).toEqual([]);
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it.each(TYPES)('%s: the readonly surface takes the two whole-field keys ONLY', (type) => {
|
|
269
|
-
// The narrow pair is deliberate (`toHostGroupProps`): a readonly display has
|
|
270
|
-
// no focusable control, so control-only plumbing must not ride along. `name`
|
|
271
|
-
// is the one that would be a DOM leak of the objectui#3291 class — it is
|
|
272
|
-
// legal on form controls only, and these surfaces are `div` / `span`.
|
|
273
|
-
renderForm([{ ...fieldConfig(type, { readonly: true }), description: 'Some help' }], {
|
|
274
|
-
[`f_${type}`]: VALUES[type],
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
const named = screen.getByRole('group', { name: `Group Label ${type}` });
|
|
278
|
-
expect(named).not.toHaveAttribute('name');
|
|
279
|
-
expect(named).not.toHaveAttribute('aria-invalid');
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it('all seven readonly in ONE form: each is named exactly once', () => {
|
|
283
|
-
const fields = TYPES.map((type) => fieldConfig(type, { readonly: true }));
|
|
284
|
-
const values = Object.fromEntries(TYPES.map((type) => [`f_${type}`, VALUES[type]]));
|
|
285
|
-
renderForm(fields, values);
|
|
286
|
-
|
|
287
|
-
for (const type of TYPES) {
|
|
288
|
-
expect(screen.getAllByRole('group', { name: `Group Label ${type}` })).toHaveLength(1);
|
|
289
|
-
}
|
|
290
|
-
expect(labelTargets().filter((l) => !l.resolves || !l.labelable)).toEqual([]);
|
|
291
|
-
});
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
describe('a ZERO-OPTION group-labelled field is named by its host label (objectui#3990)', () => {
|
|
295
|
-
it.each(OPTION_TYPES)('%s: the unfillable-options box is the named group', (type) => {
|
|
296
|
-
renderForm([fieldConfig(type, { zeroOptions: true })]);
|
|
297
|
-
|
|
298
|
-
const named = screen.getAllByRole('group', { name: `Group Label ${type}` });
|
|
299
|
-
expect(named).toHaveLength(1);
|
|
300
|
-
// The named element is the shared `OptionsEmptyState` box, not some ancestor:
|
|
301
|
-
// in this state it is the only thing the widget renders.
|
|
302
|
-
expect(named[0]).toBe(screen.getByTestId(`${type}-empty-f_${type}`));
|
|
303
|
-
expect(named[0].getAttribute('id')).toMatch(/-form-item$/);
|
|
304
|
-
expect(screen.getAllByLabelText(`Group Label ${type}`)).toEqual(named);
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
it.each(OPTION_TYPES)('%s: readonly wins over zero options, and is still named', (type) => {
|
|
308
|
-
// Both early returns at once. The readonly branch comes first, so the
|
|
309
|
-
// measured surface is the readonly one — and it must still be named.
|
|
310
|
-
renderForm([fieldConfig(type, { readonly: true, zeroOptions: true })], {
|
|
311
|
-
[`f_${type}`]: VALUES[type],
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
expect(screen.getAllByRole('group', { name: `Group Label ${type}` })).toHaveLength(1);
|
|
315
|
-
expect(screen.queryByTestId(`${type}-empty-f_${type}`)).toBeNull();
|
|
316
|
-
});
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
describe('the editable answers are untouched (objectui#3961 / #3975 regression)', () => {
|
|
320
|
-
it.each(TYPES)('%s: editable keeps its own role, readonly answers `group`', (type) => {
|
|
321
|
-
// The drift pin: one widget, two surfaces, both named. `radio` and `file`
|
|
322
|
-
// deliberately answer with DIFFERENT roles in the two states, so a future
|
|
323
|
-
// "unify the roles" edit has to come here and say which it broke.
|
|
324
|
-
renderForm([fieldConfig(type)], { [`f_${type}`]: VALUES[type] });
|
|
325
|
-
expect(
|
|
326
|
-
screen.getAllByRole(EDITABLE_ROLE[type], { name: `Group Label ${type}` }),
|
|
327
|
-
).toHaveLength(1);
|
|
328
|
-
cleanup();
|
|
329
|
-
|
|
330
|
-
renderForm([fieldConfig(type, { readonly: true })], { [`f_${type}`]: VALUES[type] });
|
|
331
|
-
expect(screen.getAllByRole('group', { name: `Group Label ${type}` })).toHaveLength(1);
|
|
332
|
-
});
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
describe('STANDALONE readonly widgets are unchanged (objectui#3990)', () => {
|
|
336
|
-
// `FieldEditWidget` (the grid's inline cell editor) and bare SDUI nodes hand
|
|
337
|
-
// down no id and no host label, so there is nothing to associate and nothing
|
|
338
|
-
// may be emitted: an unnamed `role="group"` announces a container with no name
|
|
339
|
-
// to give, and an id nobody asked for can collide with the page's own.
|
|
340
|
-
|
|
341
|
-
it.each(TYPES)('%s: no role, no IDREF, no id', (type) => {
|
|
342
|
-
const Widget = WIDGETS[type];
|
|
343
|
-
render(
|
|
344
|
-
<Widget
|
|
345
|
-
value={VALUES[type]}
|
|
346
|
-
onChange={() => {}}
|
|
347
|
-
readonly
|
|
348
|
-
field={{
|
|
349
|
-
name: type,
|
|
350
|
-
label: `Group Label ${type}`,
|
|
351
|
-
type,
|
|
352
|
-
...((OPTION_TYPES as readonly string[]).includes(type) ? { options: OPTIONS } : null),
|
|
353
|
-
}}
|
|
354
|
-
/>,
|
|
355
|
-
);
|
|
356
|
-
|
|
357
|
-
expect(screen.queryAllByRole('group')).toHaveLength(0);
|
|
358
|
-
expect(document.querySelector('[aria-labelledby]')).toBeNull();
|
|
359
|
-
expect(document.querySelector('[id]')).toBeNull();
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
it.each(TYPES)('%s: no value, still nothing emitted', (type) => {
|
|
363
|
-
const Widget = WIDGETS[type];
|
|
364
|
-
render(
|
|
365
|
-
<Widget
|
|
366
|
-
value={undefined}
|
|
367
|
-
onChange={() => {}}
|
|
368
|
-
readonly
|
|
369
|
-
field={{
|
|
370
|
-
name: type,
|
|
371
|
-
label: `Group Label ${type}`,
|
|
372
|
-
type,
|
|
373
|
-
...((OPTION_TYPES as readonly string[]).includes(type) ? { options: OPTIONS } : null),
|
|
374
|
-
}}
|
|
375
|
-
/>,
|
|
376
|
-
);
|
|
377
|
-
|
|
378
|
-
expect(screen.queryAllByRole('group')).toHaveLength(0);
|
|
379
|
-
expect(document.querySelector('[aria-labelledby]')).toBeNull();
|
|
380
|
-
// The placeholder keeps its own name and gains no role it was not given.
|
|
381
|
-
const placeholder = document.querySelector('[data-slot="empty-value"]');
|
|
382
|
-
if (placeholder) expect(placeholder).not.toHaveAttribute('role');
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
it('the shared options-empty box emits nothing for a widget that is not group-labelled', () => {
|
|
386
|
-
// The positive control for `OptionsEmptyState`'s new prop. The single
|
|
387
|
-
// `SelectField` is NOT in `FIELD_TYPES_GROUP_LABELLED` — its label keeps a
|
|
388
|
-
// plain, working `for` — so the shared box must stay attribute-for-attribute
|
|
389
|
-
// what it was: no `role`, no IDREF, no host id.
|
|
390
|
-
render(
|
|
391
|
-
<SelectField
|
|
392
|
-
value={undefined}
|
|
393
|
-
onChange={() => {}}
|
|
394
|
-
field={{ name: 'stage', label: 'Stage', type: 'select', options: [] } as any}
|
|
395
|
-
/>,
|
|
396
|
-
);
|
|
397
|
-
|
|
398
|
-
const box = screen.getByTestId('select-empty-stage');
|
|
399
|
-
expect(box).not.toHaveAttribute('role');
|
|
400
|
-
expect(box).not.toHaveAttribute('aria-labelledby');
|
|
401
|
-
expect(box).not.toHaveAttribute('id');
|
|
402
|
-
});
|
|
403
|
-
});
|
|
@@ -1,120 +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
|
-
* objectui#4272 — the two FORMATTER-level en-US channels the #4468 fix
|
|
11
|
-
* (PR #4512) deliberately left behind.
|
|
12
|
-
*
|
|
13
|
-
* PR #4512 pointed every date *renderer* at `useDisplayLocale()`. It could not
|
|
14
|
-
* reach these two, because both are properties of the formatter's signature
|
|
15
|
-
* rather than of any renderer:
|
|
16
|
-
*
|
|
17
|
-
* 1. `formatDate`'s `'short'` branch hardcoded the tag —
|
|
18
|
-
* `date.toLocaleDateString('en-US', { month: 'short' })` — so it rendered
|
|
19
|
-
* an English month name even when a caller DID thread `options.locale`
|
|
20
|
-
* into the very same call. The sibling default branch two lines below
|
|
21
|
-
* already honored `options?.locale`; `'short'` was the outlier.
|
|
22
|
-
* 2. `formatDateTime` took no options parameter at all, so no caller could
|
|
23
|
-
* localize it however hard it tried; it passed `undefined` to `Intl`,
|
|
24
|
-
* which means "the MACHINE's locale" — neither of the repo's two locale
|
|
25
|
-
* channels.
|
|
26
|
-
*
|
|
27
|
-
* ── Why these cases live in a file with NO provider ──────────────────────
|
|
28
|
-
* objectui#4514: `useObjectTranslation()` outside a provider reports
|
|
29
|
-
* react-i18next's GLOBAL language, which any `I18nProvider` mounted earlier in
|
|
30
|
-
* the same file leaves behind. These are pure-function cases that touch no
|
|
31
|
-
* hook at all, so they are immune — but they are kept apart from the
|
|
32
|
-
* provider-mounting cases regardless, so the split stays structural rather
|
|
33
|
-
* than something a later edit can quietly erode. The provider-mounted halves
|
|
34
|
-
* of this card live in `RecordPickerDialog.dateLocale.test.tsx`,
|
|
35
|
-
* `plugin-grid/.../mobileCardDateLocale.test.tsx` and
|
|
36
|
-
* `plugin-gantt/.../ObjectGantt.dateLocale.test.tsx`.
|
|
37
|
-
*
|
|
38
|
-
* ── Directions (measured, not presumed) ──────────────────────────────────
|
|
39
|
-
* Runner: node v22.22.2 / ICU 78.2 / TZ=UTC / machine locale en-US.
|
|
40
|
-
* `en` and `en-US` produce an IDENTICAL `{month:'short'}` for all 12 months,
|
|
41
|
-
* so every `en` case below is GREEN ON BOTH SIDES — it is the byte-identical
|
|
42
|
-
* must-not-change pin, NOT red evidence. The `zh` and `de` cases are the ones
|
|
43
|
-
* that actually go red against unfixed code.
|
|
44
|
-
*/
|
|
45
|
-
|
|
46
|
-
import { describe, it, expect } from 'vitest';
|
|
47
|
-
import { formatDate, formatDateTime } from '../index';
|
|
48
|
-
|
|
49
|
-
/** Local-parts dates: the rendered day is then the same in every timezone. */
|
|
50
|
-
const AUG = new Date(2026, 7, 15, 12, 0, 0);
|
|
51
|
-
const OCT = new Date(2026, 9, 15, 12, 0, 0);
|
|
52
|
-
/** A non-current year, so the year-dropping default branch is not in play. */
|
|
53
|
-
const INSTANT = new Date(2024, 0, 5, 8, 30, 0);
|
|
54
|
-
|
|
55
|
-
describe("formatDate 'short' honors the threaded locale (objectui#4272)", () => {
|
|
56
|
-
it('zh renders the Chinese month, not the hardcoded English one', () => {
|
|
57
|
-
expect(formatDate(AUG, 'short', { locale: 'zh' })).toBe("8月 15, '26");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* A non-CJK second locale, on a month whose English and German short forms
|
|
62
|
-
* differ (`Oct` / `Okt`). August would NOT discriminate — German also
|
|
63
|
-
* abbreviates it `Aug` — so this case is deliberately October.
|
|
64
|
-
*/
|
|
65
|
-
it('de renders the German month abbreviation', () => {
|
|
66
|
-
expect(formatDate(OCT, 'short', { locale: 'de' })).toBe("Okt 15, '26");
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* PIN, green on both sides: `en` and the runner's `en-US` agree on every
|
|
71
|
-
* month name, so this asserts the English output is byte-identical after
|
|
72
|
-
* the change. It is not evidence that the fix works.
|
|
73
|
-
*/
|
|
74
|
-
it('en output is byte-identical (must-not-change)', () => {
|
|
75
|
-
expect(formatDate(AUG, 'short', { locale: 'en' })).toBe("Aug 15, '26");
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('the composite shape around the month is unchanged', () => {
|
|
79
|
-
// Day and 2-digit year, apostrophe and comma placement: the `'short'`
|
|
80
|
-
// contract is "Jan 15, '24" and only the month token was localized.
|
|
81
|
-
expect(formatDate(AUG, 'short', { locale: 'en' })).toMatch(/^Aug 15, '26$/);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe('formatDateTime accepts a locale at all (objectui#4272)', () => {
|
|
86
|
-
it('zh renders the Chinese datetime form', () => {
|
|
87
|
-
expect(formatDateTime(INSTANT, { locale: 'zh' })).toBe('2024年1月5日 08:30');
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
/** PIN, green on both sides — see the `en` note above. */
|
|
91
|
-
it('en output is byte-identical (must-not-change)', () => {
|
|
92
|
-
expect(formatDateTime(INSTANT, { locale: 'en' })).toBe('Jan 5, 2024, 08:30 AM');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Backward compatibility: the parameter is optional and an existing caller
|
|
97
|
-
* that passes nothing keeps the exact behavior it had.
|
|
98
|
-
*
|
|
99
|
-
* This is the ONE case where building the expectation from the runner is
|
|
100
|
-
* the correct move rather than the objectui#4513 trap: the contract under
|
|
101
|
-
* test IS "still the runtime default", so both sides of the comparison are
|
|
102
|
-
* meant to be the machine's locale. Every other case in this file spells
|
|
103
|
-
* its tag explicitly.
|
|
104
|
-
*/
|
|
105
|
-
it('no options — still the runtime default, unchanged', () => {
|
|
106
|
-
const runtimeDefault = INSTANT.toLocaleDateString(undefined, {
|
|
107
|
-
year: 'numeric',
|
|
108
|
-
month: 'short',
|
|
109
|
-
day: 'numeric',
|
|
110
|
-
hour: '2-digit',
|
|
111
|
-
minute: '2-digit',
|
|
112
|
-
});
|
|
113
|
-
expect(formatDateTime(INSTANT)).toBe(runtimeDefault);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('the empty / invalid guards are untouched', () => {
|
|
117
|
-
expect(formatDateTime('', { locale: 'zh' })).toBe('—');
|
|
118
|
-
expect(formatDateTime('not-a-date', { locale: 'zh' })).toBe('—');
|
|
119
|
-
});
|
|
120
|
-
});
|