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