@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.
Files changed (229) hide show
  1. package/CHANGELOG.md +714 -0
  2. package/dist/FieldEditWidget.d.ts +16 -2
  3. package/dist/field-type-alias.d.ts +16 -0
  4. package/dist/index.cjs +4 -4
  5. package/dist/index.css +27 -9
  6. package/dist/index.d.ts +425 -72
  7. package/dist/index.js +2530 -2228
  8. package/dist/widgets/AddressField.d.ts +3 -3
  9. package/dist/widgets/AutoNumberField.d.ts +1 -1
  10. package/dist/widgets/AvatarField.d.ts +1 -1
  11. package/dist/widgets/BooleanField.d.ts +1 -1
  12. package/dist/widgets/CapabilityMultiSelectField.d.ts +1 -1
  13. package/dist/widgets/CheckboxesField.d.ts +1 -1
  14. package/dist/widgets/CodeField.d.ts +1 -1
  15. package/dist/widgets/ColorField.d.ts +1 -1
  16. package/dist/widgets/CurrencyField.d.ts +1 -1
  17. package/dist/widgets/DateField.d.ts +1 -1
  18. package/dist/widgets/DateTimeField.d.ts +1 -1
  19. package/dist/widgets/EmailField.d.ts +1 -1
  20. package/dist/widgets/FileField.d.ts +18 -2
  21. package/dist/widgets/FilterConditionField.d.ts +28 -1
  22. package/dist/widgets/FormulaField.d.ts +1 -1
  23. package/dist/widgets/FullscreenFieldEditor.d.ts +9 -2
  24. package/dist/widgets/GeolocationField.d.ts +1 -1
  25. package/dist/widgets/GridField.d.ts +56 -4
  26. package/dist/widgets/ImageField.d.ts +1 -1
  27. package/dist/widgets/LocationField.d.ts +1 -1
  28. package/dist/widgets/LookupField.d.ts +1 -1
  29. package/dist/widgets/MasterDetailField.d.ts +1 -1
  30. package/dist/widgets/MultiSelectField.d.ts +1 -1
  31. package/dist/widgets/NumberField.d.ts +1 -1
  32. package/dist/widgets/ObjectField.d.ts +1 -1
  33. package/dist/widgets/ObjectRefField.d.ts +1 -1
  34. package/dist/widgets/OptionsEmptyState.d.ts +13 -7
  35. package/dist/widgets/PasswordField.d.ts +1 -1
  36. package/dist/widgets/PercentField.d.ts +1 -1
  37. package/dist/widgets/PhoneField.d.ts +1 -1
  38. package/dist/widgets/QRCodeField.d.ts +1 -1
  39. package/dist/widgets/RadioField.d.ts +1 -1
  40. package/dist/widgets/RatingField.d.ts +1 -1
  41. package/dist/widgets/RecipientPickerField.d.ts +1 -1
  42. package/dist/widgets/RecordPickerDialog.d.ts +2 -4
  43. package/dist/widgets/RichTextField.d.ts +37 -1
  44. package/dist/widgets/SelectField.d.ts +1 -1
  45. package/dist/widgets/SignatureField.d.ts +32 -2
  46. package/dist/widgets/SliderField.d.ts +37 -2
  47. package/dist/widgets/SummaryField.d.ts +1 -1
  48. package/dist/widgets/TagsField.d.ts +1 -1
  49. package/dist/widgets/TextAreaField.d.ts +6 -2
  50. package/dist/widgets/TextField.d.ts +1 -1
  51. package/dist/widgets/TimeField.d.ts +1 -1
  52. package/dist/widgets/UrlField.d.ts +1 -1
  53. package/dist/widgets/UserField.d.ts +1 -1
  54. package/dist/widgets/VectorField.d.ts +1 -1
  55. package/dist/widgets/_cell-renderer-bridge.d.ts +1 -1
  56. package/dist/widgets/address-format.d.ts +27 -7
  57. package/dist/widgets/lookupColumnDisplay.d.ts +93 -0
  58. package/dist/widgets/richTextDisplay.d.ts +80 -0
  59. package/dist/widgets/toHostGroupProps.d.ts +119 -21
  60. package/dist/widgets/types.d.ts +8 -3
  61. package/package.json +9 -10
  62. package/dist/widgets/CharacterCount.d.ts +0 -56
  63. package/src/FieldEditWidget.test.ts +0 -133
  64. package/src/FieldEditWidget.tsx +0 -207
  65. package/src/__tests__/AddressCellRenderer.test.tsx +0 -166
  66. package/src/__tests__/CurrencyField.minorUnits.test.tsx +0 -191
  67. package/src/__tests__/CurrencyField.symbol.test.tsx +0 -198
  68. package/src/__tests__/DateCellRenderer.test.tsx +0 -82
  69. package/src/__tests__/NumberCellRenderer.grouping.test.tsx +0 -146
  70. package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
  71. package/src/__tests__/PercentCellRenderer.locale.test.tsx +0 -152
  72. package/src/__tests__/PercentCellRenderer.test.tsx +0 -115
  73. package/src/__tests__/boolean-label-association-e2e.test.tsx +0 -298
  74. package/src/__tests__/capability-multiselect-retired.test.ts +0 -109
  75. package/src/__tests__/cell-truncation.test.tsx +0 -96
  76. package/src/__tests__/composite-group-label-e2e.test.tsx +0 -436
  77. package/src/__tests__/composite-group-label-readonly-e2e.test.tsx +0 -403
  78. package/src/__tests__/date-formatter-residue-4272.test.ts +0 -120
  79. package/src/__tests__/date-locale-channel.test.tsx +0 -305
  80. package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
  81. package/src/__tests__/formatCurrency.centsDigit.test.tsx +0 -158
  82. package/src/__tests__/formatCurrency.minorUnits.test.tsx +0 -221
  83. package/src/__tests__/group-labelling-declaration.test.ts +0 -148
  84. package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
  85. package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
  86. package/src/__tests__/lookupCellRecordLink.test.tsx +0 -171
  87. package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
  88. package/src/__tests__/percent-cell-vs-measure-4576.test.ts +0 -115
  89. package/src/__tests__/percent-formatter-locale-4553.test.ts +0 -147
  90. package/src/__tests__/percent-tie-halfup-4590.test.ts +0 -224
  91. package/src/__tests__/registry-collision.test.ts +0 -91
  92. package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
  93. package/src/__tests__/select-label-association-e2e.test.tsx +0 -199
  94. package/src/__tests__/spec-symbol-batch7.test.ts +0 -126
  95. package/src/__tests__/validation-feedback.test.tsx +0 -361
  96. package/src/__tests__/widget-aria-invalid-e2e.test.tsx +0 -265
  97. package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +0 -338
  98. package/src/__tests__/widget-dom-leak-e2e.test.tsx +0 -357
  99. package/src/__tests__/widget-props-contract.test.tsx +0 -142
  100. package/src/coerce-safe-value.test.ts +0 -34
  101. package/src/complex-widgets.test.tsx +0 -924
  102. package/src/currency.ts +0 -144
  103. package/src/datetime-widgets.test.tsx +0 -185
  104. package/src/field-type-alias.multiple.test.ts +0 -81
  105. package/src/field-type-alias.ts +0 -156
  106. package/src/field-type-coverage.test.ts +0 -67
  107. package/src/index.css +0 -44
  108. package/src/index.tsx +0 -2745
  109. package/src/standard-widgets.test.tsx +0 -364
  110. package/src/widgets/AddressField.postalCode.test.tsx +0 -257
  111. package/src/widgets/AddressField.tsx +0 -182
  112. package/src/widgets/AddressField.uniqueIds.test.tsx +0 -93
  113. package/src/widgets/AutoNumberField.tsx +0 -18
  114. package/src/widgets/AvatarField.tsx +0 -117
  115. package/src/widgets/BooleanField.tsx +0 -86
  116. package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
  117. package/src/widgets/CapabilityMultiSelectField.tsx +0 -241
  118. package/src/widgets/CharacterCount.tsx +0 -263
  119. package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
  120. package/src/widgets/CheckboxesField.tsx +0 -169
  121. package/src/widgets/CodeField.tsx +0 -41
  122. package/src/widgets/ColorField.tsx +0 -53
  123. package/src/widgets/CurrencyField.tsx +0 -135
  124. package/src/widgets/DateField.tsx +0 -40
  125. package/src/widgets/DateTimeField.tsx +0 -46
  126. package/src/widgets/EmailField.tsx +0 -37
  127. package/src/widgets/FileField.tsx +0 -512
  128. package/src/widgets/FilterConditionField.tsx +0 -454
  129. package/src/widgets/FormulaField.tsx +0 -38
  130. package/src/widgets/FullscreenFieldEditor.tsx +0 -60
  131. package/src/widgets/GeolocationField.tsx +0 -193
  132. package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
  133. package/src/widgets/GridField.test.tsx +0 -632
  134. package/src/widgets/GridField.tsx +0 -1070
  135. package/src/widgets/ImageCropperDialog.tsx +0 -245
  136. package/src/widgets/ImageField.maxSize.test.tsx +0 -154
  137. package/src/widgets/ImageField.tsx +0 -305
  138. package/src/widgets/ImageLightbox.test.tsx +0 -52
  139. package/src/widgets/ImageLightbox.tsx +0 -108
  140. package/src/widgets/LocationField.tsx +0 -57
  141. package/src/widgets/LookupField.dependsOn.test.tsx +0 -233
  142. package/src/widgets/LookupField.gateHintLabel.test.tsx +0 -135
  143. package/src/widgets/LookupField.hydration.test.tsx +0 -136
  144. package/src/widgets/LookupField.idField.test.tsx +0 -61
  145. package/src/widgets/LookupField.tsx +0 -1295
  146. package/src/widgets/MarkdownContent.tsx +0 -19
  147. package/src/widgets/MasterDetailField.tsx +0 -131
  148. package/src/widgets/MultiSelectField.cascade.test.tsx +0 -143
  149. package/src/widgets/MultiSelectField.tsx +0 -196
  150. package/src/widgets/NumberField.tsx +0 -49
  151. package/src/widgets/ObjectField.tsx +0 -94
  152. package/src/widgets/ObjectRefField.tsx +0 -121
  153. package/src/widgets/OptionsEmptyState.no-provider.test.tsx +0 -88
  154. package/src/widgets/OptionsEmptyState.test.tsx +0 -169
  155. package/src/widgets/OptionsEmptyState.tsx +0 -99
  156. package/src/widgets/PasswordField.tsx +0 -49
  157. package/src/widgets/PeoplePicker.test.tsx +0 -360
  158. package/src/widgets/PeoplePicker.tsx +0 -579
  159. package/src/widgets/PeoplePickerComponents.test.tsx +0 -104
  160. package/src/widgets/PercentField.tsx +0 -95
  161. package/src/widgets/PersonRow.tsx +0 -117
  162. package/src/widgets/PhoneField.tsx +0 -37
  163. package/src/widgets/QRCodeField.tsx +0 -103
  164. package/src/widgets/RadioField.cascade.test.tsx +0 -141
  165. package/src/widgets/RadioField.tsx +0 -143
  166. package/src/widgets/RatingField.tsx +0 -94
  167. package/src/widgets/RecipientPickerField.test.tsx +0 -157
  168. package/src/widgets/RecipientPickerField.tsx +0 -188
  169. package/src/widgets/RecordPickerDialog.baseFilterShapes.test.tsx +0 -191
  170. package/src/widgets/RecordPickerDialog.dateLocale.test.tsx +0 -124
  171. package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
  172. package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
  173. package/src/widgets/RecordPickerDialog.tsx +0 -1365
  174. package/src/widgets/RichTextField.tsx +0 -176
  175. package/src/widgets/SelectField.cascade.test.tsx +0 -138
  176. package/src/widgets/SelectField.tsx +0 -193
  177. package/src/widgets/SelectionTray.tsx +0 -110
  178. package/src/widgets/SignatureField.tsx +0 -140
  179. package/src/widgets/SliderField.tsx +0 -39
  180. package/src/widgets/SummaryField.tsx +0 -31
  181. package/src/widgets/TagsField.placeholder.no-provider.test.tsx +0 -68
  182. package/src/widgets/TagsField.placeholder.test.tsx +0 -95
  183. package/src/widgets/TagsField.tsx +0 -88
  184. package/src/widgets/TextAreaField.tsx +0 -213
  185. package/src/widgets/TextField.tsx +0 -45
  186. package/src/widgets/TimeField.tsx +0 -31
  187. package/src/widgets/UrlField.tsx +0 -46
  188. package/src/widgets/UserField.tsx +0 -59
  189. package/src/widgets/UserFieldPicker.test.tsx +0 -78
  190. package/src/widgets/VectorField.tsx +0 -29
  191. package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +0 -66
  192. package/src/widgets/__tests__/FilterConditionField.operators.test.ts +0 -166
  193. package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +0 -248
  194. package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +0 -116
  195. package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +0 -244
  196. package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +0 -195
  197. package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +0 -417
  198. package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +0 -287
  199. package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +0 -160
  200. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +0 -161
  201. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +0 -346
  202. package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +0 -246
  203. package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +0 -175
  204. package/src/widgets/_cell-renderer-bridge.ts +0 -19
  205. package/src/widgets/address-format.ts +0 -120
  206. package/src/widgets/deriveLookupColumns.test.ts +0 -112
  207. package/src/widgets/deriveLookupColumns.ts +0 -108
  208. package/src/widgets/file-size-guard.ts +0 -55
  209. package/src/widgets/file-value.test.ts +0 -230
  210. package/src/widgets/file-value.ts +0 -246
  211. package/src/widgets/nativeDateValue.test.ts +0 -115
  212. package/src/widgets/nativeDateValue.ts +0 -112
  213. package/src/widgets/openNativePicker.ts +0 -18
  214. package/src/widgets/optionWidgets.gatedOptions.test.tsx +0 -267
  215. package/src/widgets/optionWidgets.unconfiguredOptions.test.tsx +0 -150
  216. package/src/widgets/personDisplay.test.ts +0 -113
  217. package/src/widgets/personDisplay.ts +0 -92
  218. package/src/widgets/recentLookups.test.ts +0 -45
  219. package/src/widgets/recentLookups.ts +0 -47
  220. package/src/widgets/toDomProps.ts +0 -204
  221. package/src/widgets/toHostGroupProps.ts +0 -102
  222. package/src/widgets/types.ts +0 -242
  223. package/src/widgets/useCascadingOptions.ts +0 -46
  224. package/src/widgets/useFieldTranslation.ts +0 -132
  225. package/src/widgets/useRecordQuery.test.ts +0 -174
  226. package/src/widgets/useRecordQuery.ts +0 -291
  227. package/src/widgets/useUploadingSignal.test.tsx +0 -62
  228. package/src/widgets/useUploadingSignal.ts +0 -20
  229. package/src/withFieldCarrier.tsx +0 -75
