@object-ui/fields 17.5.0 → 17.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/CHANGELOG.md +6150 -0
  2. package/README.md +108 -8
  3. package/dist/FieldEditWidget.d.ts +70 -8
  4. package/dist/coerceToSafeValue.d.ts +54 -0
  5. package/dist/currency.d.ts +1 -1
  6. package/dist/field-type-alias.d.ts +16 -0
  7. package/dist/index.cjs +5 -5
  8. package/dist/index.css +53 -11
  9. package/dist/index.d.ts +653 -128
  10. package/dist/index.js +4275 -3171
  11. package/dist/widgets/AddressField.d.ts +3 -3
  12. package/dist/widgets/AutoNumberField.d.ts +1 -1
  13. package/dist/widgets/AvatarField.d.ts +20 -4
  14. package/dist/widgets/BooleanField.d.ts +2 -2
  15. package/dist/widgets/CapabilityMultiSelectField.d.ts +1 -1
  16. package/dist/widgets/CheckboxesField.d.ts +1 -1
  17. package/dist/widgets/CodeField.d.ts +1 -1
  18. package/dist/widgets/ColorField.d.ts +1 -1
  19. package/dist/widgets/CurrencyField.d.ts +1 -1
  20. package/dist/widgets/DateField.d.ts +2 -2
  21. package/dist/widgets/DateTimeField.d.ts +2 -2
  22. package/dist/widgets/EmailField.d.ts +1 -1
  23. package/dist/widgets/FileField.d.ts +18 -2
  24. package/dist/widgets/FilterConditionField.d.ts +43 -2
  25. package/dist/widgets/FormulaField.d.ts +1 -1
  26. package/dist/widgets/FullscreenFieldEditor.d.ts +9 -2
  27. package/dist/widgets/GeolocationField.d.ts +10 -4
  28. package/dist/widgets/GridField.d.ts +102 -86
  29. package/dist/widgets/ImageField.d.ts +1 -1
  30. package/dist/widgets/LocationField.d.ts +31 -4
  31. package/dist/widgets/LookupField.d.ts +9 -1
  32. package/dist/widgets/MasterDetailField.d.ts +1 -1
  33. package/dist/widgets/MultiSelectField.d.ts +1 -1
  34. package/dist/widgets/NumberField.d.ts +2 -2
  35. package/dist/widgets/ObjectField.d.ts +10 -3
  36. package/dist/widgets/ObjectRefField.d.ts +1 -1
  37. package/dist/widgets/OptionsEmptyState.d.ts +38 -8
  38. package/dist/widgets/PasswordField.d.ts +15 -1
  39. package/dist/widgets/PeoplePicker.d.ts +17 -2
  40. package/dist/widgets/PercentField.d.ts +3 -2
  41. package/dist/widgets/PersonRow.d.ts +13 -2
  42. package/dist/widgets/PhoneField.d.ts +1 -1
  43. package/dist/widgets/QRCodeField.d.ts +1 -1
  44. package/dist/widgets/RadioField.d.ts +1 -1
  45. package/dist/widgets/RatingField.d.ts +1 -1
  46. package/dist/widgets/RecipientPickerField.d.ts +44 -1
  47. package/dist/widgets/RecordPickerDialog.d.ts +33 -16
  48. package/dist/widgets/RichTextField.d.ts +37 -1
  49. package/dist/widgets/SelectField.d.ts +1 -1
  50. package/dist/widgets/SelectionTray.d.ts +13 -2
  51. package/dist/widgets/SignatureField.d.ts +32 -2
  52. package/dist/widgets/SliderField.d.ts +37 -2
  53. package/dist/widgets/SummaryField.d.ts +1 -1
  54. package/dist/widgets/TagsField.d.ts +1 -1
  55. package/dist/widgets/TextAreaField.d.ts +6 -2
  56. package/dist/widgets/TextField.d.ts +2 -2
  57. package/dist/widgets/TimeField.d.ts +2 -2
  58. package/dist/widgets/UrlField.d.ts +1 -1
  59. package/dist/widgets/UserField.d.ts +1 -1
  60. package/dist/widgets/VectorField.d.ts +1 -1
  61. package/dist/widgets/_cell-renderer-bridge.d.ts +1 -1
  62. package/dist/widgets/address-format.d.ts +27 -7
  63. package/dist/widgets/file-affordance.d.ts +33 -0
  64. package/dist/widgets/file-size-guard.d.ts +5 -23
  65. package/dist/widgets/file-value.d.ts +38 -9
  66. package/dist/widgets/lookupColumnDisplay.d.ts +110 -0
  67. package/dist/widgets/nativeDateValue.d.ts +9 -27
  68. package/dist/widgets/number-format.d.ts +137 -0
  69. package/dist/widgets/numberBadInput.d.ts +135 -0
  70. package/dist/widgets/personDisplay.d.ts +22 -4
  71. package/dist/widgets/richTextDisplay.d.ts +116 -0
  72. package/dist/widgets/toDomProps.d.ts +10 -1
  73. package/dist/widgets/toHostGroupProps.d.ts +172 -21
  74. package/dist/widgets/toHostProps.d.ts +105 -0
  75. package/dist/widgets/types.d.ts +19 -6
  76. package/dist/widgets/upload-error-message.d.ts +6 -0
  77. package/dist/widgets/uploadingScope.d.ts +142 -0
  78. package/dist/widgets/useCascadingOptions.d.ts +3 -2
  79. package/dist/widgets/useRecordQuery.d.ts +4 -2
  80. package/dist/widgets/useUploadingSignal.d.ts +8 -0
  81. package/package.json +11 -13
  82. package/dist/widgets/CharacterCount.d.ts +0 -56
  83. package/src/FieldEditWidget.test.ts +0 -133
  84. package/src/FieldEditWidget.tsx +0 -207
  85. package/src/__tests__/AddressCellRenderer.test.tsx +0 -166
  86. package/src/__tests__/CurrencyField.minorUnits.test.tsx +0 -191
  87. package/src/__tests__/CurrencyField.symbol.test.tsx +0 -198
  88. package/src/__tests__/DateCellRenderer.test.tsx +0 -82
  89. package/src/__tests__/NumberCellRenderer.grouping.test.tsx +0 -146
  90. package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
  91. package/src/__tests__/PercentCellRenderer.locale.test.tsx +0 -152
  92. package/src/__tests__/PercentCellRenderer.test.tsx +0 -115
  93. package/src/__tests__/boolean-label-association-e2e.test.tsx +0 -298
  94. package/src/__tests__/capability-multiselect-retired.test.ts +0 -109
  95. package/src/__tests__/cell-truncation.test.tsx +0 -96
  96. package/src/__tests__/composite-group-label-e2e.test.tsx +0 -436
  97. package/src/__tests__/composite-group-label-readonly-e2e.test.tsx +0 -403
  98. package/src/__tests__/date-formatter-residue-4272.test.ts +0 -120
  99. package/src/__tests__/date-locale-channel.test.tsx +0 -305
  100. package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
  101. package/src/__tests__/formatCurrency.centsDigit.test.tsx +0 -158
  102. package/src/__tests__/formatCurrency.minorUnits.test.tsx +0 -221
  103. package/src/__tests__/group-labelling-declaration.test.ts +0 -148
  104. package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
  105. package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
  106. package/src/__tests__/lookupCellRecordLink.test.tsx +0 -171
  107. package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
  108. package/src/__tests__/percent-cell-vs-measure-4576.test.ts +0 -115
  109. package/src/__tests__/percent-formatter-locale-4553.test.ts +0 -147
  110. package/src/__tests__/percent-tie-halfup-4590.test.ts +0 -224
  111. package/src/__tests__/registry-collision.test.ts +0 -91
  112. package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
  113. package/src/__tests__/select-label-association-e2e.test.tsx +0 -199
  114. package/src/__tests__/spec-symbol-batch7.test.ts +0 -126
  115. package/src/__tests__/validation-feedback.test.tsx +0 -361
  116. package/src/__tests__/widget-aria-invalid-e2e.test.tsx +0 -265
  117. package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +0 -338
  118. package/src/__tests__/widget-dom-leak-e2e.test.tsx +0 -357
  119. package/src/__tests__/widget-props-contract.test.tsx +0 -142
  120. package/src/coerce-safe-value.test.ts +0 -34
  121. package/src/complex-widgets.test.tsx +0 -924
  122. package/src/currency.ts +0 -144
  123. package/src/datetime-widgets.test.tsx +0 -185
  124. package/src/field-type-alias.multiple.test.ts +0 -81
  125. package/src/field-type-alias.ts +0 -156
  126. package/src/field-type-coverage.test.ts +0 -67
  127. package/src/index.css +0 -44
  128. package/src/index.tsx +0 -2745
  129. package/src/standard-widgets.test.tsx +0 -364
  130. package/src/widgets/AddressField.postalCode.test.tsx +0 -257
  131. package/src/widgets/AddressField.tsx +0 -182
  132. package/src/widgets/AddressField.uniqueIds.test.tsx +0 -93
  133. package/src/widgets/AutoNumberField.tsx +0 -18
  134. package/src/widgets/AvatarField.tsx +0 -117
  135. package/src/widgets/BooleanField.tsx +0 -86
  136. package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
  137. package/src/widgets/CapabilityMultiSelectField.tsx +0 -241
  138. package/src/widgets/CharacterCount.tsx +0 -263
  139. package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
  140. package/src/widgets/CheckboxesField.tsx +0 -169
  141. package/src/widgets/CodeField.tsx +0 -41
  142. package/src/widgets/ColorField.tsx +0 -53
  143. package/src/widgets/CurrencyField.tsx +0 -135
  144. package/src/widgets/DateField.tsx +0 -40
  145. package/src/widgets/DateTimeField.tsx +0 -46
  146. package/src/widgets/EmailField.tsx +0 -37
  147. package/src/widgets/FileField.tsx +0 -512
  148. package/src/widgets/FilterConditionField.tsx +0 -454
  149. package/src/widgets/FormulaField.tsx +0 -38
  150. package/src/widgets/FullscreenFieldEditor.tsx +0 -60
  151. package/src/widgets/GeolocationField.tsx +0 -193
  152. package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
  153. package/src/widgets/GridField.test.tsx +0 -632
  154. package/src/widgets/GridField.tsx +0 -1070
  155. package/src/widgets/ImageCropperDialog.tsx +0 -245
  156. package/src/widgets/ImageField.maxSize.test.tsx +0 -154
  157. package/src/widgets/ImageField.tsx +0 -305
  158. package/src/widgets/ImageLightbox.test.tsx +0 -52
  159. package/src/widgets/ImageLightbox.tsx +0 -108
  160. package/src/widgets/LocationField.tsx +0 -57
  161. package/src/widgets/LookupField.dependsOn.test.tsx +0 -233
  162. package/src/widgets/LookupField.gateHintLabel.test.tsx +0 -135
  163. package/src/widgets/LookupField.hydration.test.tsx +0 -136
  164. package/src/widgets/LookupField.idField.test.tsx +0 -61
  165. package/src/widgets/LookupField.tsx +0 -1295
  166. package/src/widgets/MarkdownContent.tsx +0 -19
  167. package/src/widgets/MasterDetailField.tsx +0 -131
  168. package/src/widgets/MultiSelectField.cascade.test.tsx +0 -143
  169. package/src/widgets/MultiSelectField.tsx +0 -196
  170. package/src/widgets/NumberField.tsx +0 -49
  171. package/src/widgets/ObjectField.tsx +0 -94
  172. package/src/widgets/ObjectRefField.tsx +0 -121
  173. package/src/widgets/OptionsEmptyState.no-provider.test.tsx +0 -88
  174. package/src/widgets/OptionsEmptyState.test.tsx +0 -169
  175. package/src/widgets/OptionsEmptyState.tsx +0 -99
  176. package/src/widgets/PasswordField.tsx +0 -49
  177. package/src/widgets/PeoplePicker.test.tsx +0 -360
  178. package/src/widgets/PeoplePicker.tsx +0 -579
  179. package/src/widgets/PeoplePickerComponents.test.tsx +0 -104
  180. package/src/widgets/PercentField.tsx +0 -95
  181. package/src/widgets/PersonRow.tsx +0 -117
  182. package/src/widgets/PhoneField.tsx +0 -37
  183. package/src/widgets/QRCodeField.tsx +0 -103
  184. package/src/widgets/RadioField.cascade.test.tsx +0 -141
  185. package/src/widgets/RadioField.tsx +0 -143
  186. package/src/widgets/RatingField.tsx +0 -94
  187. package/src/widgets/RecipientPickerField.test.tsx +0 -157
  188. package/src/widgets/RecipientPickerField.tsx +0 -188
  189. package/src/widgets/RecordPickerDialog.baseFilterShapes.test.tsx +0 -191
  190. package/src/widgets/RecordPickerDialog.dateLocale.test.tsx +0 -124
  191. package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
  192. package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
  193. package/src/widgets/RecordPickerDialog.tsx +0 -1365
  194. package/src/widgets/RichTextField.tsx +0 -176
  195. package/src/widgets/SelectField.cascade.test.tsx +0 -138
  196. package/src/widgets/SelectField.tsx +0 -193
  197. package/src/widgets/SelectionTray.tsx +0 -110
  198. package/src/widgets/SignatureField.tsx +0 -140
  199. package/src/widgets/SliderField.tsx +0 -39
  200. package/src/widgets/SummaryField.tsx +0 -31
  201. package/src/widgets/TagsField.placeholder.no-provider.test.tsx +0 -68
  202. package/src/widgets/TagsField.placeholder.test.tsx +0 -95
  203. package/src/widgets/TagsField.tsx +0 -88
  204. package/src/widgets/TextAreaField.tsx +0 -213
  205. package/src/widgets/TextField.tsx +0 -45
  206. package/src/widgets/TimeField.tsx +0 -31
  207. package/src/widgets/UrlField.tsx +0 -46
  208. package/src/widgets/UserField.tsx +0 -59
  209. package/src/widgets/UserFieldPicker.test.tsx +0 -78
  210. package/src/widgets/VectorField.tsx +0 -29
  211. package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +0 -66
  212. package/src/widgets/__tests__/FilterConditionField.operators.test.ts +0 -166
  213. package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +0 -248
  214. package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +0 -116
  215. package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +0 -244
  216. package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +0 -195
  217. package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +0 -417
  218. package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +0 -287
  219. package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +0 -160
  220. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +0 -161
  221. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +0 -346
  222. package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +0 -246
  223. package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +0 -175
  224. package/src/widgets/_cell-renderer-bridge.ts +0 -19
  225. package/src/widgets/address-format.ts +0 -120
  226. package/src/widgets/deriveLookupColumns.test.ts +0 -112
  227. package/src/widgets/deriveLookupColumns.ts +0 -108
  228. package/src/widgets/file-size-guard.ts +0 -55
  229. package/src/widgets/file-value.test.ts +0 -230
  230. package/src/widgets/file-value.ts +0 -246
  231. package/src/widgets/nativeDateValue.test.ts +0 -115
  232. package/src/widgets/nativeDateValue.ts +0 -112
  233. package/src/widgets/openNativePicker.ts +0 -18
  234. package/src/widgets/optionWidgets.gatedOptions.test.tsx +0 -267
  235. package/src/widgets/optionWidgets.unconfiguredOptions.test.tsx +0 -150
  236. package/src/widgets/personDisplay.test.ts +0 -113
  237. package/src/widgets/personDisplay.ts +0 -92
  238. package/src/widgets/recentLookups.test.ts +0 -45
  239. package/src/widgets/recentLookups.ts +0 -47
  240. package/src/widgets/toDomProps.ts +0 -204
  241. package/src/widgets/toHostGroupProps.ts +0 -102
  242. package/src/widgets/types.ts +0 -242
  243. package/src/widgets/useCascadingOptions.ts +0 -46
  244. package/src/widgets/useFieldTranslation.ts +0 -132
  245. package/src/widgets/useRecordQuery.test.ts +0 -174
  246. package/src/widgets/useRecordQuery.ts +0 -291
  247. package/src/widgets/useUploadingSignal.test.tsx +0 -62
  248. package/src/widgets/useUploadingSignal.ts +0 -20
  249. package/src/withFieldCarrier.tsx +0 -75
