@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,360 +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
- import { render, screen, fireEvent, waitFor } from '@testing-library/react';
10
- import { describe, it, expect, vi, beforeEach } from 'vitest';
11
- import { PeoplePicker } from './PeoplePicker';
12
- import { pushRecentLookupId } from './recentLookups';
13
-
14
- const users = [
15
- { id: 'u1', name: 'Amy Lin', email: 'amy@x.io', primary_business_unit_id: { name: 'Sales' } },
16
- { id: 'u2', name: 'Bob Wu', email: 'bob@x.io' },
17
- { id: 'u3', name: 'Cara Xu', email: 'cara@x.io' },
18
- ];
19
-
20
- function makeDataSource() {
21
- const find = vi.fn(async (_obj: string, params: any) => {
22
- const idIn = params?.$filter?.id?.$in as any[] | undefined;
23
- if (idIn) {
24
- const set = new Set(idIn.map(String));
25
- const data = users.filter(u => set.has(String(u.id)));
26
- return { data, total: data.length };
27
- }
28
- const search: string | undefined = params?.$search;
29
- const data = search
30
- ? users.filter(u => u.name.toLowerCase().includes(search.toLowerCase()))
31
- : users;
32
- return { data, total: data.length };
33
- });
34
- return { find } as any;
35
- }
36
-
37
- const baseProps = {
38
- open: true,
39
- objectName: 'sys_user',
40
- subtitleFields: ['primary_business_unit_id.name', 'email'],
41
- };
42
-
43
- beforeEach(() => {
44
- // jsdom has no matchMedia; useIsMobile needs it. Default to desktop width.
45
- Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 1280 });
46
- window.matchMedia = ((query: string) => ({
47
- matches: window.innerWidth < 768,
48
- media: query,
49
- onchange: null,
50
- addEventListener: () => {},
51
- removeEventListener: () => {},
52
- addListener: () => {},
53
- removeListener: () => {},
54
- dispatchEvent: () => false,
55
- })) as any;
56
- try {
57
- window.localStorage.clear();
58
- } catch {
59
- /* ignore */
60
- }
61
- });
62
-
63
- describe('PeoplePicker', () => {
64
- it('lists candidate rows on open and filters by $search', async () => {
65
- const ds = makeDataSource();
66
- render(
67
- <PeoplePicker
68
- {...baseProps}
69
- dataSource={ds}
70
- onOpenChange={vi.fn()}
71
- onSelect={vi.fn()}
72
- />,
73
- );
74
-
75
- await waitFor(() => expect(screen.getByText('Amy Lin')).toBeTruthy());
76
- expect(screen.getByText('Sales · amy@x.io')).toBeTruthy();
77
- expect(screen.getAllByTestId('person-row')).toHaveLength(3);
78
-
79
- fireEvent.change(screen.getByTestId('people-picker-search'), { target: { value: 'amy' } });
80
- await waitFor(() => expect(screen.getAllByTestId('person-row')).toHaveLength(1));
81
- // Relation subtitle (primary_business_unit_id.name) auto-adds $expand.
82
- expect(ds.find).toHaveBeenLastCalledWith('sys_user', {
83
- $top: 25,
84
- $search: 'amy',
85
- $expand: ['primary_business_unit_id'],
86
- });
87
- });
88
-
89
- it('applies base candidate filters (banned != true)', async () => {
90
- const ds = makeDataSource();
91
- render(
92
- <PeoplePicker
93
- {...baseProps}
94
- dataSource={ds}
95
- lookupFilters={[{ field: 'banned', operator: 'ne', value: true }]}
96
- onOpenChange={vi.fn()}
97
- onSelect={vi.fn()}
98
- />,
99
- );
100
- await waitFor(() =>
101
- expect(ds.find).toHaveBeenCalledWith('sys_user', {
102
- $top: 25,
103
- $filter: { banned: { $ne: true } },
104
- $expand: ['primary_business_unit_id'],
105
- }),
106
- );
107
- });
108
-
109
- it('single-select commits immediately and closes', async () => {
110
- const ds = makeDataSource();
111
- const onSelect = vi.fn();
112
- const onSelectRecords = vi.fn();
113
- const onOpenChange = vi.fn();
114
- render(
115
- <PeoplePicker
116
- {...baseProps}
117
- dataSource={ds}
118
- onSelect={onSelect}
119
- onSelectRecords={onSelectRecords}
120
- onOpenChange={onOpenChange}
121
- />,
122
- );
123
- await waitFor(() => expect(screen.getByText('Amy Lin')).toBeTruthy());
124
-
125
- fireEvent.click(screen.getByText('Amy Lin'));
126
- expect(onSelect).toHaveBeenCalledWith('u1');
127
- expect(onSelectRecords).toHaveBeenCalledWith([users[0]]);
128
- expect(onOpenChange).toHaveBeenCalledWith(false);
129
- });
130
-
131
- it('multi-select echoes chips and confirms ids + records', async () => {
132
- const ds = makeDataSource();
133
- const onSelect = vi.fn();
134
- const onSelectRecords = vi.fn();
135
- render(
136
- <PeoplePicker
137
- {...baseProps}
138
- multiple
139
- dataSource={ds}
140
- onSelect={onSelect}
141
- onSelectRecords={onSelectRecords}
142
- onOpenChange={vi.fn()}
143
- />,
144
- );
145
- await waitFor(() => expect(screen.getByText('Amy Lin')).toBeTruthy());
146
-
147
- fireEvent.click(screen.getByText('Amy Lin'));
148
- fireEvent.click(screen.getByText('Bob Wu'));
149
- await waitFor(() => expect(screen.getAllByTestId('selection-chip')).toHaveLength(2));
150
-
151
- fireEvent.click(screen.getByText('Confirm'));
152
- expect(onSelect).toHaveBeenCalledWith(['u1', 'u2']);
153
- expect(onSelectRecords).toHaveBeenCalledWith([users[0], users[1]]);
154
- });
155
-
156
- it('surfaces recent contacts in their own section when idle', async () => {
157
- pushRecentLookupId('sys_user', 'u3');
158
- const ds = makeDataSource();
159
- render(
160
- <PeoplePicker
161
- {...baseProps}
162
- dataSource={ds}
163
- onOpenChange={vi.fn()}
164
- onSelect={vi.fn()}
165
- />,
166
- );
167
- await waitFor(() => expect(screen.getByText('Recently used')).toBeTruthy());
168
- // Cara appears under "Recently used" and is de-duped out of the main list.
169
- expect(screen.getByText('Cara Xu')).toBeTruthy();
170
- expect(screen.getByText('All results')).toBeTruthy();
171
- });
172
-
173
- it('keyboard: ArrowDown then Enter commits the active row (single)', async () => {
174
- const ds = makeDataSource();
175
- const onSelect = vi.fn();
176
- render(
177
- <PeoplePicker {...baseProps} dataSource={ds} onSelect={onSelect} onSelectRecords={vi.fn()} onOpenChange={vi.fn()} />,
178
- );
179
- await waitFor(() => expect(screen.getByText('Amy Lin')).toBeTruthy());
180
- const search = screen.getByTestId('people-picker-search');
181
- fireEvent.keyDown(search, { key: 'ArrowDown' });
182
- // Wait for the cursor to actually land on the first row before committing —
183
- // guards against a late background re-render swallowing the keypress (the
184
- // old identity-keyed cursor reset made this flaky on slow CI).
185
- await waitFor(() =>
186
- expect(screen.getAllByTestId('person-row')[0].getAttribute('data-active')).toBe('true'),
187
- );
188
- fireEvent.keyDown(search, { key: 'Enter' });
189
- expect(onSelect).toHaveBeenCalledWith('u1');
190
- });
191
-
192
- it('keyboard: cursor resets when the results are replaced by a search', async () => {
193
- // Pins the semantics the render-phase reset must preserve: a NEW result
194
- // set does not inherit the previous set's cursor. (The reset moved out of
195
- // an effect into the render phase so it can never land AFTER a subsequent
196
- // ArrowDown — the CI flake on the test above.)
197
- const ds = makeDataSource();
198
- render(
199
- <PeoplePicker {...baseProps} dataSource={ds} onSelect={vi.fn()} onSelectRecords={vi.fn()} onOpenChange={vi.fn()} />,
200
- );
201
- await waitFor(() => expect(screen.getByText('Amy Lin')).toBeTruthy());
202
- const search = screen.getByTestId('people-picker-search');
203
- fireEvent.keyDown(search, { key: 'ArrowDown' });
204
- await waitFor(() =>
205
- expect(screen.getAllByTestId('person-row')[0].getAttribute('data-active')).toBe('true'),
206
- );
207
- fireEvent.change(search, { target: { value: 'bob' } });
208
- await waitFor(() => expect(screen.getAllByTestId('person-row')).toHaveLength(1));
209
- expect(screen.getAllByTestId('person-row')[0].getAttribute('data-active')).not.toBe('true');
210
- });
211
-
212
- it('keyboard: Backspace on empty search removes the last chip (multi)', async () => {
213
- const ds = makeDataSource();
214
- render(
215
- <PeoplePicker {...baseProps} multiple dataSource={ds} onSelect={vi.fn()} onSelectRecords={vi.fn()} onOpenChange={vi.fn()} />,
216
- );
217
- await waitFor(() => expect(screen.getByText('Amy Lin')).toBeTruthy());
218
- fireEvent.click(screen.getByText('Amy Lin'));
219
- fireEvent.click(screen.getByText('Bob Wu'));
220
- await waitFor(() => expect(screen.getAllByTestId('selection-chip')).toHaveLength(2));
221
- fireEvent.keyDown(screen.getByTestId('people-picker-search'), { key: 'Backspace' });
222
- await waitFor(() => expect(screen.getAllByTestId('selection-chip')).toHaveLength(1));
223
- });
224
-
225
- it('multi: Clear all empties the tray', async () => {
226
- const ds = makeDataSource();
227
- render(
228
- <PeoplePicker {...baseProps} multiple dataSource={ds} onSelect={vi.fn()} onSelectRecords={vi.fn()} onOpenChange={vi.fn()} />,
229
- );
230
- await waitFor(() => expect(screen.getByText('Amy Lin')).toBeTruthy());
231
- fireEvent.click(screen.getByText('Amy Lin'));
232
- fireEvent.click(screen.getByText('Bob Wu'));
233
- await waitFor(() => expect(screen.getAllByTestId('selection-chip')).toHaveLength(2));
234
- fireEvent.click(screen.getByTestId('selection-clear'));
235
- await waitFor(() => expect(screen.queryAllByTestId('selection-chip')).toHaveLength(0));
236
- });
237
-
238
- it('highlights the matched term in rows while searching', async () => {
239
- const ds = makeDataSource();
240
- // Dialog content portals onto document.body, so query the document, not the
241
- // render container.
242
- render(<PeoplePicker {...baseProps} dataSource={ds} onSelect={vi.fn()} onOpenChange={vi.fn()} />);
243
- await waitFor(() => expect(screen.getByText('Amy Lin')).toBeTruthy());
244
- fireEvent.change(screen.getByTestId('people-picker-search'), { target: { value: 'amy' } });
245
- await waitFor(() => expect(document.querySelector('mark')).toBeTruthy());
246
- expect(document.querySelector('mark')?.textContent?.toLowerCase()).toContain('am');
247
- });
248
-
249
- it('shows an error with a retry that refetches', async () => {
250
- let calls = 0;
251
- const ds = {
252
- find: vi.fn(async () => {
253
- calls += 1;
254
- if (calls === 1) throw new Error('boom');
255
- return { data: [users[0]], total: 1 };
256
- }),
257
- } as any;
258
- render(
259
- <PeoplePicker {...baseProps} dataSource={ds} onSelect={vi.fn()} onOpenChange={vi.fn()} />,
260
- );
261
- await waitFor(() => expect(screen.getByText('boom')).toBeTruthy());
262
- fireEvent.click(screen.getByText('Retry'));
263
- await waitFor(() => expect(screen.getByText('Amy Lin')).toBeTruthy());
264
- });
265
-
266
- it('renders a Dialog on desktop and a bottom Sheet on mobile', async () => {
267
- const ds = makeDataSource();
268
- const { unmount } = render(
269
- <PeoplePicker {...baseProps} dataSource={ds} onSelect={vi.fn()} onOpenChange={vi.fn()} />,
270
- );
271
- await waitFor(() => expect(screen.getByTestId('people-picker-dialog')).toBeTruthy());
272
- expect(screen.queryByTestId('people-picker-sheet')).toBeNull();
273
- unmount();
274
-
275
- Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 375 });
276
- render(<PeoplePicker {...baseProps} dataSource={ds} onSelect={vi.fn()} onOpenChange={vi.fn()} />);
277
- await waitFor(() => expect(screen.getByTestId('people-picker-sheet')).toBeTruthy());
278
- expect(screen.queryByTestId('people-picker-dialog')).toBeNull();
279
- });
280
-
281
- it('seeds the tray from an existing multi value (edit mode)', async () => {
282
- const ds = makeDataSource();
283
- render(
284
- <PeoplePicker
285
- {...baseProps}
286
- multiple
287
- dataSource={ds}
288
- value={['u1']}
289
- onSelect={vi.fn()}
290
- onSelectRecords={vi.fn()}
291
- onOpenChange={vi.fn()}
292
- />,
293
- );
294
- await waitFor(() => expect(screen.getAllByTestId('selection-chip')).toHaveLength(1));
295
- expect(screen.getByTestId('selection-tray').textContent).toContain('Amy Lin');
296
- });
297
-
298
- it('inline: renders an anchored popover, not a modal dialog', async () => {
299
- const ds = makeDataSource();
300
- render(
301
- <PeoplePicker
302
- {...baseProps}
303
- inline
304
- trigger={<button data-testid="inline-trigger">open</button>}
305
- dataSource={ds}
306
- onOpenChange={vi.fn()}
307
- onSelect={vi.fn()}
308
- />,
309
- );
310
- await waitFor(() => expect(screen.getByTestId('people-picker-inline')).toBeTruthy());
311
- expect(screen.queryByTestId('people-picker-dialog')).toBeNull();
312
- expect(screen.getByTestId('inline-trigger')).toBeTruthy();
313
- });
314
-
315
- it('inline multi: toggling a row commits live, with no confirm button', async () => {
316
- const ds = makeDataSource();
317
- const onSelect = vi.fn();
318
- render(
319
- <PeoplePicker
320
- {...baseProps}
321
- inline
322
- multiple
323
- value={[]}
324
- dataSource={ds}
325
- trigger={<button data-testid="inline-trigger" />}
326
- onOpenChange={vi.fn()}
327
- onSelect={onSelect}
328
- onSelectRecords={vi.fn()}
329
- />,
330
- );
331
- await waitFor(() => expect(screen.getByText('Amy Lin')).toBeTruthy());
332
- // Inline has no staging tray / confirm — selection lives in the field chips.
333
- expect(screen.queryByText('Confirm')).toBeNull();
334
- fireEvent.click(screen.getByText('Amy Lin'));
335
- expect(onSelect).toHaveBeenLastCalledWith(['u1']);
336
- fireEvent.click(screen.getByText('Bob Wu'));
337
- expect(onSelect).toHaveBeenLastCalledWith(['u1', 'u2']);
338
- });
339
-
340
- it('inline single: clicking a row commits and closes', async () => {
341
- const ds = makeDataSource();
342
- const onSelect = vi.fn();
343
- const onOpenChange = vi.fn();
344
- render(
345
- <PeoplePicker
346
- {...baseProps}
347
- inline
348
- dataSource={ds}
349
- trigger={<button data-testid="inline-trigger" />}
350
- onOpenChange={onOpenChange}
351
- onSelect={onSelect}
352
- onSelectRecords={vi.fn()}
353
- />,
354
- );
355
- await waitFor(() => expect(screen.getByText('Amy Lin')).toBeTruthy());
356
- fireEvent.click(screen.getByText('Amy Lin'));
357
- expect(onSelect).toHaveBeenCalledWith('u1');
358
- expect(onOpenChange).toHaveBeenCalledWith(false);
359
- });
360
- });