@@ -1,224 +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#4590 — `formatPercent` rounds a tie the wrong way, because it renders
11
- * a value that is ALREADY in percentage points through `Intl`'s
12
- * `style: 'percent'`, which wants a FRACTION.
13
- *
14
- * The old body divided by 100 so `Intl` could multiply straight back:
15
- *
16
- * formatDisplayNumber(displayValue / 100, { style: 'percent', … })
17
- *
18
- * That round trip is not value-preserving. `Intl` formats from the SHORTEST
19
- * decimal representation of the double it is handed, and `displayValue / 100`
20
- * has a different shortest representation from `displayValue`: `1.005` is
21
- * `1.005`, but `1.005 / 100` is `0.010049999999999999`, which percent-scales to
22
- * `1.0049999999999999` and rounds DOWN. The DIVISION loses the digit, not the
23
- * rounding. It is therefore a numeral defect and not a convention one — it
24
- * reproduces identically in every locale, in Arabic-Indic digits too.
25
- *
26
- * The fix is the option #4576 / PR #4589 put in `@object-ui/core` for exactly
27
- * this: `style: 'percentPoints'` formats the points DIRECTLY (`Intl`'s
28
- * `style: 'unit'` / `unit: 'percent'` / `unitDisplay: 'narrow'`), which was
29
- * measured to give a byte-identical percent affix to `style: 'percent'` across
30
- * all 171 locale tags tested.
31
- *
32
- * ── PREDICTIONS, written before the run ─────────────────────────────────────
33
- * Runner measured: node v22.22.2, ICU 78.2, machine locale en-US.
34
- *
35
- * RED before the fix — the numerals:
36
- * formatPercent(1.005, 2, 'en-US') `1.00%` → `1.01%`
37
- * formatPercent(1.025, 2, 'en-US') `1.02%` → `1.03%`
38
- * formatPercent(1.45, 1, 'en-US') `1.4%` → `1.5%`
39
- * formatPercent(1.055, 2, 'en-US') `1.05%` → `1.06%`
40
- * formatPercent(99999.995, 2, 'en') `99,999.99%` → `100,000.00%`
41
- * MAX_SAFE_INTEGER p0 `9,007,199,254,740,990%` → `…991%`
42
- * 1e23 p0 `99,999,999,999,999,990,000,000%`
43
- * → `100,000,000,000,000,000,000,000%`
44
- * the same four ties in de-DE / tr-TR / ar-EG — the locale-independence
45
- * claim, red in every one of them
46
- *
47
- * GREEN ON BOTH SIDES — the must-not-change set, and the STOP condition if
48
- * any of it moves (this card is NUMERAL-only; a convention move is a defect
49
- * in the fix, not a pin to update):
50
- * the percent CONVENTION of all ten locales at 1234.5 p1 — de/fr/ru/sv's
51
- * no-break space, tr's PREFIX sign, ar's U+066A + U+061C, en/ja/zh's bare
52
- * suffix, fr's U+202F group separator, bn's Bengali digits
53
- * the sign position and glyph on negatives (sv-SE's U+2212 included)
54
- * the affix parity check against `Intl`'s own `style: 'percent'`
55
- * `percentDisplayValue`'s fraction/whole scaling — upstream of the render
56
- * and deliberately untouched by this card
57
- *
58
- * MEASURED, this call shape, old route vs new: 10 locales x 18 values x 4
59
- * precisions = 720 combinations — 0 convention diffs, 130 numeral diffs (13 per
60
- * locale, the SAME 13 in every locale). On the wide en-US grid the issue
61
- * quantified (0.005 steps to 2000, precisions 0/1/2) 27,577 of 1,200,003 forms
62
- * move. Every one is a last-digit-off-by-one.
63
- *
64
- * Every non-ASCII byte below is written as a `\u` escape, never pasted.
65
- */
66
-
67
- import { describe, it, expect } from 'vitest';
68
- import { formatPercent } from '../index';
69
-
70
- /** U+00A0 NO-BREAK SPACE — de/fr/ru/sv put one before the percent sign. */
71
- const NBSP = '\u00a0';
72
- /** U+202F NARROW NO-BREAK SPACE — fr's group separator. */
73
- const NNBSP = '\u202f';
74
- /** U+066A ARABIC PERCENT SIGN, U+061C ARABIC LETTER MARK. */
75
- const ARABIC_PERCENT = '\u066a';
76
- const ALM = '\u061c';
77
-
78
- describe('formatPercent rounds a tie half-up on the authored decimal (#4590)', () => {
79
- /**
80
- * MOVING PINS — the issue's measured table, verbatim. Each was the artefact
81
- * of the `/ 100` round trip; the right-hand column is half-up on the decimal
82
- * the author actually wrote.
83
- */
84
- it.each([
85
- [1.005, 2, '1.01%', '1.00%'],
86
- [1.025, 2, '1.03%', '1.02%'],
87
- [1.45, 1, '1.5%', '1.4%'],
88
- [1.055, 2, '1.06%', '1.05%'],
89
- ])('formatPercent(%s, %s) is %s (was %s)', (value, precision, expected) => {
90
- expect(formatPercent(value, precision, 'en-US')).toBe(expected);
91
- });
92
-
93
- /**
94
- * A tie that carries into a new digit COLUMN rather than just flipping the
95
- * last one — the round trip lost the carry as well as the digit.
96
- */
97
- it('carries across the grouping boundary: 99999.995 to 2 decimals', () => {
98
- // was `99,999.99%`
99
- expect(formatPercent(99999.995, 2, 'en-US')).toBe('100,000.00%');
100
- });
101
-
102
- /**
103
- * The defect is in the VALUE, not the convention, so it must reproduce in
104
- * every locale — including one that writes the sign in front and one that
105
- * uses neither ASCII digits nor an ASCII sign.
106
- */
107
- it('the same ties move in every locale — this is a numeral defect', () => {
108
- expect(formatPercent(1.005, 2, 'de-DE')).toBe(`1,01${NBSP}%`); // was `1,00 %`
109
- expect(formatPercent(1.45, 1, 'de-DE')).toBe(`1,5${NBSP}%`); // was `1,4 %`
110
- expect(formatPercent(1.005, 2, 'tr-TR')).toBe('%1,01'); // was `%1,00`
111
- // U+0661 U+066B U+0660 U+0661 — Arabic-Indic `1`, decimal separator, `0`, `1`
112
- expect(formatPercent(1.005, 2, 'ar-EG')).toBe(
113
- `\u0661\u066b\u0660\u0661${ARABIC_PERCENT}${ALM}`,
114
- ); // was `\u0661\u066b\u0660\u0660` + the same affix
115
- });
116
- });
117
-
118
- describe('formatPercent keeps every digit at the top of the double range (#4590)', () => {
119
- /**
120
- * MOVING PINS, before/after verbatim. `Number.MAX_SAFE_INTEGER` percentage
121
- * points used to render with its last digit replaced by a zero, and 1e23 used
122
- * to render as a number that is not 1e23 at all. Both are the same artefact
123
- * as the ties, at the other end of the range: divide by 100 and the shortest
124
- * representation of the quotient no longer round-trips.
125
- */
126
- it('MAX_SAFE_INTEGER keeps its last digit', () => {
127
- // was `9,007,199,254,740,990%` — a 1 turned into a 0
128
- expect(formatPercent(Number.MAX_SAFE_INTEGER, 0, 'en-US')).toBe('9,007,199,254,740,991%');
129
- });
130
-
131
- it('1e23 renders as 1e23', () => {
132
- // was `99,999,999,999,999,990,000,000%`
133
- expect(formatPercent(1e23, 0, 'en-US')).toBe('100,000,000,000,000,000,000,000%');
134
- });
135
- });
136
-
137
- describe('MUST NOT CHANGE: the locale percent CONVENTION (#4590 is numeral-only)', () => {
138
- /**
139
- * The affix is what `style: 'percentPoints'` promises to preserve, so this is
140
- * the half that proves the fix did not become a convention change. Byte-exact
141
- * strings, chosen at a magnitude and precision where no tie is in play — they
142
- * are GREEN on both sides of the fix, and a diff here is a STOP condition.
143
- */
144
- it.each([
145
- ['en-US', '1,234.5%'],
146
- ['de-DE', `1.234,5${NBSP}%`],
147
- ['fr-FR', `1${NNBSP}234,5${NBSP}%`],
148
- ['tr-TR', '%1.234,5'], // sign in FRONT
149
- ['ar-EG', `\u0661\u066c\u0662\u0663\u0664\u066b\u0665${ARABIC_PERCENT}${ALM}`],
150
- ['ja-JP', '1,234.5%'],
151
- ['zh-CN', '1,234.5%'],
152
- ['ru-RU', `1${NBSP}234,5${NBSP}%`],
153
- ['sv-SE', `1${NBSP}234,5${NBSP}%`],
154
- ['bn-IN', `\u09e7,\u09e8\u09e9\u09ea.\u09eb%`],
155
- ])('%s renders its own percent convention, unchanged', (locale, expected) => {
156
- expect(formatPercent(1234.5, 1, locale)).toBe(expected);
157
- });
158
-
159
- /**
160
- * Sign POSITION and sign GLYPH are convention too — Turkish puts the minus
161
- * outside its prefixed percent sign, Arabic leads with U+061C, and Swedish
162
- * uses U+2212 MINUS SIGN rather than ASCII hyphen-minus. None of it moves.
163
- */
164
- it.each([
165
- ['en-US', '-45.5%'],
166
- ['de-DE', `-45,5${NBSP}%`],
167
- ['tr-TR', '-%45,5'],
168
- ['ar-EG', `${ALM}-\u0664\u0665\u066b\u0665${ARABIC_PERCENT}${ALM}`],
169
- ['sv-SE', `\u221245,5${NBSP}%`],
170
- ])('%s keeps its negative-sign convention', (locale, expected) => {
171
- expect(formatPercent(-45.5, 1, locale)).toBe(expected);
172
- });
173
-
174
- /**
175
- * The parity claim asserted rather than trusted: the affix `formatPercent`
176
- * now produces must be the one `Intl`'s OWN `style: 'percent'` produces for
177
- * the same locale. Comparing the affix alone (digits stripped) is deliberate
178
- * — the numerals are exactly what this card moves, so a whole-string compare
179
- * would re-assert the defect.
180
- */
181
- it('the affix is byte-identical to Intl style:percent, for every locale here', () => {
182
- const affix = (rendered: string) => rendered.replace(/[\p{Nd}]/gu, '');
183
- for (const locale of ['en-US', 'de-DE', 'fr-FR', 'tr-TR', 'ar-EG', 'ja-JP',
184
- 'zh-CN', 'ru-RU', 'sv-SE', 'bn-IN']) {
185
- const cell = formatPercent(1234.5, 1, locale);
186
- const intl = new Intl.NumberFormat(locale, {
187
- style: 'percent',
188
- minimumFractionDigits: 1,
189
- maximumFractionDigits: 1,
190
- }).format(12.345);
191
- expect(affix(cell), `percent affix for ${locale}`).toBe(affix(intl));
192
- }
193
- });
194
- });
195
-
196
- describe('MUST NOT CHANGE: percent SCALING is upstream of the render (#4590)', () => {
197
- /**
198
- * `percentDisplayValue` (a stored fraction below 1 scales by 100, a value at
199
- * or above 1 passes through) decides WHICH number gets rendered; this card
200
- * changes only HOW that number is rendered. Pinned unmoved so a future reader
201
- * cannot mistake the two halves for one.
202
- */
203
- it('a stored fraction still scales, a stored whole number still does not', () => {
204
- expect(formatPercent(0.8, 0, 'en-US')).toBe('80%');
205
- expect(formatPercent(0.5, 0, 'en-US')).toBe('50%');
206
- expect(formatPercent(0.075, 1, 'en-US')).toBe('7.5%');
207
- expect(formatPercent(80, 0, 'en-US')).toBe('80%');
208
- expect(formatPercent(100, 0, 'en-US')).toBe('100%');
209
- // The boundary itself: 1 is NOT a fraction, so it stays 1%.
210
- expect(formatPercent(1, 0, 'en-US')).toBe('1%');
211
- // …which is why the tie cases above are authored just above 1.
212
- expect(formatPercent(0.9999, 0, 'en-US')).toBe('100%');
213
- });
214
-
215
- it('a malformed locale tag still falls back instead of throwing', () => {
216
- expect(() => formatPercent(80, 0, 'not a locale')).not.toThrow();
217
- expect(formatPercent(80, 0, 'not a locale')).toContain('80');
218
- });
219
-
220
- it('is still callable with no locale and no precision', () => {
221
- expect(formatPercent(80)).toBe('80%');
222
- expect(formatPercent(80, 0)).toBe('80%');
223
- });
224
- });
@@ -1,91 +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
- * Registry collision guard.
11
- *
12
- * `@object-ui/components` registers display widgets (`text`, `image`,
13
- * `avatar`, `html`, `grid`) under their bare names. `@object-ui/fields`
14
- * registers form-input widgets that — by name — collide with those.
15
- *
16
- * The fix: field registrations for these 5 types pass `skipFallback: true`,
17
- * so the bare lookup keeps returning the display widget while the field
18
- * input is still reachable via the `field:<type>` namespace.
19
- *
20
- * Regression contract:
21
- * 1. `FIELD_TYPES_SKIP_FALLBACK` must contain every colliding name.
22
- * 2. After `registerAllFields()`, bare lookups must NOT resolve to a
23
- * field renderer for any of those names.
24
- * 3. The `field:<type>` namespaced lookup must still work, so form
25
- * schemas using `{ type: 'text' }` still find an input renderer
26
- * when explicitly requested.
27
- */
28
-
29
- import { describe, it, expect, beforeAll } from 'vitest';
30
- import { ComponentRegistry } from '@object-ui/core';
31
-
32
- // Importing the fields entry runs `registerAllFields()` at module load.
33
- // Importing the components entry runs display-widget registration the
34
- // same way. Both must be loaded before we inspect the registry.
35
- import '@object-ui/components';
36
- import { registerAllFields } from '../index';
37
-
38
- const COLLIDING_TYPES = ['text', 'html', 'image', 'avatar', 'grid'] as const;
39
-
40
- describe('registry collision (fields ↔ components)', () => {
41
- beforeAll(() => {
42
- // Defensive: in case the auto-register at import time hasn't run yet
43
- // under the test runner's module isolation.
44
- registerAllFields();
45
- });
46
-
47
- it.each(COLLIDING_TYPES)(
48
- 'bare "%s" lookup is NOT clobbered by the field renderer',
49
- (type) => {
50
- // The bare lookup must resolve, and it must resolve to a display widget
51
- // (or a FallbackComponent / null) — never to the lazy field renderer.
52
- // The field renderer is a React.lazy() result; the display widget is a
53
- // plain function component. We don't pin to a specific identity, but we
54
- // DO pin that whatever the bare lookup returns, the namespaced
55
- // `field:<type>` lookup returns something different.
56
- const bare = ComponentRegistry.get(type);
57
- const namespaced = ComponentRegistry.get(`field:${type}`);
58
-
59
- // Namespaced field renderer must exist for every colliding type —
60
- // that's the whole point of keeping the field accessible.
61
- expect(
62
- namespaced,
63
- `field:${type} must be registered so forms can still use it`,
64
- ).toBeTruthy();
65
-
66
- // The bare and namespaced renderers must NOT be identical. If they
67
- // are, it means the field renderer wrote to the bare fallback and
68
- // would render an empty <input> instead of the display widget.
69
- expect(
70
- bare,
71
- `bare "${type}" must not be the same component as field:${type}`,
72
- ).not.toBe(namespaced);
73
- },
74
- );
75
-
76
- it('non-colliding field types DO populate the bare fallback', () => {
77
- // Sanity check: a field type with no display-widget collision (e.g.
78
- // `email`, `password`, `number`) must still register both at the
79
- // namespaced AND bare keys, otherwise we'd silently break forms.
80
- for (const type of ['email', 'password', 'number', 'date', 'select']) {
81
- expect(
82
- ComponentRegistry.get(`field:${type}`),
83
- `field:${type} must be registered`,
84
- ).toBeTruthy();
85
- expect(
86
- ComponentRegistry.get(type),
87
- `bare "${type}" must also be reachable (no collision)`,
88
- ).toBeTruthy();
89
- }
90
- });
91
- });
@@ -1,20 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { resolveFieldCurrency } from '../index';
3
-
4
- describe('resolveFieldCurrency', () => {
5
- it('prefers the field explicit currency over everything', () => {
6
- expect(resolveFieldCurrency({ currency: 'JPY', currencyConfig: { defaultCurrency: 'EUR' } }, 'USD')).toBe('JPY');
7
- });
8
- it('falls back to currencyConfig.defaultCurrency, then legacy defaultCurrency', () => {
9
- expect(resolveFieldCurrency({ currencyConfig: { defaultCurrency: 'EUR' } }, 'USD')).toBe('EUR');
10
- expect(resolveFieldCurrency({ defaultCurrency: 'GBP' } as any, 'USD')).toBe('GBP');
11
- });
12
- it('falls back to the tenant default when the field omits its own', () => {
13
- expect(resolveFieldCurrency({}, 'CNY')).toBe('CNY');
14
- expect(resolveFieldCurrency(null, 'CNY')).toBe('CNY');
15
- });
16
- it('returns undefined when nothing is known (→ renderer shows a plain number)', () => {
17
- expect(resolveFieldCurrency({})).toBeUndefined();
18
- expect(resolveFieldCurrency(undefined, undefined)).toBeUndefined();
19
- });
20
- });
@@ -1,199 +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: BOTH select paths associate the form's visible label with the
11
- * control the user operates (objectui#3976, the built-in half of #3306).
12
- *
13
- * There are two `select` paths through the form renderer and they do not share
14
- * an implementation:
15
- *
16
- * - **registered widget** — `type: 'field:select'` (what `mapFieldTypeToFormType`
17
- * emits for object-driven forms) resolves to this package's `SelectField`.
18
- * objectui#3306 moved its DOM pass-through from Radix's `Select.Root` — which
19
- * renders no element and silently drops what it does not recognise — onto
20
- * `SelectTrigger`, the focusable `<button role="combobox">`.
21
- * - **built-in branch** — a hand-written `type: 'select'` never reaches the
22
- * registry at all (`'select'` is a `BUILTIN_FIELD_TYPES` member), so the form
23
- * renderer's own `Select` branch renders it. That branch kept spreading onto
24
- * Root until objectui#3976: the visible label pointed at an id no element
25
- * carried, `getByLabelText` found nothing, and `aria-describedby` /
26
- * `aria-invalid` / `aria-required` never reached the DOM either.
27
- *
28
- * The paths are pinned side by side, in one file, precisely because the fork is
29
- * what caused the bug: #3306 fixed one half and the other silently kept the
30
- * defect for as long as nobody wrote a bare `type: 'select'` into a test. The
31
- * registered half here is a POSITIVE CONTROL — it must stay green through any
32
- * change to the built-in branch.
33
- *
34
- * The widget is registered raw rather than through `registerAllFields()`, which
35
- * wraps every loader in `React.lazy`: an unbounded module load inside a bounded
36
- * `findBy`/`waitFor` window is the repo's known flake generator (AGENTS.md
37
- * 测试纪律, objectui#3010). Same component, no Suspense boundary.
38
- */
39
-
40
- import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest';
41
- import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react';
42
- import '@testing-library/jest-dom';
43
- import { ComponentRegistry } from '@object-ui/core';
44
- // Module scope: pulls in the form renderer's registration side effect.
45
- import '@object-ui/components';
46
-
47
- import { SelectField } from '../widgets/SelectField';
48
-
49
- const OPTIONS = [
50
- { label: 'Alpha', value: 'alpha' },
51
- { label: 'Beta', value: 'beta' },
52
- ];
53
-
54
- beforeAll(() => {
55
- ComponentRegistry.register('select', SelectField as any, {
56
- namespace: 'field',
57
- skipFallback: true,
58
- });
59
- }, 30000);
60
-
61
- beforeEach(() => {
62
- if (!(Element.prototype as any).scrollIntoView) {
63
- (Element.prototype as any).scrollIntoView = () => {};
64
- }
65
- });
66
-
67
- afterEach(() => {
68
- cleanup();
69
- vi.restoreAllMocks();
70
- });
71
-
72
- /** The real form renderer hosting one field. */
73
- function renderForm(field: any, defaultValue: unknown = undefined) {
74
- const Form = ComponentRegistry.get('form')!;
75
- return render(
76
- <Form
77
- schema={{
78
- type: 'form',
79
- mode: 'create',
80
- showSubmit: true,
81
- showCancel: false,
82
- submitLabel: 'Create',
83
- defaultValues: { [field.name]: defaultValue },
84
- fields: [field],
85
- onSubmit: () => {},
86
- }}
87
- />,
88
- );
89
- }
90
-
91
- /** Every `<label for=…>` in the document, paired with its resolved target. */
92
- function labelTargets(): Array<{ text: string; forId: string; resolves: boolean }> {
93
- return Array.from(document.querySelectorAll('label'))
94
- .map((el) => ({
95
- text: (el.textContent ?? '').trim(),
96
- forId: el.getAttribute('for') ?? '',
97
- resolves: !!el.getAttribute('for') && !!document.getElementById(el.getAttribute('for')!),
98
- }))
99
- .filter((l) => l.forId !== '');
100
- }
101
-
102
- /** The trigger button, by role — fails loudly if it stops being a combobox. */
103
- function trigger(name: string): Element {
104
- const el = document.querySelector(`[data-field="${name}"] [role="combobox"]`);
105
- if (!el) throw new Error(`no select trigger rendered for field "${name}"`);
106
- return el;
107
- }
108
-
109
- const submit = () => fireEvent.click(screen.getByRole('button', { name: /create/i }));
110
-
111
- /**
112
- * The guarantees a select must give whichever path rendered it. Run twice, so
113
- * neither path can drift away from the other again.
114
- */
115
- const PATHS = [
116
- ['built-in branch (bare `type: select`, objectui#3976)', 'select'],
117
- ['registered widget (`field:select`, objectui#3306 — positive control)', 'field:select'],
118
- ] as const;
119
-
120
- describe.each(PATHS)('%s', (_label, type) => {
121
- const field = { name: 'choice', label: 'Choice', type, options: OPTIONS };
122
-
123
- it('emits no label pointing at an id nothing carries', () => {
124
- renderForm(field);
125
-
126
- const labels = labelTargets();
127
- expect(labels.length).toBeGreaterThan(0);
128
- expect(labels.filter((l) => !l.resolves)).toEqual([]);
129
- });
130
-
131
- it('resolves the visible label to the trigger button', () => {
132
- renderForm(field);
133
-
134
- // `getAllByLabelText` walks `for`→id, so it guards the association itself:
135
- // on the built-in path this used to find NOTHING at all.
136
- const labelled = screen.getAllByLabelText('Choice');
137
- expect(labelled).toHaveLength(1);
138
- expect(labelled[0]).toBe(trigger('choice'));
139
- expect(labelled[0].tagName).toBe('BUTTON');
140
- });
141
-
142
- it('gives the combobox the label as its accessible name', () => {
143
- renderForm(field);
144
-
145
- expect(screen.getByRole('combobox', { name: 'Choice' })).toBe(trigger('choice'));
146
- });
147
-
148
- it('carries the required state on the trigger', () => {
149
- renderForm({ ...field, required: true });
150
-
151
- expect(trigger('choice')).toHaveAttribute('aria-required', 'true');
152
- });
153
-
154
- it('announces invalid on the trigger and links the message', async () => {
155
- renderForm({ ...field, required: true }, null);
156
-
157
- // A valid field SAYS it is valid rather than staying mute (#3222/#3306).
158
- expect(trigger('choice')).toHaveAttribute('aria-invalid', 'false');
159
-
160
- submit();
161
-
162
- const message = await screen.findByText('Choice is required');
163
- await waitFor(() => expect(trigger('choice')).toHaveAttribute('aria-invalid', 'true'));
164
- expect(message.id).not.toBe('');
165
- const describedBy = trigger('choice').getAttribute('aria-describedby') ?? '';
166
- expect(describedBy.split(/\s+/)).toContain(message.id);
167
- });
168
-
169
- it('keeps `name` on Root, where the submitted hidden select is', () => {
170
- // The whitelist both paths share: `name` is the ONE key Root consumes — it
171
- // forwards it to the hidden native `<select>` that takes part in form
172
- // submission. On the trigger it would sit uselessly on a non-submitter.
173
- renderForm(field);
174
-
175
- expect(document.querySelector('[data-field="choice"] select[aria-hidden="true"]')).toHaveAttribute(
176
- 'name',
177
- 'choice',
178
- );
179
- expect(trigger('choice')).not.toHaveAttribute('name');
180
- });
181
- });
182
-
183
- describe('the fork itself (objectui#3976 / #3231)', () => {
184
- it('a bare `type: select` renders the BUILT-IN branch even though field:select resolves', () => {
185
- // Measured, not inferred — this is the fact that made #3976 a built-in-side
186
- // bug rather than a duplicate of #3306: `renderFieldComponent` skips the
187
- // registry entirely for a `BUILTIN_FIELD_TYPES` member, so `SelectField` is
188
- // NOT what renders a hand-written `type: 'select'`, even here where the real
189
- // widget is registered under `field:select`.
190
- expect(ComponentRegistry.get('field:select')).toBe(SelectField);
191
-
192
- renderForm({ name: 'choice', label: 'Choice', type: 'select', options: OPTIONS });
193
-
194
- // `SelectField` stamps this locator on the trigger it renders; the built-in
195
- // branch does not. Same role, different implementation.
196
- expect(document.querySelector('[data-testid="select-trigger-choice"]')).toBeNull();
197
- expect(trigger('choice')).toBeInTheDocument();
198
- });
199
- });
@@ -1,126 +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
- * `@object-ui/fields` ↔ `@objectstack/spec` symbol-collision guards
11
- * (objectui#3161, objectstack#4115 ledger batch 7).
12
- *
13
- * Two symbols, two different verdicts:
14
- *
15
- * - `isFileIdToken` was a character-for-character copy of the spec's function
16
- * under the spec's own name. It is a re-export now, and the gate below is
17
- * REFERENCE IDENTITY — not behaviour. A faithful copy passes every behaviour
18
- * test ever written for it (objectui#3003, re-proved on
19
- * `isAggregatedViewContainer` in objectui#3169), so `toBe` is the only
20
- * assertion that can tell a re-export from a fork.
21
- *
22
- * - `FieldWidgetProps` is now `FieldWidgetComponentProps`. The spec owns that
23
- * name for the DECLARED widget-plugin props contract; this package's is the
24
- * React interface its widgets implement. The pins record what still makes
25
- * re-exporting impossible (`field` is the far richer `FieldMetadata` here)
26
- * and, since objectui#3222, what no longer does: the validation slot is the
27
- * spec's `error` on both sides.
28
- *
29
- * The other direction — "the spec must not start owning the NEW names" — is not
30
- * asserted here because `scripts/check-spec-symbol-derivation.mjs` already
31
- * answers it on every CI run, for every name in the package rather than the
32
- * handful a test would remember to list.
33
- */
34
-
35
- import { describe, it, expect } from 'vitest';
36
- import { isFileIdToken as specIsFileIdToken } from '@objectstack/spec/data';
37
- import type { FieldWidgetProps as SpecFieldWidgetProps } from '@objectstack/spec/ui';
38
-
39
- import { isFileIdToken, fileIdOf } from '../widgets/file-value';
40
- import type { FieldWidgetComponentProps } from '../widgets/types';
41
-
42
- describe('isFileIdToken IS the spec function', () => {
43
- it('is the same function object, not an equivalent one', () => {
44
- // The whole point. Every value/behaviour assertion below this line would
45
- // also pass against a re-forked copy; this one would not.
46
- expect(isFileIdToken).toBe(specIsFileIdToken);
47
- });
48
-
49
- it('is the arbiter the rest of the module routes through', () => {
50
- // `fileIdOf` must keep agreeing with the shared predicate, so a future
51
- // "small local tweak" to the id shape cannot be reintroduced one caller at
52
- // a time.
53
- for (const value of ['abc123', 'AbC-_9', 'x'.repeat(64)]) {
54
- expect(specIsFileIdToken(value)).toBe(true);
55
- expect(fileIdOf(value)).toBe(value);
56
- }
57
- for (const value of ['https://example.com/f.png', '/api/v1/storage/files/1', 'a.b', 'x'.repeat(65)]) {
58
- expect(specIsFileIdToken(value)).toBe(false);
59
- expect(fileIdOf(value)).toBeUndefined();
60
- }
61
- });
62
- });
63
-
64
- /* -------------------------------------------------------------------------- */
65
- /* Compile-time pins. A violation is a `tsc` error, not a runtime failure. */
66
- /* This package's tsconfig.json includes its tests, so `type-check` compiles */
67
- /* them (scripts/check-type-check-coverage.mjs keeps that true). */
68
- /* -------------------------------------------------------------------------- */
69
-
70
- type Assert<T extends true> = T;
71
- type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
72
- type Extends<A, B> = [A] extends [B] ? true : false;
73
- type IsAny<T> = 0 extends 1 & T ? true : false;
74
- type HasKey<T, K extends string> = K extends keyof T ? true : false;
75
-
76
- describe('FieldWidgetComponentProps is the RENDERED layer of the spec contract', () => {
77
- it('is pinned at compile time', () => {
78
- // The import above is itself half the tripwire: the day the spec stops
79
- // exporting `FieldWidgetProps`, this file stops compiling and the rename's
80
- // reason is up for re-triage (the plain name could come back).
81
- type _SpecIsReal = Assert<Equal<IsAny<SpecFieldWidgetProps>, false>>;
82
-
83
- // The validation-message slot no longer diverges. objectui#3161 could only
84
- // RECORD that the declared contract called it `error` while every widget
85
- // here read `errorMessage`; objectui#3222 resolved it in the direction the
86
- // contract points — this package adopted `error`, and the form renderer
87
- // started producing it, so the name and the delivery landed together.
88
- // Both sides are pinned: the spec still owns the name, and this package
89
- // still spells it the same way.
90
- type _SpecNamesItError = Assert<HasKey<SpecFieldWidgetProps, 'error'>>;
91
- type _LocalNamesItErrorToo = Assert<HasKey<FieldWidgetComponentProps, 'error'>>;
92
- type _SameOptionalStringSlot = Assert<
93
- Equal<FieldWidgetComponentProps['error'], SpecFieldWidgetProps['error']>
94
- >;
95
- // The old name is gone, not kept as an alias. An alias would be exactly the
96
- // lenient second dialect AGENTS.md #0.1 forbids — and the reason a missed
97
- // call site would have gone quiet again.
98
- type _OldNameRetired = Assert<Equal<HasKey<FieldWidgetComponentProps, 'errorMessage'>, false>>;
99
-
100
- // …and the reason nobody noticed used to sit right here: three pins
101
- // asserting that `[key: string]: any` was still present, and that
102
- // `props.required` / `props.error` therefore read as `any`. objectui#3221
103
- // removed that index signature, so those pins have gone red on purpose and
104
- // are gone with it. What replaces them is the inverse claim — the type is
105
- // now CLOSED, so a key the spec declares and this one does not can finally
106
- // be reported as missing. That is what made objectui#3222's rename
107
- // decidable by the compiler instead of by a symbol guard.
108
- type _NoStringIndexSignature = Assert<Equal<Extends<string, keyof FieldWidgetComponentProps>, false>>;
109
- // `required` is the one key that stayed behind, and deliberately so
110
- // (objectui#3222): the required marker has a single author — the form
111
- // renderer's `<FormLabel>` — and lowering the flag into widget props gives
112
- // it a second, i.e. the double-display failure that also keeps the
113
- // validation TEXT out of the widget. The a11y state a widget could
114
- // legitimately carry is `aria-required`, and `AriaAttributes` already
115
- // supplies that key with no contract change at all.
116
- type _RequiredIsAbsent = Assert<Equal<HasKey<FieldWidgetComponentProps, 'required'>, false>>;
117
-
118
- // `data-*` stays open (it is open in HTML too), but as a template-literal
119
- // key — the distinction that keeps `keyof` finite above. A pin, because
120
- // widening it back to `[key: string]` would silently restore the defect
121
- // while every assertion here still read as "closed".
122
- type _DataAttributesStayOpen = Assert<Extends<'data-testid', keyof FieldWidgetComponentProps>>;
123
-
124
- expect(true).toBe(true);
125
- });
126
- });