@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,248 +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
- * The REGISTERED path's fullscreen long-text dialog speaks the session locale —
11
- * objectui#3404.
12
- *
13
- * This is the second half of objectui#3272. That issue translated the built-in
14
- * branch (`FullscreenTextarea` in `components/src/renderers/form/form.tsx`,
15
- * pinned next door in `form-fullscreen-textarea-i18n.test.tsx`); the shared
16
- * `FullscreenFieldEditor` that `TextAreaField` and `RichTextField` render was
17
- * left with four English literals — the toggle's accessible name
18
- * (`Edit ${label} fullscreen`), the title fallback `Edit text`, `Cancel` and
19
- * `Done`.
20
- *
21
- * Nothing about the keys was missing: `form.fullscreen.*` and `common.cancel`
22
- * have shipped in all ten locale packs since #3272. This path simply never
23
- * consumed them, so the defect was visible INSIDE A SINGLE FORM: with
24
- * `ObjectFormSchema.mobile.fullscreenLongText` on, `ObjectForm` stamps
25
- * `mobile_fullscreen` onto every long-text field, and a zh session got
26
- * 「取消 / 完成」 from a built-in-rendered field and `Cancel / Done` from a
27
- * registered-widget one.
28
- *
29
- * ## What the cases assert, and why in this shape
30
- *
31
- * - **Both widgets.** `TextAreaField` (`field:textarea`) and `RichTextField`
32
- * (`field:markdown` / `field:html`) are the two hosts, and the copy lives in
33
- * the component they share. Covering one would leave a regression that broke
34
- * only the other's props path (`label`, `testIdPrefix`) unreported.
35
- * - **`en` is a NO-OP pin, not a defect reproducer.** The four literals were
36
- * byte-identical to the `en` pack, so the English cases below were green
37
- * before this change too. They are here to prove the swap did not alter the
38
- * English rendering — the direction that only a positive assertion can see.
39
- * - **Non-`en` positive AND English-literal negative, together.** A re-inlined
40
- * literal rendered next to a translated sibling still satisfies a
41
- * positive-only assertion. `ja` joins `zh` because the toggle's name is
42
- * INTERPOLATED (`{{label}}` + a translated generic noun) and the two packs
43
- * put the noun on opposite sides — a build that dropped the interpolation
44
- * passes a zh-only check by luck.
45
- * - **Un-labelled fixtures for the generic copy.** `form.fullscreen.title` and
46
- * `form.fullscreen.textFallback` are reachable only when the field has no
47
- * `label` (a labelled field titles the dialog with its own label, in the
48
- * author's language, on both paths). The labelled stack is pinned separately
49
- * at the bottom, where what matters is that `{{label}}` is interpolated into
50
- * translated surroundings rather than concatenated in English.
51
- */
52
- import { describe, it, expect, vi } from 'vitest';
53
- import { render, screen, fireEvent } from '@testing-library/react';
54
- import '@testing-library/jest-dom';
55
- import { I18nProvider } from '@object-ui/i18n';
56
-
57
- import { TextAreaField } from '../TextAreaField';
58
- import { RichTextField } from '../RichTextField';
59
- import type { FieldMetadata } from '@object-ui/types';
60
-
61
- /**
62
- * No `label` — the one authoring stack the generic copy is written for. The
63
- * flag is spelled `mobile_fullscreen`, the single carrier `ObjectForm`
64
- * produces (#3233/#3301).
65
- */
66
- const textareaField = (extra: Record<string, unknown> = {}) =>
67
- ({ name: 'notes', type: 'textarea', mobile_fullscreen: true, ...extra }) as unknown as FieldMetadata;
68
-
69
- const markdownField = (extra: Record<string, unknown> = {}) =>
70
- ({ name: 'notes', type: 'markdown', mobile_fullscreen: true, ...extra }) as unknown as FieldMetadata;
71
-
72
- function renderIn(language: string, element: React.ReactElement) {
73
- return render(
74
- <I18nProvider config={{ defaultLanguage: language, detectBrowserLanguage: false }}>
75
- {element}
76
- </I18nProvider>,
77
- );
78
- }
79
-
80
- /** Open the dialog and hand back the trigger, so its name can be asserted too. */
81
- function openFullscreen(prefix: 'textarea' | 'richtext') {
82
- const toggle = screen.getByTestId(`${prefix}-fullscreen-toggle`);
83
- fireEvent.click(toggle);
84
- return toggle;
85
- }
86
-
87
- describe('TextAreaField fullscreen dialog is translated (objectui#3404)', () => {
88
- it('renders the English copy under an en provider', () => {
89
- renderIn('en', <TextAreaField value="hello" onChange={vi.fn()} field={textareaField()} />);
90
- const toggle = openFullscreen('textarea');
91
-
92
- // Byte-identical to the literals this replaced, so `en` is a no-op change.
93
- expect(toggle).toHaveAttribute('aria-label', 'Edit text fullscreen');
94
- expect(screen.getByText('Edit text')).toBeInTheDocument();
95
- expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
96
- expect(screen.getByRole('button', { name: 'Done' })).toBeInTheDocument();
97
- });
98
-
99
- it('renders the whole dialog in Chinese under a zh provider', () => {
100
- renderIn('zh', <TextAreaField value="hello" onChange={vi.fn()} field={textareaField()} />);
101
- const toggle = openFullscreen('textarea');
102
-
103
- expect(toggle).toHaveAttribute('aria-label', '全屏编辑文本');
104
- expect(screen.getByText('编辑文本')).toBeInTheDocument();
105
- expect(screen.getByRole('button', { name: '取消' })).toBeInTheDocument();
106
- expect(screen.getByRole('button', { name: '完成' })).toBeInTheDocument();
107
-
108
- // The four literals, asserted absent. `Edit text` is matched loosely
109
- // because it was BOTH the dialog title and part of the trigger's name.
110
- expect(screen.queryByText(/edit text/i)).not.toBeInTheDocument();
111
- expect(screen.queryByRole('button', { name: 'Cancel' })).not.toBeInTheDocument();
112
- expect(screen.queryByRole('button', { name: 'Done' })).not.toBeInTheDocument();
113
- expect(toggle.getAttribute('aria-label')).not.toMatch(/fullscreen/i);
114
- });
115
-
116
- it('renders the whole dialog in Japanese under a ja provider', () => {
117
- renderIn('ja', <TextAreaField value="hello" onChange={vi.fn()} field={textareaField()} />);
118
- const toggle = openFullscreen('textarea');
119
-
120
- expect(toggle).toHaveAttribute('aria-label', 'テキストを全画面で編集');
121
- expect(screen.getByText('テキストを編集')).toBeInTheDocument();
122
- expect(screen.getByRole('button', { name: 'キャンセル' })).toBeInTheDocument();
123
- expect(screen.getByRole('button', { name: '完了' })).toBeInTheDocument();
124
-
125
- expect(screen.queryByText(/edit text/i)).not.toBeInTheDocument();
126
- expect(screen.queryByRole('button', { name: 'Done' })).not.toBeInTheDocument();
127
- });
128
- });
129
-
130
- describe('RichTextField fullscreen dialog is translated (objectui#3404)', () => {
131
- // The second host of the SAME component. The copy is shared, the props path
132
- // into it (`label`, `testIdPrefix`) is not.
133
- it('renders the English copy under an en provider', () => {
134
- renderIn('en', <RichTextField value="# hello" onChange={vi.fn()} field={markdownField()} />);
135
- const toggle = openFullscreen('richtext');
136
-
137
- expect(toggle).toHaveAttribute('aria-label', 'Edit text fullscreen');
138
- expect(screen.getByText('Edit text')).toBeInTheDocument();
139
- expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
140
- expect(screen.getByRole('button', { name: 'Done' })).toBeInTheDocument();
141
- });
142
-
143
- it('renders the whole dialog in Chinese under a zh provider', () => {
144
- renderIn('zh', <RichTextField value="# hello" onChange={vi.fn()} field={markdownField()} />);
145
- const toggle = openFullscreen('richtext');
146
-
147
- expect(toggle).toHaveAttribute('aria-label', '全屏编辑文本');
148
- expect(screen.getByText('编辑文本')).toBeInTheDocument();
149
- expect(screen.getByRole('button', { name: '取消' })).toBeInTheDocument();
150
- expect(screen.getByRole('button', { name: '完成' })).toBeInTheDocument();
151
-
152
- expect(screen.queryByText(/edit text/i)).not.toBeInTheDocument();
153
- expect(screen.queryByRole('button', { name: 'Cancel' })).not.toBeInTheDocument();
154
- expect(screen.queryByRole('button', { name: 'Done' })).not.toBeInTheDocument();
155
- expect(toggle.getAttribute('aria-label')).not.toMatch(/fullscreen/i);
156
- });
157
- });
158
-
159
- describe('fullscreen dialog copy — the field label is interpolated, not concatenated (objectui#3404)', () => {
160
- it("titles the dialog with the author's label and interpolates it into the trigger name", () => {
161
- // A labelled field keeps its own label as the title on both paths — what
162
- // must be translated around it is the trigger's sentence. `Edit Notes
163
- // fullscreen` was the literal; zh wraps the label instead of prefixing it.
164
- renderIn(
165
- 'zh',
166
- <TextAreaField value="hello" onChange={vi.fn()} field={textareaField({ label: 'Notes' })} />,
167
- );
168
- const toggle = openFullscreen('textarea');
169
-
170
- expect(toggle).toHaveAttribute('aria-label', '全屏编辑Notes');
171
- expect(screen.getByText('Notes')).toBeInTheDocument();
172
- expect(toggle.getAttribute('aria-label')).not.toMatch(/^Edit /);
173
- });
174
-
175
- it('still commits the draft — translating the footer did not unwire it', () => {
176
- // "Done" lost its literal child. This pins that the click handler still
177
- // rides on the translated button and not on some node that merely used to
178
- // carry the old text.
179
- const onChange = vi.fn();
180
- renderIn('zh', <TextAreaField value="hello" onChange={onChange} field={textareaField()} />);
181
- openFullscreen('textarea');
182
-
183
- fireEvent.change(screen.getByTestId('textarea-fullscreen-input'), {
184
- target: { value: 'hello from fullscreen' },
185
- });
186
- fireEvent.click(screen.getByRole('button', { name: '完成' }));
187
-
188
- expect(onChange).toHaveBeenCalledWith('hello from fullscreen');
189
- expect(screen.queryByTestId('textarea-fullscreen-dialog')).not.toBeInTheDocument();
190
- });
191
-
192
- it('still discards the draft on the translated Cancel', () => {
193
- const onChange = vi.fn();
194
- renderIn('zh', <TextAreaField value="hello" onChange={onChange} field={textareaField()} />);
195
- openFullscreen('textarea');
196
-
197
- fireEvent.change(screen.getByTestId('textarea-fullscreen-input'), {
198
- target: { value: 'discarded' },
199
- });
200
- fireEvent.click(screen.getByRole('button', { name: '取消' }));
201
-
202
- expect(onChange).not.toHaveBeenCalled();
203
- expect(screen.queryByTestId('textarea-fullscreen-dialog')).not.toBeInTheDocument();
204
- });
205
- });
206
-
207
- describe('fullscreen dialog has an accessible description (objectui#3404)', () => {
208
- /**
209
- * The issue listed this as an unverified aside. What was measured: Radix
210
- * only sets `aria-describedby` when a `Dialog.Description` is present
211
- * (`descriptionPresent ? descriptionId : undefined`), so before this change
212
- * the registered path's dialog had NO accessible description while the
213
- * byte-identical built-in one did, and `form.fullscreen.description` was a
214
- * key with no consumer on this path.
215
- *
216
- * Explicitly NOT justified by a console warning: `@radix-ui/react-dialog`
217
- * 1.1.23 (the pinned version) contains no `console.*` call at all — the
218
- * missing-Description warning older versions emitted is gone upstream, and
219
- * no warning fired in the baseline run.
220
- */
221
- it('wires aria-describedby to the translated sr-only description', () => {
222
- renderIn('zh', <TextAreaField value="hello" onChange={vi.fn()} field={textareaField()} />);
223
- openFullscreen('textarea');
224
-
225
- const dialog = screen.getByTestId('textarea-fullscreen-dialog');
226
- const describedBy = dialog.getAttribute('aria-describedby');
227
- expect(describedBy).toBeTruthy();
228
-
229
- const description = document.getElementById(describedBy!);
230
- expect(description).not.toBeNull();
231
- expect(description).toHaveTextContent('编辑完整的文本内容,然后保存或取消更改。');
232
- });
233
-
234
- it('renders the English description under an en provider', () => {
235
- renderIn('en', <TextAreaField value="hello" onChange={vi.fn()} field={textareaField()} />);
236
- openFullscreen('textarea');
237
-
238
- expect(
239
- screen.getByText('Edit the full text value, then save or cancel your changes.'),
240
- ).toBeInTheDocument();
241
- });
242
- });
243
-
244
- // The provider-LESS leg lives in `FullscreenFieldEditor.no-provider.test.tsx`.
245
- // It cannot share this file: mounting `I18nProvider` installs its instance as
246
- // react-i18next's GLOBAL default, so after any case above there is no
247
- // "no provider" state left in this module graph to observe. See that file's
248
- // header — it was measured, not assumed.
@@ -1,116 +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
- * The shared fullscreen long-text dialog with NO `I18nProvider` mounted
11
- * (objectui#3404) — standalone/embedded hosts, and most of this package's own
12
- * bare-render tests.
13
- *
14
- * ## Why this leg decided the implementation
15
- *
16
- * Routing the copy through i18n must not turn it into raw keys where nothing
17
- * is configured. MEASURED on the pre-change tree with a probe rendering bare
18
- * `useObjectTranslation()` and no provider:
19
- *
20
- * {"cancel":"common.cancel","done":"form.fullscreen.done",
21
- * "title":"form.fullscreen.title","toggle":"form.fullscreen.toggle"}
22
- *
23
- * Raw keys on every one, and the `{{label}}` interpolation gone entirely —
24
- * strictly WORSE than the English literals being replaced. So
25
- * `FullscreenFieldEditor` consumes `useFieldTranslation()`
26
- * (`createSafeTranslation` + English defaults, the same shape the built-in
27
- * branch has used since objectui#3272) rather than the bare hook, and the
28
- * defaults are byte-identical to the old literals so this rendering is
29
- * unchanged.
30
- *
31
- * ## Why a separate FILE and not another `describe`
32
- *
33
- * Mounting `I18nProvider` calls `initReactI18next`, which installs that
34
- * instance as react-i18next's GLOBAL default. Once any sibling case has done
35
- * so there is no "no provider" state left in the module graph, and these
36
- * assertions would pass by reading that leaked instance instead of the
37
- * fallback — green for the wrong reason. That is not a hypothetical: these
38
- * cases were FIRST written inside `FullscreenFieldEditor.i18n.test.tsx`, and
39
- * the reverse check (swap `useFieldTranslation` back to bare
40
- * `useObjectTranslation`, expect red) left them GREEN there while it correctly
41
- * turned `RichTextField.mobileFullscreen.test.tsx` red. Split out, the same
42
- * swap turns them red — which is what makes them a pin.
43
- *
44
- * Same split, same reason as `TagsField.placeholder.no-provider.test.tsx` and
45
- * `OptionsEmptyState.no-provider.test.tsx`; vitest's per-file isolation is
46
- * what makes the observation honest.
47
- */
48
- import { describe, it, expect, vi } from 'vitest';
49
- import { render, screen, fireEvent } from '@testing-library/react';
50
- import '@testing-library/jest-dom';
51
-
52
- import { TextAreaField } from '../TextAreaField';
53
- import type { FieldMetadata } from '@object-ui/types';
54
-
55
- const textareaField = (extra: Record<string, unknown> = {}) =>
56
- ({ name: 'notes', type: 'textarea', mobile_fullscreen: true, ...extra }) as unknown as FieldMetadata;
57
-
58
- function openFullscreen() {
59
- const toggle = screen.getByTestId('textarea-fullscreen-toggle');
60
- fireEvent.click(toggle);
61
- return toggle;
62
- }
63
-
64
- describe('fullscreen dialog copy — English fallback survives with no i18n configured (objectui#3404)', () => {
65
- it('renders the English defaults, not raw keys', () => {
66
- render(<TextAreaField value="hello" onChange={vi.fn()} field={textareaField()} />);
67
- const toggle = openFullscreen();
68
-
69
- // Byte-identical to the literals this change replaced.
70
- expect(toggle).toHaveAttribute('aria-label', 'Edit text fullscreen');
71
- expect(screen.getByText('Edit text')).toBeInTheDocument();
72
- expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
73
- expect(screen.getByRole('button', { name: 'Done' })).toBeInTheDocument();
74
-
75
- // The measured failure mode, stated directly — a raw key reaching a user.
76
- expect(document.body.textContent).not.toMatch(/form\.fullscreen\./);
77
- expect(document.body.textContent).not.toMatch(/common\.cancel/);
78
- });
79
-
80
- it('still interpolates the field label into the trigger name', () => {
81
- // `createSafeTranslation`'s fallback does its own `{{k}}` replacement.
82
- // Bare `useObjectTranslation` returns `form.fullscreen.toggle` verbatim
83
- // here, label dropped.
84
- render(
85
- <TextAreaField value="hello" onChange={vi.fn()} field={textareaField({ label: 'Notes' })} />,
86
- );
87
-
88
- expect(openFullscreen()).toHaveAttribute('aria-label', 'Edit Notes fullscreen');
89
- });
90
-
91
- it('the code-shipped defaults carry no CJK (Commandment #-1)', () => {
92
- // With no provider mounted, whatever renders here came from CODE
93
- // (FIELD_DEFAULTS). Escaped ranges on purpose: a literal CJK class would
94
- // itself violate the commandment this asserts (same shape as
95
- // `TagsField.placeholder.no-provider.test.tsx`).
96
- render(<TextAreaField value="hello" onChange={vi.fn()} field={textareaField()} />);
97
- openFullscreen();
98
-
99
- expect(screen.getByTestId('textarea-fullscreen-dialog').textContent).not.toMatch(
100
- /[\u3000-\u30ff\u4e00-\u9fff]/,
101
- );
102
- });
103
-
104
- it('still gives the dialog an accessible description', () => {
105
- render(<TextAreaField value="hello" onChange={vi.fn()} field={textareaField()} />);
106
- openFullscreen();
107
-
108
- const describedBy = screen
109
- .getByTestId('textarea-fullscreen-dialog')
110
- .getAttribute('aria-describedby');
111
- expect(describedBy).toBeTruthy();
112
- expect(document.getElementById(describedBy!)).toHaveTextContent(
113
- 'Edit the full text value, then save or cancel your changes.',
114
- );
115
- });
116
- });
@@ -1,244 +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
- * A DISABLED `RichTextField` cannot be edited through its fullscreen dialog —
11
- * objectui#3402.
12
- *
13
- * The rich-text half of the same defect the sibling
14
- * `TextAreaField.fullscreenDisabled.test.tsx` pins, and it is a genuinely
15
- * separate code path rather than a copy of the assertions: this widget reaches
16
- * the affordance through `RichTextEditorSurface`'s `overlay` slot, and the
17
- * dialog renders the SAME surface again. `disabled` therefore has to be
18
- * resolved once and given to both renderings, exactly like `formatLabel` /
19
- * `hint` / `placeholder` already were — otherwise the two surfaces disagree
20
- * about whether the field is editable, which is the drift #3301 built the
21
- * shared surface to prevent.
22
- *
23
- * Measured on `origin/main` before the fix, by a dynamic probe (the issue
24
- * carried static evidence only):
25
- *
26
- * PROBE richtext: inline disabled = true , toggle disabled = false
27
- * PROBE richtext: dialog opened = true , dialog input disabled = false
28
- * PROBE richtext: onChange calls = [["# EDITED WHILE DISABLED"]]
29
- *
30
- * `readonly` was never part of this: the widget early-returns a read-only
31
- * display before the affordance is computed. That path is untouched by the fix
32
- * and pinned below as a non-regression.
33
- */
34
-
35
- import { describe, it, expect, vi } from 'vitest';
36
- import { render, screen, fireEvent } from '@testing-library/react';
37
- import '@testing-library/jest-dom';
38
-
39
- import { RichTextField } from '../RichTextField';
40
- import type { FieldMetadata } from '@object-ui/types';
41
-
42
- const fieldMeta = (extra: Record<string, unknown> = {}) =>
43
- ({
44
- name: 'notes',
45
- label: 'Notes',
46
- type: 'markdown',
47
- mobile_fullscreen: true,
48
- ...extra,
49
- }) as unknown as FieldMetadata;
50
-
51
- const toggle = () =>
52
- screen.queryByTestId('richtext-fullscreen-toggle') as HTMLButtonElement | null;
53
- const dialogInput = () =>
54
- screen.queryByTestId('richtext-fullscreen-input') as HTMLTextAreaElement | null;
55
- const saveButton = () =>
56
- screen.queryByTestId('richtext-fullscreen-save') as HTMLButtonElement | null;
57
-
58
- describe('RichTextField fullscreen — disabled keeps the affordance but makes it inert (objectui#3402)', () => {
59
- it('keeps the expand button and disables it', () => {
60
- render(
61
- <RichTextField value="# hello" onChange={() => {}} field={fieldMeta()} disabled />,
62
- );
63
-
64
- expect(toggle()).not.toBeNull();
65
- expect(toggle()!.disabled).toBe(true);
66
- });
67
-
68
- it('does not open the dialog when that button is clicked anyway', () => {
69
- render(
70
- <RichTextField value="# hello" onChange={() => {}} field={fieldMeta()} disabled />,
71
- );
72
-
73
- fireEvent.click(toggle()!);
74
-
75
- expect(screen.queryByTestId('richtext-fullscreen-dialog')).not.toBeInTheDocument();
76
- expect(dialogInput()).toBeNull();
77
- });
78
-
79
- it('still disables the inline editing surface', () => {
80
- render(
81
- <RichTextField value="# hello" onChange={() => {}} field={fieldMeta()} disabled />,
82
- );
83
-
84
- const inline = screen.getAllByRole('textbox')[0] as HTMLTextAreaElement;
85
- expect(inline.disabled).toBe(true);
86
- });
87
-
88
- it('never commits a value while disabled', () => {
89
- const onChange = vi.fn();
90
- render(
91
- <RichTextField value="# hello" onChange={onChange} field={fieldMeta()} disabled />,
92
- );
93
-
94
- fireEvent.click(toggle()!);
95
-
96
- expect(onChange).not.toHaveBeenCalled();
97
- });
98
- });
99
-
100
- describe('RichTextField fullscreen — the dialog holds on its own when disabled arrives mid-edit (objectui#3402)', () => {
101
- /**
102
- * The `isSubmitting` shape at widget level: the form renderer has already
103
- * OR-ed `isSubmitting` into the `disabled` this widget receives, so a submit
104
- * starting under an open dialog looks exactly like this rerender.
105
- */
106
- function openThenDisable(onChange: () => void) {
107
- const { rerender } = render(
108
- <RichTextField value="# hello" onChange={onChange} field={fieldMeta()} />,
109
- );
110
- fireEvent.click(toggle()!);
111
- expect(dialogInput()).not.toBeNull();
112
-
113
- rerender(
114
- <RichTextField value="# hello" onChange={onChange} field={fieldMeta()} disabled />,
115
- );
116
- }
117
-
118
- it('locks the dialog controls', () => {
119
- openThenDisable(vi.fn());
120
-
121
- expect(dialogInput()).not.toBeNull();
122
- expect(dialogInput()!.disabled).toBe(true);
123
- expect(saveButton()!.disabled).toBe(true);
124
- });
125
-
126
- /** Split from the attribute assertions for the reason its textarea sibling
127
- * spells out: as a tail on them it could only run when the native attribute
128
- * was already doing the work. */
129
- it('refuses the write-back even when "Done" is driven directly', () => {
130
- const onChange = vi.fn();
131
- openThenDisable(onChange);
132
-
133
- fireEvent.change(dialogInput()!, { target: { value: '# EDITED WHILE SUBMITTING' } });
134
- fireEvent.click(saveButton()!);
135
-
136
- expect(onChange).not.toHaveBeenCalled();
137
- });
138
-
139
- it('still lets the user out of the dialog', () => {
140
- const onChange = vi.fn();
141
- const { rerender } = render(
142
- <RichTextField value="# hello" onChange={onChange} field={fieldMeta()} />,
143
- );
144
-
145
- fireEvent.click(toggle()!);
146
- rerender(
147
- <RichTextField value="# hello" onChange={onChange} field={fieldMeta()} disabled />,
148
- );
149
-
150
- fireEvent.click(screen.getByRole('button', { name: /cancel/i }));
151
-
152
- expect(screen.queryByTestId('richtext-fullscreen-dialog')).not.toBeInTheDocument();
153
- expect(onChange).not.toHaveBeenCalled();
154
- });
155
- });
156
-
157
- describe('RichTextField fullscreen — the states the fix must not have moved (objectui#3402)', () => {
158
- it('still edits fullscreen and commits when the field is editable', () => {
159
- const onChange = vi.fn();
160
- render(<RichTextField value="# hello" onChange={onChange} field={fieldMeta()} />);
161
-
162
- expect(toggle()!.disabled).toBe(false);
163
- fireEvent.click(toggle()!);
164
-
165
- const input = dialogInput()!;
166
- expect(input.disabled).toBe(false);
167
- expect(saveButton()!.disabled).toBe(false);
168
-
169
- fireEvent.change(input, { target: { value: '# hello from fullscreen' } });
170
- fireEvent.click(saveButton()!);
171
-
172
- expect(onChange).toHaveBeenCalledWith('# hello from fullscreen');
173
- });
174
-
175
- it('keeps the dialog holding the REAL editor, disabled and not', () => {
176
- // #3301's acceptance criterion, re-pinned under `disabled`: the dialog
177
- // shows the same surface as the inline one (same format indicator), and now
178
- // the same interactivity too — the very thing that had drifted.
179
- const onChange = vi.fn();
180
- const { rerender } = render(
181
- <RichTextField
182
- value=""
183
- onChange={onChange}
184
- field={fieldMeta({ format: 'html' })}
185
- />,
186
- );
187
-
188
- fireEvent.click(toggle()!);
189
- expect(screen.getAllByText('Format: html')).toHaveLength(2);
190
-
191
- rerender(
192
- <RichTextField
193
- value=""
194
- onChange={onChange}
195
- field={fieldMeta({ format: 'html' })}
196
- disabled
197
- />,
198
- );
199
-
200
- // Both surfaces present, both inert.
201
- expect(screen.getAllByText('Format: html')).toHaveLength(2);
202
- // Queried off the document rather than by role: the dialog is modal, so
203
- // Radix marks the background `aria-hidden` and a role query sees only the
204
- // dialog's own control — which is precisely the textarea NOT at issue here.
205
- // Both surfaces have to be disabled, so both are read directly.
206
- const textareas = Array.from(
207
- document.body.querySelectorAll('textarea'),
208
- ) as HTMLTextAreaElement[];
209
- expect(textareas).toHaveLength(2);
210
- expect(textareas.every((el) => el.disabled)).toBe(true);
211
- });
212
-
213
- it('still renders no affordance at all in readonly mode', () => {
214
- // The untouched early return — why readonly was never part of this defect.
215
- render(
216
- <RichTextField value="# hello" onChange={() => {}} readonly field={fieldMeta()} />,
217
- );
218
-
219
- expect(toggle()).toBeNull();
220
- expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
221
- expect(screen.getByText('# hello')).toBeInTheDocument();
222
- });
223
-
224
- it('still renders no affordance when the metadata does not opt in, disabled or not', () => {
225
- const { rerender } = render(
226
- <RichTextField
227
- value="# hello"
228
- onChange={() => {}}
229
- field={fieldMeta({ mobile_fullscreen: false })}
230
- />,
231
- );
232
- expect(toggle()).toBeNull();
233
-
234
- rerender(
235
- <RichTextField
236
- value="# hello"
237
- onChange={() => {}}
238
- field={fieldMeta({ mobile_fullscreen: false })}
239
- disabled
240
- />,
241
- );
242
- expect(toggle()).toBeNull();
243
- });
244
- });