@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,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
- });