@@ -1,298 +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 boolean field's VISIBLE label is associated with the control
11
- * the widget renders (objectui#3952).
12
- *
13
- * The defect: `<FormControl>` is a Radix `Slot` that hands its child the id
14
- * `<FormLabel htmlFor>` already points at (`_r_N_-form-item`), and
15
- * `BooleanField` REPLACED it with the field name. So the form's visible label
16
- * referenced an id no element carried — clicking it toggled nothing — while the
17
- * widget emitted a SECOND, `sr-only` label of its own carrying the same text.
18
- * The accessible name survived only by that accident.
19
- *
20
- * Two scenarios, pinned separately, because the widget has two hosts with
21
- * opposite needs:
22
- *
23
- * - **hosted** (the form renderer, `ObjectForm`, any `<FormControl>`): the host
24
- * owns the id AND the label. The widget must use the id it was handed and
25
- * must NOT emit a label of its own — that is the duplicate half of the bug.
26
- * - **standalone** (`FieldEditWidget`'s inline grid editor, a bare SDUI node):
27
- * nobody hands an id and nobody renders a label, so the widget's own
28
- * `sr-only` label is the control's ONLY accessible name and must stay.
29
- *
30
- * The widgets are registered raw rather than through `registerAllFields()`,
31
- * which wraps every loader in `React.lazy`: an unbounded module load inside a
32
- * bounded `findBy`/`waitFor` window is the repo's known flake generator
33
- * (AGENTS.md 测试纪律, objectui#3010). Same component, no Suspense boundary.
34
- */
35
-
36
- import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest';
37
- import { render, screen, fireEvent, cleanup } from '@testing-library/react';
38
- import '@testing-library/jest-dom';
39
- import { ComponentRegistry } from '@object-ui/core';
40
- // Module scope: pulls in the form renderer's registration side effect.
41
- import '@object-ui/components';
42
-
43
- import { BooleanField } from '../widgets/BooleanField';
44
-
45
- beforeAll(() => {
46
- ComponentRegistry.register('boolean', BooleanField as any, {
47
- namespace: 'field',
48
- skipFallback: true,
49
- });
50
- }, 30000);
51
-
52
- beforeEach(() => {
53
- if (!(Element.prototype as any).scrollIntoView) {
54
- (Element.prototype as any).scrollIntoView = () => {};
55
- }
56
- });
57
-
58
- afterEach(() => {
59
- cleanup();
60
- vi.restoreAllMocks();
61
- });
62
-
63
- /** The real form renderer hosting one field — the #3952 reproduction. */
64
- function renderForm(field: any, defaultValue: unknown) {
65
- const Form = ComponentRegistry.get('form')!;
66
- return render(
67
- <Form
68
- schema={{
69
- type: 'form',
70
- mode: 'create',
71
- showSubmit: true,
72
- showCancel: false,
73
- submitLabel: 'Create',
74
- defaultValues: { [field.name]: defaultValue },
75
- fields: [field],
76
- onSubmit: () => {},
77
- }}
78
- />,
79
- );
80
- }
81
-
82
- /** Every `<label for=…>` in the document, paired with its resolved target. */
83
- function labelTargets(): Array<{ text: string; forId: string; resolves: boolean }> {
84
- return Array.from(document.querySelectorAll('label'))
85
- .map((el) => ({
86
- text: (el.textContent ?? '').trim(),
87
- forId: el.getAttribute('for') ?? '',
88
- resolves: !!el.getAttribute('for') && !!document.getElementById(el.getAttribute('for')!),
89
- }))
90
- .filter((l) => l.forId !== '');
91
- }
92
-
93
- const switchOf = (name: string): Element => {
94
- const el = document.querySelector(`[data-field="${name}"] [role="switch"]`);
95
- if (!el) throw new Error(`no switch rendered for field "${name}"`);
96
- return el;
97
- };
98
-
99
- const BOOLEAN_FIELD = {
100
- name: 'notifications',
101
- label: 'Email Notifications',
102
- type: 'boolean',
103
- };
104
-
105
- describe('a form-hosted boolean field is labelled by its HOST (objectui#3952)', () => {
106
- it('every label the form emits points at an element that exists', () => {
107
- renderForm(BOOLEAN_FIELD, false);
108
-
109
- const labels = labelTargets();
110
- expect(labels.length).toBeGreaterThan(0);
111
- // A `for` naming an id nothing carries is the defect verbatim: the form's
112
- // visible label used to reference `_r_N_-form-item` while the switch
113
- // carried `notifications`.
114
- expect(labels.filter((l) => !l.resolves)).toEqual([]);
115
- });
116
-
117
- it('the visible label names the switch — one label, associated with the control', () => {
118
- renderForm(BOOLEAN_FIELD, false);
119
-
120
- // `getAllByLabelText` resolves label→control through `for`/id, so it guards
121
- // the ID half: restore the override and it does not merely return the wrong
122
- // node, it throws "found a label … however no form control was found
123
- // associated to that label" — the switch left with NO accessible name,
124
- // which is exactly what #3952 warned would happen the day the widget's own
125
- // label went away. It does NOT guard the duplicate half: two labels
126
- // pointing at ONE control still resolve to one element (measured), so the
127
- // duplicate is pinned by the label-text count below.
128
- const labelled = screen.getAllByLabelText('Email Notifications');
129
- expect(labelled).toHaveLength(1);
130
- expect(labelled[0]).toBe(switchOf('notifications'));
131
- expect(labelled[0]).toHaveAttribute('role', 'switch');
132
- });
133
-
134
- it('the label text appears exactly once in the DOM', () => {
135
- renderForm(BOOLEAN_FIELD, false);
136
-
137
- // The duplicate half of the defect (and why a naive `getByText` on a
138
- // boolean field returned two nodes).
139
- expect(screen.getAllByText('Email Notifications')).toHaveLength(1);
140
- });
141
-
142
- it('activating the visible label toggles the switch', () => {
143
- renderForm(BOOLEAN_FIELD, false);
144
-
145
- const control = switchOf('notifications');
146
- expect(control).toHaveAttribute('aria-checked', 'false');
147
-
148
- // The user-facing symptom of #3952: the normal affordance for a switch row
149
- // is clicking its label. It reaches the control only through the `for`/id
150
- // association the form set up.
151
- fireEvent.click(screen.getByText('Email Notifications'));
152
-
153
- expect(switchOf('notifications')).toHaveAttribute('aria-checked', 'true');
154
- });
155
-
156
- it('the control keeps the HOST id verbatim — the widget does not rename it', () => {
157
- renderForm(BOOLEAN_FIELD, false);
158
-
159
- const control = switchOf('notifications');
160
- const id = control.getAttribute('id') ?? '';
161
- // The host's id is a `useId()`-derived form-item id; the field name is what
162
- // the widget used to substitute for it.
163
- expect(id).not.toBe('');
164
- expect(id).not.toBe('notifications');
165
- expect(id).toMatch(/-form-item$/);
166
- // …and it is the id the visible label points at.
167
- const visible = screen.getByText('Email Notifications');
168
- expect(visible.getAttribute('for')).toBe(id);
169
- });
170
-
171
- it('two boolean fields in one form each get their own label', () => {
172
- const Form = ComponentRegistry.get('form')!;
173
- render(
174
- <Form
175
- schema={{
176
- type: 'form',
177
- mode: 'create',
178
- showSubmit: false,
179
- showCancel: false,
180
- defaultValues: { notifications: false, digest: false },
181
- fields: [BOOLEAN_FIELD, { name: 'digest', label: 'Weekly Digest', type: 'boolean' }],
182
- onSubmit: () => {},
183
- }}
184
- />,
185
- );
186
-
187
- expect(labelTargets().filter((l) => !l.resolves)).toEqual([]);
188
- // Each label reaches its OWN switch (a shared/colliding id would make both
189
- // resolve to the first one).
190
- fireEvent.click(screen.getByText('Weekly Digest'));
191
- expect(switchOf('digest')).toHaveAttribute('aria-checked', 'true');
192
- expect(switchOf('notifications')).toHaveAttribute('aria-checked', 'false');
193
- });
194
- });
195
-
196
- describe('a STANDALONE boolean widget keeps its own accessible name (objectui#3952)', () => {
197
- // `FieldEditWidget` (the grid's inline cell editor) renders these widgets with
198
- // no `id` and no label of its own, so removing the widget's `sr-only` label
199
- // unconditionally would leave the control unnamed. These pin the fallback.
200
-
201
- it('switch variant: the sr-only label names the control', () => {
202
- render(
203
- <BooleanField
204
- value={false}
205
- onChange={() => {}}
206
- field={{ name: 'notifications', label: 'Email Notifications', type: 'boolean' } as any}
207
- />,
208
- );
209
-
210
- expect(labelTargets().filter((l) => !l.resolves)).toEqual([]);
211
- const labelled = screen.getAllByLabelText('Email Notifications');
212
- expect(labelled).toHaveLength(1);
213
- expect(labelled[0]).toHaveAttribute('role', 'switch');
214
- });
215
-
216
- it('checkbox variant: the sr-only label names the control', () => {
217
- render(
218
- <BooleanField
219
- value={false}
220
- onChange={() => {}}
221
- field={
222
- {
223
- name: 'agreed',
224
- label: 'I agree',
225
- type: 'boolean',
226
- widget: 'checkbox',
227
- } as any
228
- }
229
- />,
230
- );
231
-
232
- expect(labelTargets().filter((l) => !l.resolves)).toEqual([]);
233
- const labelled = screen.getAllByLabelText('I agree');
234
- expect(labelled).toHaveLength(1);
235
- expect(labelled[0]).toHaveAttribute('role', 'checkbox');
236
- });
237
- });
238
-
239
- describe('a host that labels by field NAME keeps its association (objectui#3952)', () => {
240
- // `@object-ui/app-shell`'s `ActionParamDialog` boolean branch renders
241
- // `<Label htmlFor={param.name}>` and — unlike its own generic branch, which
242
- // passes `id={param.name}` — hands the widget NO id. It works only because
243
- // the fallback chain's second term derives the id from the field name
244
- // (`paramToField` sets `name: param.name`).
245
- //
246
- // So the chain is `hostId || config.name || useId()`, in that order, and the
247
- // middle term is NOT dead weight to be "cleaned up" into a bare `useId()`:
248
- // that would silently unlink every boolean action param in the console. This
249
- // pin is what makes that attempt fail loudly instead.
250
- it('falls back to the field name when no id was handed down', () => {
251
- render(
252
- <div>
253
- <BooleanField
254
- value={false}
255
- onChange={() => {}}
256
- field={{ name: 'confirmed', label: 'Confirmed', type: 'boolean' } as any}
257
- />
258
- <label htmlFor="confirmed">Send anyway</label>
259
- </div>,
260
- );
261
-
262
- const control = document.querySelector('[role="switch"]')!;
263
- expect(control).toHaveAttribute('id', 'confirmed');
264
- expect(labelTargets().filter((l) => !l.resolves)).toEqual([]);
265
- // The host's OWN label reaches the control through that id.
266
- expect(screen.getAllByLabelText(/Send anyway/)).toHaveLength(1);
267
- });
268
- });
269
-
270
- describe('a host-supplied id wins for BOTH variants (objectui#3952)', () => {
271
- // The checkbox variant is unreachable through the standalone form renderer —
272
- // `resolvedType = widget || field.widget || type` turns `widget: 'checkbox'`
273
- // into the renderer's own BUILTIN `checkbox` branch before the registry is
274
- // consulted — so its host contract is pinned at the prop boundary, exactly
275
- // as `<FormControl>`'s Slot delivers it.
276
- it.each([
277
- ['switch', undefined, 'switch'],
278
- ['checkbox', 'checkbox', 'checkbox'],
279
- ] as const)('%s variant uses the id the host handed it', (_name, widget, role) => {
280
- render(
281
- <label htmlFor="host-item">
282
- Host Label
283
- <BooleanField
284
- value={false}
285
- onChange={() => {}}
286
- id="host-item"
287
- field={{ name: 'agreed', label: 'I agree', type: 'boolean', widget } as any}
288
- />
289
- </label>,
290
- );
291
-
292
- const control = document.querySelector(`[role="${role}"]`)!;
293
- expect(control).toHaveAttribute('id', 'host-item');
294
- // And the widget adds no second label of its own.
295
- expect(screen.queryByText('I agree')).toBeNull();
296
- expect(document.querySelectorAll('label')).toHaveLength(1);
297
- });
298
- });
@@ -1,109 +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
- * Retirement pin — `field:capability-multiselect` (objectui#3308, ADR-0049
11
- * enforce-or-remove).
12
- *
13
- * The key was registered on ONE path only: `registerFields()`, whose sole caller
14
- * was the docs site. The live path (`registerAllFields()`, run at module import)
15
- * never had it, because `fieldWidgetMap` never listed it — so a field authored
16
- * with `widget: 'capability-multiselect'` resolved to nothing in every real app
17
- * while a code comment advertised it as usable from a record form. Nothing
18
- * stamped the hint either: ADR-0056 P1 stamps `permission-facet-link` on all six
19
- * `sys_permission_set` facets (including `system_permissions`), and P2 put the
20
- * capability editor in Studio.
21
- *
22
- * `registerFields()` itself is gone as of objectui#3910 (ruling B of
23
- * objectui#3798): the docs catalog's field examples are form-hosted now, so the
24
- * demo adapter that path existed for has no reason to exist. That REMOVES this
25
- * file's second and third assertions rather than re-spelling them — a test may
26
- * not call a function that no longer exists, and asserting over a deleted path
27
- * would pass for the empty reason (nothing is registered because nothing runs).
28
- * What each one becomes:
29
- *
30
- * 1. absent on the live path — UNCHANGED, and it is now the whole fact. While
31
- * a second path existed this assertion pinned nothing (it passed on
32
- * `origin/main` too, as PR #3793 recorded); with that path deleted, the
33
- * live registry is the only registry, so re-adding the name to
34
- * `fieldWidgetMap` is exactly what turns this red.
35
- * 2. "absent after `registerFields()` too" — DELETED with the path.
36
- * 3. "registers every other docs-path key" — RE-POINTED at the live path,
37
- * which is a superset of the 38 keys the docs path used to register. It
38
- * still pins "the retirement subtracts exactly one name", now against the
39
- * surviving seam instead of a deleted one.
40
- *
41
- * `CapabilityMultiSelectField` the component is deliberately NOT asserted gone:
42
- * Studio's `PermissionMatrixEditor` imports and renders it directly, which is
43
- * ADR-0056 P2's design. Only the widget NAME is retired.
44
- */
45
-
46
- import { describe, it, expect } from 'vitest';
47
- import { ComponentRegistry } from '@object-ui/core';
48
-
49
- // Importing the entry runs `registerAllFields()` at module load — the live path.
50
- import { registerAllFields, FORM_FIELD_TYPES } from '../index';
51
-
52
- const RETIRED = 'capability-multiselect';
53
-
54
- /**
55
- * Every key the retired docs-demo path (`registerFields()`) registered, minus the
56
- * retired one. Spelled out rather than derived so a future edit that silently
57
- * drops one of these registrations fails here instead of passing vacuously.
58
- * The live path registers these AND more (`multiselect`, `radio`, `checkboxes`,
59
- * `tags`, `richtext`, and the widget-hint-only pickers) — this list is the floor
60
- * the retirement promised to leave intact, not the full live surface.
61
- */
62
- const RETAINED_FIELD_KEYS = [
63
- 'text', 'textarea', 'number', 'boolean', 'select', 'date', 'datetime', 'time',
64
- 'email', 'phone', 'url',
65
- 'currency', 'percent', 'password', 'markdown', 'html', 'lookup', 'master_detail',
66
- 'file', 'image', 'location',
67
- 'formula', 'summary', 'auto_number',
68
- 'user', 'owner',
69
- 'object', 'vector', 'grid',
70
- 'color', 'slider', 'rating', 'code', 'avatar', 'address', 'geolocation',
71
- 'signature', 'qrcode',
72
- ] as const;
73
-
74
- describe('capability-multiselect widget retirement (objectui#3308)', () => {
75
- it('is absent on the live registerAllFields() path', () => {
76
- registerAllFields();
77
- expect(ComponentRegistry.get(`field:${RETIRED}`)).toBeUndefined();
78
- expect(ComponentRegistry.get(RETIRED)).toBeUndefined();
79
- });
80
-
81
- it('is not in fieldWidgetMap, so no bare field type can reach it either', () => {
82
- expect(FORM_FIELD_TYPES).not.toContain(RETIRED);
83
- });
84
-
85
- it('registers every retained field key (retirement subtracts exactly one)', () => {
86
- registerAllFields();
87
- for (const key of RETAINED_FIELD_KEYS) {
88
- expect(
89
- ComponentRegistry.get(`field:${key}`),
90
- `field:${key} must survive the retirement`,
91
- ).toBeTruthy();
92
- }
93
- expect(RETAINED_FIELD_KEYS).toHaveLength(38);
94
- });
95
-
96
- it('has no second registration path left to shadow the live one (objectui#3910)', async () => {
97
- // The defect ruling B closed was structural, not cosmetic: `registerFields()`
98
- // re-registered the SAME `field:<type>` keys with demo-only wrappers, so
99
- // whichever ran last won the registry for every consumer sharing it. Pinning
100
- // the export gone is what stops it growing back under a new name.
101
- const entry = (await import('../index')) as Record<string, unknown>;
102
- expect(entry.registerFields).toBeUndefined();
103
- expect(entry.createFieldRenderer).toBeUndefined();
104
- // The surviving seam is still exported — this test must fail loudly if the
105
- // removal ever takes the live path with it.
106
- expect(typeof entry.registerAllFields).toBe('function');
107
- expect(typeof entry.registerField).toBe('function');
108
- });
109
- });
@@ -1,96 +0,0 @@
1
- /**
2
- * Regression for objectstack-ai/objectui#3466: single-line cell renderers
3
- * emitted a BARE INLINE `span.truncate`. An inline box has no width box, so
4
- * `overflow:hidden` / `text-overflow:ellipsis` never engage — the value
5
- * rendered at full content width and its tail was silently clipped by
6
- * whatever ancestor happened to clip (the record-detail card edge), with no
7
- * ellipsis and no way to read the full text (the detail row's `title` is the
8
- * inline-edit hint, not the value). Same mechanism as the JSON cell fix in
9
- * objectui#2578.
10
- *
11
- * Pin: every single-line value renderer emits a BLOCK-level, `max-w-full`
12
- * truncating span that carries the full text in `title` (so hovering the
13
- * value shows the full text, taking precedence over any ancestor `title`).
14
- */
15
- import { describe, it, expect, vi } from 'vitest';
16
- import { render, screen } from '@testing-library/react';
17
- import '@testing-library/jest-dom';
18
- import React from 'react';
19
-
20
- import {
21
- TextCellRenderer,
22
- LookupCellRenderer,
23
- UserCellRenderer,
24
- FileCellRenderer,
25
- SelectCellRenderer,
26
- } from '../index';
27
- import { SchemaRendererProvider } from '@object-ui/react';
28
-
29
- const LONG =
30
- 'A remarkably long referenced-record title that a detail card column can never fit on a single line without an ellipsis';
31
-
32
- /** The block-level truncation contract every single-line value span must meet. */
33
- function expectTruncating(el: HTMLElement, fullText: string) {
34
- expect(el).toHaveClass('block', 'max-w-full', 'truncate');
35
- expect(el).toHaveAttribute('title', fullText);
36
- }
37
-
38
- describe('cell renderers truncate for real and expose the full text (issue #3466)', () => {
39
- it('TextCellRenderer: block-level truncating span with title fallback', () => {
40
- render(<TextCellRenderer value={LONG} field={{ type: 'text' } as any} />);
41
- expectTruncating(screen.getByText(LONG), LONG);
42
- });
43
-
44
- it('LookupCellRenderer: expanded record object', () => {
45
- const ds = { find: vi.fn(), findOne: vi.fn() } as any;
46
- render(
47
- <SchemaRendererProvider dataSource={ds}>
48
- <LookupCellRenderer
49
- value={{ id: 'rec1', name: LONG }}
50
- field={{ type: 'lookup', reference_to: 'obj_a' } as any}
51
- />
52
- </SchemaRendererProvider>,
53
- );
54
- expectTruncating(screen.getByText(LONG), LONG);
55
- });
56
-
57
- it('LookupCellRenderer: primitive non-opaque value', () => {
58
- const ds = { find: vi.fn(), findOne: vi.fn() } as any;
59
- render(
60
- <SchemaRendererProvider dataSource={ds}>
61
- <LookupCellRenderer value={LONG} field={{ type: 'lookup' } as any} />
62
- </SchemaRendererProvider>,
63
- );
64
- expectTruncating(screen.getByText(LONG), LONG);
65
- });
66
-
67
- it('UserCellRenderer: display name beside the avatar', () => {
68
- render(<UserCellRenderer value={{ name: LONG }} field={{ type: 'user' } as any} />);
69
- expectTruncating(screen.getByText(LONG), LONG);
70
- });
71
-
72
- it('FileCellRenderer: single file name', () => {
73
- render(<FileCellRenderer value={{ name: LONG }} field={{ type: 'file' } as any} />);
74
- expectTruncating(screen.getByText(LONG), LONG);
75
- });
76
-
77
- it('SelectCellRenderer (dot): bounded container with title, shrinkable label', () => {
78
- render(
79
- <SelectCellRenderer
80
- value="opt1"
81
- field={{
82
- type: 'select',
83
- appearance: 'dot',
84
- options: [{ value: 'opt1', label: LONG }],
85
- } as any}
86
- />,
87
- );
88
- const label = screen.getByText(LONG);
89
- // The label shrinks inside the dot row; the row itself is width-bounded
90
- // and carries the full text on hover.
91
- expect(label).toHaveClass('min-w-0', 'truncate');
92
- const row = label.parentElement!;
93
- expect(row).toHaveClass('max-w-full');
94
- expect(row).toHaveAttribute('title', LONG);
95
- });
96
- });