@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.
Files changed (265) hide show
  1. package/CHANGELOG.md +1655 -0
  2. package/README.md +1 -12
  3. package/dist/FieldEditWidget.d.ts +23 -3
  4. package/dist/currency.d.ts +46 -0
  5. package/dist/field-type-alias.d.ts +35 -1
  6. package/dist/index.cjs +4 -4
  7. package/dist/index.css +651 -0
  8. package/dist/index.d.ts +474 -85
  9. package/dist/index.js +5418 -5023
  10. package/dist/widgets/AddressField.d.ts +4 -12
  11. package/dist/widgets/AutoNumberField.d.ts +1 -1
  12. package/dist/widgets/AvatarField.d.ts +1 -1
  13. package/dist/widgets/BooleanField.d.ts +1 -1
  14. package/dist/widgets/CapabilityMultiSelectField.d.ts +1 -1
  15. package/dist/widgets/CheckboxesField.d.ts +1 -1
  16. package/dist/widgets/CodeField.d.ts +1 -1
  17. package/dist/widgets/ColorField.d.ts +1 -1
  18. package/dist/widgets/CurrencyField.d.ts +1 -1
  19. package/dist/widgets/DateField.d.ts +1 -1
  20. package/dist/widgets/DateTimeField.d.ts +1 -1
  21. package/dist/widgets/EmailField.d.ts +1 -1
  22. package/dist/widgets/FileField.d.ts +18 -2
  23. package/dist/widgets/FilterConditionField.d.ts +28 -1
  24. package/dist/widgets/FormulaField.d.ts +1 -1
  25. package/dist/widgets/FullscreenFieldEditor.d.ts +49 -152
  26. package/dist/widgets/GeolocationField.d.ts +1 -1
  27. package/dist/widgets/GridField.d.ts +68 -6
  28. package/dist/widgets/ImageField.d.ts +1 -1
  29. package/dist/widgets/LocationField.d.ts +1 -1
  30. package/dist/widgets/LookupField.d.ts +1 -1
  31. package/dist/widgets/MasterDetailField.d.ts +1 -1
  32. package/dist/widgets/MultiSelectField.d.ts +1 -1
  33. package/dist/widgets/NumberField.d.ts +1 -1
  34. package/dist/widgets/ObjectField.d.ts +1 -1
  35. package/dist/widgets/ObjectRefField.d.ts +1 -1
  36. package/dist/widgets/OptionsEmptyState.d.ts +21 -1
  37. package/dist/widgets/PasswordField.d.ts +1 -1
  38. package/dist/widgets/PercentField.d.ts +1 -1
  39. package/dist/widgets/PhoneField.d.ts +1 -1
  40. package/dist/widgets/QRCodeField.d.ts +1 -1
  41. package/dist/widgets/RadioField.d.ts +1 -1
  42. package/dist/widgets/RatingField.d.ts +1 -1
  43. package/dist/widgets/RecipientPickerField.d.ts +1 -1
  44. package/dist/widgets/RecordPickerDialog.d.ts +34 -10
  45. package/dist/widgets/RichTextField.d.ts +37 -1
  46. package/dist/widgets/SelectField.d.ts +26 -6
  47. package/dist/widgets/SignatureField.d.ts +32 -2
  48. package/dist/widgets/SliderField.d.ts +37 -2
  49. package/dist/widgets/SummaryField.d.ts +1 -1
  50. package/dist/widgets/TagsField.d.ts +1 -1
  51. package/dist/widgets/TextAreaField.d.ts +6 -2
  52. package/dist/widgets/TextField.d.ts +1 -1
  53. package/dist/widgets/TimeField.d.ts +1 -1
  54. package/dist/widgets/UrlField.d.ts +1 -1
  55. package/dist/widgets/UserField.d.ts +1 -1
  56. package/dist/widgets/VectorField.d.ts +1 -1
  57. package/dist/widgets/_cell-renderer-bridge.d.ts +1 -1
  58. package/dist/widgets/address-format.d.ts +88 -0
  59. package/dist/widgets/file-size-guard.d.ts +41 -0
  60. package/dist/widgets/lookupColumnDisplay.d.ts +93 -0
  61. package/dist/widgets/nativeDateValue.d.ts +37 -0
  62. package/dist/widgets/richTextDisplay.d.ts +80 -0
  63. package/dist/widgets/toDomProps.d.ts +30 -10
  64. package/dist/widgets/toHostGroupProps.d.ts +189 -0
  65. package/dist/widgets/types.d.ts +8 -3
  66. package/dist/widgets/useRecordQuery.d.ts +7 -1
  67. package/package.json +17 -14
  68. package/dist/FieldEditWidget.test.d.ts +0 -8
  69. package/dist/__tests__/DateCellRenderer.test.d.ts +0 -0
  70. package/dist/__tests__/NumberCellRenderer.test.d.ts +0 -0
  71. package/dist/__tests__/cell-truncation.test.d.ts +0 -0
  72. package/dist/__tests__/field-carrier-sdui.test.d.ts +0 -0
  73. package/dist/__tests__/lookupCellDisplayField.test.d.ts +0 -0
  74. package/dist/__tests__/lookupCellDisplayName.test.d.ts +0 -0
  75. package/dist/__tests__/lookupCellReferenceKey.test.d.ts +0 -0
  76. package/dist/__tests__/registry-collision.test.d.ts +0 -0
  77. package/dist/__tests__/resolveFieldCurrency.test.d.ts +0 -1
  78. package/dist/__tests__/spec-symbol-batch7.test.d.ts +0 -8
  79. package/dist/__tests__/validation-feedback.test.d.ts +0 -0
  80. package/dist/__tests__/widget-aria-invalid-e2e.test.d.ts +0 -0
  81. package/dist/__tests__/widget-aria-invalid-registry-e2e.test.d.ts +0 -0
  82. package/dist/__tests__/widget-dom-leak-e2e.test.d.ts +0 -0
  83. package/dist/__tests__/widget-props-contract.test.d.ts +0 -8
  84. package/dist/coerce-safe-value.test.d.ts +0 -1
  85. package/dist/complex-widgets.test.d.ts +0 -1
  86. package/dist/datetime-widgets.test.d.ts +0 -1
  87. package/dist/field-type-coverage.test.d.ts +0 -1
  88. package/dist/standard-widgets.test.d.ts +0 -0
  89. package/dist/widgets/AddressField.uniqueIds.test.d.ts +0 -0
  90. package/dist/widgets/CapabilityMultiSelectField.test.d.ts +0 -0
  91. package/dist/widgets/CharacterCount.d.ts +0 -56
  92. package/dist/widgets/CheckboxesField.cascade.test.d.ts +0 -0
  93. package/dist/widgets/GeolocationField.uniqueIds.test.d.ts +0 -0
  94. package/dist/widgets/GridField.test.d.ts +0 -1
  95. package/dist/widgets/ImageLightbox.test.d.ts +0 -1
  96. package/dist/widgets/LookupField.dependsOn.test.d.ts +0 -8
  97. package/dist/widgets/LookupField.gateHintLabel.test.d.ts +0 -8
  98. package/dist/widgets/LookupField.hydration.test.d.ts +0 -1
  99. package/dist/widgets/LookupField.idField.test.d.ts +0 -1
  100. package/dist/widgets/MultiSelectField.cascade.test.d.ts +0 -0
  101. package/dist/widgets/OptionsEmptyState.no-provider.test.d.ts +0 -0
  102. package/dist/widgets/OptionsEmptyState.test.d.ts +0 -0
  103. package/dist/widgets/PeoplePicker.test.d.ts +0 -8
  104. package/dist/widgets/PeoplePickerComponents.test.d.ts +0 -8
  105. package/dist/widgets/RadioField.cascade.test.d.ts +0 -0
  106. package/dist/widgets/RecipientPickerField.test.d.ts +0 -0
  107. package/dist/widgets/RecordPickerDialog.filterOptions.test.d.ts +0 -8
  108. package/dist/widgets/RecordPickerDialog.selectLabel.test.d.ts +0 -8
  109. package/dist/widgets/SelectField.cascade.test.d.ts +0 -0
  110. package/dist/widgets/TagsField.placeholder.no-provider.test.d.ts +0 -0
  111. package/dist/widgets/TagsField.placeholder.test.d.ts +0 -0
  112. package/dist/widgets/UserFieldPicker.test.d.ts +0 -8
  113. package/dist/widgets/__tests__/FilterConditionField.matchAll.test.d.ts +0 -8
  114. package/dist/widgets/__tests__/FilterConditionField.operators.test.d.ts +0 -8
  115. package/dist/widgets/__tests__/FullscreenFieldEditor.i18n.test.d.ts +0 -0
  116. package/dist/widgets/__tests__/FullscreenFieldEditor.no-provider.test.d.ts +0 -0
  117. package/dist/widgets/__tests__/RichTextField.fullscreenDisabled.test.d.ts +0 -0
  118. package/dist/widgets/__tests__/RichTextField.mobileFullscreen.test.d.ts +0 -0
  119. package/dist/widgets/__tests__/TextAreaField.characterCount.announcements.test.d.ts +0 -0
  120. package/dist/widgets/__tests__/TextAreaField.characterCount.i18n.test.d.ts +0 -0
  121. package/dist/widgets/__tests__/TextAreaField.characterCount.no-provider.test.d.ts +0 -0
  122. package/dist/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.d.ts +0 -0
  123. package/dist/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.d.ts +0 -0
  124. package/dist/widgets/__tests__/TextAreaField.fullscreenDisabled.test.d.ts +0 -0
  125. package/dist/widgets/__tests__/TextAreaField.mobileFullscreen.test.d.ts +0 -0
  126. package/dist/widgets/deriveLookupColumns.test.d.ts +0 -1
  127. package/dist/widgets/file-value.test.d.ts +0 -1
  128. package/dist/widgets/personDisplay.test.d.ts +0 -8
  129. package/dist/widgets/recentLookups.test.d.ts +0 -1
  130. package/dist/widgets/useRecordQuery.test.d.ts +0 -8
  131. package/dist/widgets/useUploadingSignal.test.d.ts +0 -1
  132. package/src/FieldEditWidget.test.ts +0 -133
  133. package/src/FieldEditWidget.tsx +0 -191
  134. package/src/__tests__/DateCellRenderer.test.tsx +0 -73
  135. package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
  136. package/src/__tests__/cell-truncation.test.tsx +0 -96
  137. package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
  138. package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
  139. package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
  140. package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
  141. package/src/__tests__/registry-collision.test.ts +0 -91
  142. package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
  143. package/src/__tests__/spec-symbol-batch7.test.ts +0 -126
  144. package/src/__tests__/validation-feedback.test.tsx +0 -361
  145. package/src/__tests__/widget-aria-invalid-e2e.test.tsx +0 -265
  146. package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +0 -338
  147. package/src/__tests__/widget-dom-leak-e2e.test.tsx +0 -598
  148. package/src/__tests__/widget-props-contract.test.tsx +0 -142
  149. package/src/coerce-safe-value.test.ts +0 -34
  150. package/src/complex-widgets.test.tsx +0 -908
  151. package/src/currency.ts +0 -14
  152. package/src/datetime-widgets.test.tsx +0 -93
  153. package/src/field-type-alias.ts +0 -100
  154. package/src/field-type-coverage.test.ts +0 -67
  155. package/src/index.tsx +0 -2503
  156. package/src/standard-widgets.test.tsx +0 -357
  157. package/src/widgets/AddressField.tsx +0 -132
  158. package/src/widgets/AddressField.uniqueIds.test.tsx +0 -92
  159. package/src/widgets/AutoNumberField.tsx +0 -18
  160. package/src/widgets/AvatarField.tsx +0 -117
  161. package/src/widgets/BooleanField.tsx +0 -52
  162. package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
  163. package/src/widgets/CapabilityMultiSelectField.tsx +0 -229
  164. package/src/widgets/CharacterCount.tsx +0 -263
  165. package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
  166. package/src/widgets/CheckboxesField.tsx +0 -136
  167. package/src/widgets/CodeField.tsx +0 -41
  168. package/src/widgets/ColorField.tsx +0 -53
  169. package/src/widgets/CurrencyField.tsx +0 -95
  170. package/src/widgets/DateField.tsx +0 -31
  171. package/src/widgets/DateTimeField.tsx +0 -37
  172. package/src/widgets/EmailField.tsx +0 -37
  173. package/src/widgets/FileField.tsx +0 -494
  174. package/src/widgets/FilterConditionField.tsx +0 -454
  175. package/src/widgets/FormulaField.tsx +0 -33
  176. package/src/widgets/FullscreenFieldEditor.tsx +0 -271
  177. package/src/widgets/GeolocationField.tsx +0 -167
  178. package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
  179. package/src/widgets/GridField.test.tsx +0 -406
  180. package/src/widgets/GridField.tsx +0 -966
  181. package/src/widgets/ImageCropperDialog.tsx +0 -245
  182. package/src/widgets/ImageField.tsx +0 -255
  183. package/src/widgets/ImageLightbox.test.tsx +0 -52
  184. package/src/widgets/ImageLightbox.tsx +0 -108
  185. package/src/widgets/LocationField.tsx +0 -57
  186. package/src/widgets/LookupField.dependsOn.test.tsx +0 -233
  187. package/src/widgets/LookupField.gateHintLabel.test.tsx +0 -135
  188. package/src/widgets/LookupField.hydration.test.tsx +0 -136
  189. package/src/widgets/LookupField.idField.test.tsx +0 -61
  190. package/src/widgets/LookupField.tsx +0 -1291
  191. package/src/widgets/MarkdownContent.tsx +0 -19
  192. package/src/widgets/MasterDetailField.tsx +0 -131
  193. package/src/widgets/MultiSelectField.cascade.test.tsx +0 -143
  194. package/src/widgets/MultiSelectField.tsx +0 -142
  195. package/src/widgets/NumberField.tsx +0 -49
  196. package/src/widgets/ObjectField.tsx +0 -94
  197. package/src/widgets/ObjectRefField.tsx +0 -121
  198. package/src/widgets/OptionsEmptyState.no-provider.test.tsx +0 -88
  199. package/src/widgets/OptionsEmptyState.test.tsx +0 -169
  200. package/src/widgets/OptionsEmptyState.tsx +0 -83
  201. package/src/widgets/PasswordField.tsx +0 -49
  202. package/src/widgets/PeoplePicker.test.tsx +0 -360
  203. package/src/widgets/PeoplePicker.tsx +0 -579
  204. package/src/widgets/PeoplePickerComponents.test.tsx +0 -104
  205. package/src/widgets/PercentField.tsx +0 -95
  206. package/src/widgets/PersonRow.tsx +0 -117
  207. package/src/widgets/PhoneField.tsx +0 -37
  208. package/src/widgets/QRCodeField.tsx +0 -103
  209. package/src/widgets/RadioField.cascade.test.tsx +0 -141
  210. package/src/widgets/RadioField.tsx +0 -111
  211. package/src/widgets/RatingField.tsx +0 -81
  212. package/src/widgets/RecipientPickerField.test.tsx +0 -157
  213. package/src/widgets/RecipientPickerField.tsx +0 -188
  214. package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
  215. package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
  216. package/src/widgets/RecordPickerDialog.tsx +0 -1316
  217. package/src/widgets/RichTextField.tsx +0 -176
  218. package/src/widgets/SelectField.cascade.test.tsx +0 -138
  219. package/src/widgets/SelectField.tsx +0 -162
  220. package/src/widgets/SelectionTray.tsx +0 -110
  221. package/src/widgets/SignatureField.tsx +0 -140
  222. package/src/widgets/SliderField.tsx +0 -39
  223. package/src/widgets/SummaryField.tsx +0 -31
  224. package/src/widgets/TagsField.placeholder.no-provider.test.tsx +0 -68
  225. package/src/widgets/TagsField.placeholder.test.tsx +0 -95
  226. package/src/widgets/TagsField.tsx +0 -88
  227. package/src/widgets/TextAreaField.tsx +0 -213
  228. package/src/widgets/TextField.tsx +0 -45
  229. package/src/widgets/TimeField.tsx +0 -31
  230. package/src/widgets/UrlField.tsx +0 -46
  231. package/src/widgets/UserField.tsx +0 -58
  232. package/src/widgets/UserFieldPicker.test.tsx +0 -78
  233. package/src/widgets/VectorField.tsx +0 -29
  234. package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +0 -66
  235. package/src/widgets/__tests__/FilterConditionField.operators.test.ts +0 -142
  236. package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +0 -248
  237. package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +0 -116
  238. package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +0 -244
  239. package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +0 -195
  240. package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +0 -417
  241. package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +0 -287
  242. package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +0 -160
  243. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +0 -161
  244. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +0 -346
  245. package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +0 -246
  246. package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +0 -175
  247. package/src/widgets/_cell-renderer-bridge.ts +0 -19
  248. package/src/widgets/deriveLookupColumns.test.ts +0 -112
  249. package/src/widgets/deriveLookupColumns.ts +0 -108
  250. package/src/widgets/file-value.test.ts +0 -230
  251. package/src/widgets/file-value.ts +0 -246
  252. package/src/widgets/openNativePicker.ts +0 -18
  253. package/src/widgets/personDisplay.test.ts +0 -113
  254. package/src/widgets/personDisplay.ts +0 -92
  255. package/src/widgets/recentLookups.test.ts +0 -45
  256. package/src/widgets/recentLookups.ts +0 -47
  257. package/src/widgets/toDomProps.ts +0 -175
  258. package/src/widgets/types.ts +0 -242
  259. package/src/widgets/useCascadingOptions.ts +0 -46
  260. package/src/widgets/useFieldTranslation.ts +0 -150
  261. package/src/widgets/useRecordQuery.test.ts +0 -174
  262. package/src/widgets/useRecordQuery.ts +0 -264
  263. package/src/widgets/useUploadingSignal.test.tsx +0 -62
  264. package/src/widgets/useUploadingSignal.ts +0 -20
  265. package/src/withFieldCarrier.tsx +0 -75
@@ -1,265 +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 REAL widget, inside the REAL form renderer, announces an
11
- * invalid field to assistive tech (objectui#3222).
12
- *
13
- * The two halves of this contract have unit tests of their own — the widgets
14
- * mark themselves invalid when handed an `error`
15
- * (`validation-feedback.test.tsx`), and the form renderer produces one
16
- * (`form-error-delivery.test.tsx` in `@object-ui/components`). Neither alone
17
- * would have caught the actual defect, because the defect was precisely that
18
- * the two halves never met: the widgets read `errorMessage`, the spec's
19
- * published contract said `error`, and NOTHING in `packages/` or `apps/`
20
- * produced either. Seven widgets computed `aria-invalid={!!errorMessage}` from
21
- * a value that was `undefined` forever, so `aria-invalid` had never once been
22
- * set on a failing field.
23
- *
24
- * So this file joins them: real react-hook-form validation, real widget, one
25
- * assertion per widget that the control is `aria-invalid="true"` after the
26
- * failure and `"false"` before it. `aria-invalid="false"` up front is the load
27
- * bearing half of each pair — `<FormControl>` is a Radix `Slot` that hands its
28
- * child a correct `aria-invalid`, and the widget's own attribute is written
29
- * AFTER the props spread, so it wins. A widget computing it from a prop nobody
30
- * passes does not merely miss the signal, it OVERWRITES the correct one with
31
- * `false`.
32
- *
33
- * The widgets are registered raw rather than through `registerAllFields()`,
34
- * which wraps every loader in `React.lazy`: an unbounded module load inside a
35
- * bounded `findBy`/`waitFor` window is the repo's known flake generator
36
- * (AGENTS.md 测试纪律, objectui#3010). Same component, no Suspense boundary.
37
- */
38
-
39
- import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest';
40
- import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react';
41
- import '@testing-library/jest-dom';
42
- import { ComponentRegistry } from '@object-ui/core';
43
- // Module scope: pulls in the form renderer's registration side effect.
44
- import '@object-ui/components';
45
-
46
- import { EmailField } from '../widgets/EmailField';
47
- import { PhoneField } from '../widgets/PhoneField';
48
- import { UrlField } from '../widgets/UrlField';
49
- import { TextAreaField } from '../widgets/TextAreaField';
50
- import { CurrencyField } from '../widgets/CurrencyField';
51
- import { PercentField } from '../widgets/PercentField';
52
- import { RichTextField } from '../widgets/RichTextField';
53
- import { TextField } from '../widgets/TextField';
54
- import { SelectField } from '../widgets/SelectField';
55
-
56
- /**
57
- * The widgets that read the validation slot, by their form-path key: the seven
58
- * from objectui#3222, plus `select` (objectui#3306) — the widget whose control
59
- * is a Radix trigger rather than a native input, where every aria-* used to be
60
- * swallowed by the non-DOM `Select.Root`.
61
- */
62
- const WIDGETS = [
63
- ['email', EmailField],
64
- ['phone', PhoneField],
65
- ['url', UrlField],
66
- ['textarea', TextAreaField],
67
- ['currency', CurrencyField],
68
- ['percent', PercentField],
69
- ['markdown', RichTextField],
70
- ['select', SelectField],
71
- ] as const;
72
-
73
- beforeAll(() => {
74
- for (const [type, Widget] of WIDGETS) {
75
- ComponentRegistry.register(type, Widget as any, { namespace: 'field', skipFallback: true });
76
- }
77
- }, 30000);
78
-
79
- beforeEach(() => {
80
- if (!(Element.prototype as any).scrollIntoView) {
81
- (Element.prototype as any).scrollIntoView = () => {};
82
- }
83
- });
84
-
85
- afterEach(() => {
86
- cleanup();
87
- vi.restoreAllMocks();
88
- });
89
-
90
- function renderForm(field: any, defaultValue: unknown) {
91
- const Form = ComponentRegistry.get('form')!;
92
- return render(
93
- <Form
94
- schema={{
95
- type: 'form',
96
- mode: 'create',
97
- showSubmit: true,
98
- showCancel: false,
99
- submitLabel: 'Create',
100
- defaultValues: { [field.name]: defaultValue },
101
- fields: [field],
102
- onSubmit: () => {},
103
- }}
104
- />,
105
- );
106
- }
107
-
108
- /**
109
- * The control the widget rendered, located through the form row rather than by
110
- * role — the seven widgets render `input[type=email|tel|url|number]` and
111
- * `textarea` between them, and the point of the assertion is the ATTRIBUTE,
112
- * not which element type carries it.
113
- */
114
- function controlOf(name: string): Element {
115
- const control = document.querySelector(
116
- `[data-field="${name}"] input, [data-field="${name}"] textarea`,
117
- );
118
- if (!control) throw new Error(`no control rendered for field "${name}"`);
119
- return control;
120
- }
121
-
122
- const submit = () => fireEvent.click(screen.getByRole('button', { name: /create/i }));
123
-
124
- describe('field widgets announce an invalid field to AT (objectui#3222)', () => {
125
- // `field:textarea` because the bare `textarea` key is a BUILTIN_FIELD_TYPE in
126
- // the form renderer and never reaches the registry — it is the prefixed id
127
- // (`mapFieldTypeToFormType`) that resolves to this package's widget.
128
- it.each([
129
- ['email', 'email', ''],
130
- ['phone', 'phone', ''],
131
- ['url', 'url', ''],
132
- ['field:textarea', 'notes', ''],
133
- ['currency', 'amount', null],
134
- ['percent', 'ratio', null],
135
- ['markdown', 'body', ''],
136
- ] as const)(
137
- 'a required %s field is aria-invalid only AFTER validation fails',
138
- async (type, name, emptyValue) => {
139
- renderForm({ name, label: name, type, required: true }, emptyValue);
140
-
141
- // Before the submit there is no error, and the widget must say so —
142
- // this is the half that a Slot-provided `aria-invalid` cannot fake.
143
- expect(controlOf(name)).toHaveAttribute('aria-invalid', 'false');
144
-
145
- submit();
146
-
147
- await waitFor(() => {
148
- expect(controlOf(name)).toHaveAttribute('aria-invalid', 'true');
149
- });
150
- },
151
- );
152
-
153
- it('the message itself is rendered once, by the form — not by the widget', async () => {
154
- // Responsibility split (the ruling's scope item 3): the widget consumes
155
- // `error` for `aria-invalid` ONLY. `<FormMessage/>` owns the text; a widget
156
- // that printed it too would show the user the same sentence twice.
157
- renderForm({ name: 'email', label: 'Email', type: 'email', required: true }, '');
158
-
159
- submit();
160
-
161
- await waitFor(() => expect(screen.getAllByText('Email is required')).toHaveLength(1));
162
- // And the message is NOT inside the control the widget rendered.
163
- expect(controlOf('email')).not.toHaveTextContent('Email is required');
164
- });
165
-
166
- it('a widget that does NOT read `error` is still marked invalid, by the Slot', async () => {
167
- // The counterpart that makes the diagnosis precise: `TextField` never
168
- // computed its own `aria-invalid`, so `<FormControl>`'s Slot value reached
169
- // its input untouched and it announced invalid fields correctly all along.
170
- // Only the seven widgets that DID compute one were broken — they wrote
171
- // theirs after the props spread, so `!!undefined` won.
172
- //
173
- // Pinned because the obvious "cleanup" here is to stop forwarding `error`
174
- // to widgets that do not declare a use for it. That cannot be known (a
175
- // third-party widget is exactly the case the contract exists for), and
176
- // withholding it would recreate declared-≠-delivered one widget at a time.
177
- ComponentRegistry.register('plaintext', TextField as any, { namespace: 'field', skipFallback: true });
178
- renderForm({ name: 'title', label: 'Title', type: 'plaintext', required: true }, '');
179
-
180
- submit();
181
-
182
- await waitFor(() => expect(controlOf('title')).toHaveAttribute('aria-invalid', 'true'));
183
- });
184
-
185
- it('clears the invalid state when the field becomes valid again', async () => {
186
- renderForm({ name: 'email', label: 'Email', type: 'email', required: true }, '');
187
-
188
- submit();
189
- await waitFor(() => expect(controlOf('email')).toHaveAttribute('aria-invalid', 'true'));
190
-
191
- fireEvent.change(controlOf('email'), { target: { value: 'a@example.com' } });
192
-
193
- await waitFor(() => expect(controlOf('email')).toHaveAttribute('aria-invalid', 'false'));
194
- });
195
- });
196
-
197
- describe('field:select announces validation state on its TRIGGER (objectui#3306)', () => {
198
- // Radix `Select.Root` renders no DOM element and silently drops every prop
199
- // it does not recognise. The widget used to spread its DOM pass-through onto
200
- // Root, so `aria-invalid` / `aria-describedby` / `aria-required` all
201
- // vanished: a failed required select showed the red message while assistive
202
- // tech was told nothing. The real control is the focusable
203
- // `<button role="combobox">` the trigger renders — that is where every
204
- // assertion below reads from, never a wrapper.
205
- //
206
- // `field:select` (not bare `select`) because `select` is a
207
- // BUILTIN_FIELD_TYPE in the form renderer and never reaches the registry —
208
- // same reason as `field:textarea` above.
209
- const OPTIONS = [
210
- { label: 'Alpha', value: 'alpha' },
211
- { label: 'Beta', value: 'beta' },
212
- ];
213
-
214
- const selectField = {
215
- name: 'choice',
216
- label: 'Choice',
217
- type: 'field:select',
218
- required: true,
219
- options: OPTIONS,
220
- };
221
-
222
- /** The trigger button, by role — fails loudly if it stops being a combobox. */
223
- function trigger(): Element {
224
- const el = document.querySelector('[data-field="choice"] [role="combobox"]');
225
- if (!el) throw new Error('no select trigger rendered for field "choice"');
226
- return el;
227
- }
228
-
229
- it('a required select is aria-invalid only AFTER validation fails, on the trigger', async () => {
230
- renderForm(selectField, null);
231
-
232
- // The explicit "false" half is load-bearing, same as the widgets above: a
233
- // valid field SAYS it is valid rather than staying mute.
234
- expect(trigger()).toHaveAttribute('aria-invalid', 'false');
235
-
236
- submit();
237
-
238
- // Prove the failure actually rendered before reading aria off it — an
239
- // error variant that silently produces no error tests nothing.
240
- await waitFor(() =>
241
- expect(screen.getAllByText('Choice is required')).toHaveLength(1),
242
- );
243
- expect(trigger()).toHaveAttribute('aria-invalid', 'true');
244
- });
245
-
246
- it('carries aria-required on the trigger — the state channel of objectui#3290', () => {
247
- renderForm(selectField, null);
248
-
249
- // Root would swallow it; the trigger is "the control" for a Radix select.
250
- expect(trigger()).toHaveAttribute('aria-required', 'true');
251
- });
252
-
253
- it('aria-describedby on the trigger references the rendered error message', async () => {
254
- renderForm(selectField, null);
255
-
256
- submit();
257
-
258
- const message = await screen.findByText('Choice is required');
259
- // The association is only real if the id chain closes: the trigger must
260
- // point at the exact element carrying the message text.
261
- expect(message.id).not.toBe('');
262
- const describedBy = trigger().getAttribute('aria-describedby') ?? '';
263
- expect(describedBy.split(/\s+/)).toContain(message.id);
264
- });
265
- });
@@ -1,338 +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
- * GATE: every registered field widget must announce a validation failure to
11
- * assistive tech — `aria-invalid="true"` on some element inside its own form
12
- * row (objectui#3306, the paradigm of objectui#3291's leak sweep).
13
- *
14
- * ## Why a registry-wide sweep
15
- *
16
- * This failure class recurs per widget: objectui#3222 fixed seven widgets that
17
- * computed `aria-invalid` from a prop nobody produced; objectui#3290 fixed the
18
- * `aria-required` state channel; objectui#3306 fixed `select`, whose Radix
19
- * `Select.Root` — not being a DOM element — silently swallowed every `aria-*`
20
- * the form renderer delivered. Each was invisible until someone happened to
21
- * audit that one widget, because the per-widget tests only cover the widgets
22
- * someone already fixed. This sweep covers the REGISTRY, so the next widget
23
- * with a swallowed or missing `aria-invalid` fails here by name instead of
24
- * shipping.
25
- *
26
- * ## The ratchet
27
- *
28
- * Widgets listed in {@link NOT_YET_DELIVERED} are known not to deliver the
29
- * attribute today. They are asserted in the OPPOSITE direction — the sweep
30
- * proves they still do NOT deliver — so fixing one turns its entry red and
31
- * forces its removal from the ledger. The ledger can only shrink; a widget can
32
- * never silently regress INTO it, and a fix can never silently go unrecorded.
33
- * The remaining entries are tracked as follow-up work (see the ledger's doc
34
- * comment); this file is the source of truth for what is left.
35
- *
36
- * ## Discipline inherited from the #3291 sweep
37
- *
38
- * - **An error variant that produces no error tests nothing** — every case
39
- * asserts the "is required" message rendered before it reads any aria. The
40
- * `required` check is a PRESENCE check (`null` is missing, `false`/`0` are
41
- * values — cloud#972), so `null` drives a real failure for every type.
42
- * - **Popovers are not opened** (happy-dom lacks the pointer-capture APIs
43
- * Radix needs); the inline control every widget renders is the surface under
44
- * test, which is exactly where `aria-invalid` must sit.
45
- * - Widgets are registered from STATIC imports wrapped in `withFieldCarrier`
46
- * (the real registration seam, objectui#3233), never `registerAllFields()`,
47
- * whose `React.lazy` loaders are this repo's known flake generator
48
- * (AGENTS.md 测试纪律 / objectui#3010).
49
- */
50
-
51
- import type { ComponentType } from 'react';
52
- import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest';
53
- import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react';
54
- import '@testing-library/jest-dom';
55
- import { ComponentRegistry } from '@object-ui/core';
56
- // Module scope: pulls in the form renderer's registration side effect.
57
- import '@object-ui/components';
58
-
59
- import { withFieldCarrier } from '../withFieldCarrier';
60
- import { FORM_FIELD_TYPES } from '../index';
61
-
62
- import { TextField } from '../widgets/TextField';
63
- import { TextAreaField } from '../widgets/TextAreaField';
64
- import { NumberField } from '../widgets/NumberField';
65
- import { BooleanField } from '../widgets/BooleanField';
66
- import { SelectField } from '../widgets/SelectField';
67
- import { DateField } from '../widgets/DateField';
68
- import { DateTimeField } from '../widgets/DateTimeField';
69
- import { TimeField } from '../widgets/TimeField';
70
- import { EmailField } from '../widgets/EmailField';
71
- import { PhoneField } from '../widgets/PhoneField';
72
- import { UrlField } from '../widgets/UrlField';
73
- import { MultiSelectField } from '../widgets/MultiSelectField';
74
- import { RadioField } from '../widgets/RadioField';
75
- import { CheckboxesField } from '../widgets/CheckboxesField';
76
- import { TagsField } from '../widgets/TagsField';
77
- import { CurrencyField } from '../widgets/CurrencyField';
78
- import { PercentField } from '../widgets/PercentField';
79
- import { PasswordField } from '../widgets/PasswordField';
80
- import { RichTextField } from '../widgets/RichTextField';
81
- import { LookupField } from '../widgets/LookupField';
82
- import { FileField } from '../widgets/FileField';
83
- import { ImageField } from '../widgets/ImageField';
84
- import { LocationField } from '../widgets/LocationField';
85
- import { FormulaField } from '../widgets/FormulaField';
86
- import { SummaryField } from '../widgets/SummaryField';
87
- import { AutoNumberField } from '../widgets/AutoNumberField';
88
- import { UserField } from '../widgets/UserField';
89
- import { ObjectField } from '../widgets/ObjectField';
90
- import { VectorField } from '../widgets/VectorField';
91
- import { GridField } from '../widgets/GridField';
92
- import { ColorField } from '../widgets/ColorField';
93
- import { SliderField } from '../widgets/SliderField';
94
- import { RatingField } from '../widgets/RatingField';
95
- import { CodeField } from '../widgets/CodeField';
96
- import { AvatarField } from '../widgets/AvatarField';
97
- import { AddressField } from '../widgets/AddressField';
98
- import { GeolocationField } from '../widgets/GeolocationField';
99
- import { SignatureField } from '../widgets/SignatureField';
100
- import { QRCodeField } from '../widgets/QRCodeField';
101
- import { ObjectRefField } from '../widgets/ObjectRefField';
102
- import { FilterConditionField } from '../widgets/FilterConditionField';
103
- import { RecipientPickerField } from '../widgets/RecipientPickerField';
104
-
105
- /** Same widget map as the #3291 sweep, so the parity check covers the registry. */
106
- const WIDGETS: Record<string, ComponentType<any>> = {
107
- text: TextField,
108
- textarea: TextAreaField,
109
- number: NumberField,
110
- boolean: BooleanField,
111
- select: SelectField,
112
- date: DateField,
113
- datetime: DateTimeField,
114
- time: TimeField,
115
- email: EmailField,
116
- phone: PhoneField,
117
- url: UrlField,
118
- multiselect: MultiSelectField,
119
- radio: RadioField,
120
- checkboxes: CheckboxesField,
121
- tags: TagsField,
122
- currency: CurrencyField,
123
- percent: PercentField,
124
- password: PasswordField,
125
- markdown: RichTextField,
126
- html: RichTextField,
127
- richtext: RichTextField,
128
- lookup: LookupField,
129
- master_detail: LookupField,
130
- file: FileField,
131
- image: ImageField,
132
- location: LocationField,
133
- formula: FormulaField,
134
- summary: SummaryField,
135
- auto_number: AutoNumberField,
136
- user: UserField,
137
- owner: UserField,
138
- object: ObjectField,
139
- vector: VectorField,
140
- grid: GridField,
141
- color: ColorField,
142
- slider: SliderField,
143
- rating: RatingField,
144
- code: CodeField,
145
- avatar: AvatarField,
146
- address: AddressField,
147
- geolocation: GeolocationField,
148
- signature: SignatureField,
149
- qrcode: QRCodeField,
150
- 'object-ref': ObjectRefField,
151
- 'filter-condition': FilterConditionField,
152
- 'recipient-picker': RecipientPickerField,
153
- };
154
-
155
- /**
156
- * THE RATCHET LEDGER — widget types MEASURED (by this very sweep, run with an
157
- * empty ledger) not to put `aria-invalid` on any element of their row after a
158
- * real validation failure. Every entry is a known accessibility gap tracked in
159
- * objectui#3318, not an accepted state: the field shows its red message while
160
- * assistive tech is told nothing.
161
- *
162
- * The objectui#3318 delivery batch cleared 20 of the original 29 entries with
163
- * the objectui#3222/#3306 pattern. What remains cannot take that pattern
164
- * as-is; each entry names why, so the follow-up starts from the blocker and
165
- * not from a re-audit:
166
- *
167
- * - `formula` / `summary` / `auto_number` / `vector` — read-only computed /
168
- * display widgets: they render static text and NO focusable control, so
169
- * there is no element assistive tech would read the state from (marking the
170
- * text span would be exactly the non-focusable-wrapper move this ledger's
171
- * rules forbid). A `required` failure on a non-editable field is a metadata
172
- * authoring problem more than a widget one.
173
- * - `grid` — composite line-item editor with its own per-CELL `aria-invalid`
174
- * for line validation; driving it from a FORM-level failure needs a design
175
- * (which cell? the add-row button?) rather than a spread.
176
- * - `slider` — the focusable thumb (`role="slider"`, the ARIA-designated
177
- * carrier) is rendered inside the synced shadcn `ui/slider.tsx` (a #7
178
- * no-touch file), which forwards arbitrary props only to its non-focusable
179
- * Root span; delivering needs a components-level change.
180
- * - `signature` — the drawing `<canvas>` is not focusable at all (no keyboard
181
- * input path exists); the only focusable element is the auxiliary Clear
182
- * button. Needs a real a11y design, not an attribute.
183
- * - `filter-condition` / `recipient-picker` — dependency-gated: with no
184
- * sibling `object_name` / `recipient_type` chosen (the state a fresh form
185
- * and THIS SWEEP render) they show a hint paragraph with no focusable
186
- * control. Their editable states DO deliver `aria-invalid` since #3318's
187
- * delivery batch.
188
- *
189
- * Do NOT add to this list to make a new widget pass; fix the widget (the
190
- * objectui#3222/#3306 pattern: spread `toDomProps(props)` onto the real
191
- * focusable control — never a non-DOM Radix Root — then `aria-invalid=
192
- * {!!error}` after the spread). An entry is removed by fixing the widget: the
193
- * sweep asserts each entry still fails to deliver, so a fixed widget turns
194
- * its own ledger row red until it is removed here. The ledger only shrinks.
195
- */
196
- const NOT_YET_DELIVERED: ReadonlySet<string> = new Set([
197
- 'formula',
198
- 'summary',
199
- 'auto_number',
200
- 'vector',
201
- 'grid',
202
- 'slider',
203
- 'signature',
204
- 'filter-condition',
205
- 'recipient-picker',
206
- ]);
207
-
208
- /** Option widgets render an "unfillable" placeholder unless offered a list. */
209
- const OPTION_TYPES = new Set(['select', 'multiselect', 'radio', 'checkboxes', 'tags']);
210
- const OPTIONS = [
211
- { label: 'Alpha', value: 'alpha' },
212
- { label: 'Beta', value: 'beta' },
213
- ];
214
-
215
- /** `null` is MISSING for the presence-check `required` (cloud#972). */
216
- const MISSING_VALUE = null;
217
-
218
- function fieldConfig(type: string) {
219
- return {
220
- name: 'f',
221
- label: 'F',
222
- type: `field:${type}`,
223
- required: true,
224
- ...(OPTION_TYPES.has(type) ? { options: OPTIONS } : {}),
225
- };
226
- }
227
-
228
- function renderForm(field: Record<string, unknown>) {
229
- const Form = ComponentRegistry.get('form')!;
230
- return render(
231
- <Form
232
- schema={{
233
- type: 'form',
234
- mode: 'create',
235
- showSubmit: true,
236
- showCancel: false,
237
- submitLabel: 'Create',
238
- defaultValues: { f: MISSING_VALUE },
239
- fields: [field],
240
- onSubmit: () => {},
241
- }}
242
- />,
243
- );
244
- }
245
-
246
- async function formRow(): Promise<Element> {
247
- return waitFor(() => {
248
- const row = document.querySelector('[data-field="f"]');
249
- if (!row) throw new Error('field row never rendered');
250
- return row;
251
- });
252
- }
253
-
254
- /**
255
- * Drive a REAL validation failure and prove it rendered before reading aria:
256
- * submit, then wait for the "is required" message inside the row.
257
- */
258
- async function failValidation(): Promise<Element> {
259
- fireEvent.click(screen.getByRole('button', { name: /create/i }));
260
- await waitFor(() => {
261
- expect(document.querySelector('[data-field="f"]')?.textContent ?? '').toContain(
262
- 'is required',
263
- );
264
- });
265
- return formRow();
266
- }
267
-
268
- beforeAll(() => {
269
- for (const [type, Widget] of Object.entries(WIDGETS)) {
270
- ComponentRegistry.register(type, withFieldCarrier(Widget) as any, {
271
- namespace: 'field',
272
- skipFallback: true,
273
- });
274
- }
275
- }, 60000);
276
-
277
- beforeEach(() => {
278
- if (!(Element.prototype as any).scrollIntoView) {
279
- (Element.prototype as any).scrollIntoView = () => {};
280
- }
281
- });
282
-
283
- afterEach(() => {
284
- cleanup();
285
- vi.restoreAllMocks();
286
- });
287
-
288
- const ALL_TYPES = Object.keys(WIDGETS);
289
- const DELIVERING = ALL_TYPES.filter((type) => !NOT_YET_DELIVERED.has(type));
290
- const LEDGERED = ALL_TYPES.filter((type) => NOT_YET_DELIVERED.has(type));
291
-
292
- describe('every registered field widget announces a failed validation (objectui#3306)', () => {
293
- it('covers every field type the form can render', () => {
294
- expect(Object.keys(WIDGETS).sort()).toEqual([...FORM_FIELD_TYPES].sort());
295
- });
296
-
297
- it('the ledger only names types that exist', () => {
298
- // A renamed/removed widget must not leave a stale ledger entry that would
299
- // silently assert against nothing.
300
- expect([...NOT_YET_DELIVERED].filter((type) => !(type in WIDGETS))).toEqual([]);
301
- });
302
-
303
- it.each(DELIVERING)(
304
- 'field:%s — carries aria-invalid inside its row after a real failure',
305
- async (type) => {
306
- renderForm(fieldConfig(type));
307
- const row = await formRow();
308
-
309
- // No premature alarm: a widget must not claim invalid before validation
310
- // ran. (`aria-invalid="false"` is fine and expected — asserting on the
311
- // "true" value keeps this half meaningful for widgets that stay silent
312
- // until they fail.)
313
- expect(row.querySelector('[aria-invalid="true"]')).toBeNull();
314
-
315
- const after = await failValidation();
316
- expect(
317
- after.querySelector('[aria-invalid="true"]'),
318
- `field:${type} rendered its "is required" message but no element in its row carries aria-invalid="true" — assistive tech is never told the field failed`,
319
- ).not.toBeNull();
320
- },
321
- );
322
-
323
- it.each(LEDGERED)(
324
- 'field:%s — KNOWN GAP: still does not deliver aria-invalid (fix it, then remove it from NOT_YET_DELIVERED)',
325
- async (type) => {
326
- renderForm(fieldConfig(type));
327
- await formRow();
328
-
329
- const after = await failValidation();
330
- // The ratchet: the day this widget starts delivering, this assertion
331
- // fails and the entry MUST be removed from the ledger above.
332
- expect(
333
- after.querySelector('[aria-invalid="true"]'),
334
- `field:${type} now delivers aria-invalid — remove it from NOT_YET_DELIVERED so the sweep guards it forward`,
335
- ).toBeNull();
336
- },
337
- );
338
- });