@object-ui/fields 17.3.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 (265) hide show
  1. package/CHANGELOG.md +1655 -0
  2. package/README.md +1 -12
  3. package/dist/FieldEditWidget.d.ts +23 -3
  4. package/dist/currency.d.ts +46 -0
  5. package/dist/field-type-alias.d.ts +35 -1
  6. package/dist/index.cjs +4 -4
  7. package/dist/index.css +651 -0
  8. package/dist/index.d.ts +474 -85
  9. package/dist/index.js +5418 -5023
  10. package/dist/widgets/AddressField.d.ts +4 -12
  11. package/dist/widgets/AutoNumberField.d.ts +1 -1
  12. package/dist/widgets/AvatarField.d.ts +1 -1
  13. package/dist/widgets/BooleanField.d.ts +1 -1
  14. package/dist/widgets/CapabilityMultiSelectField.d.ts +1 -1
  15. package/dist/widgets/CheckboxesField.d.ts +1 -1
  16. package/dist/widgets/CodeField.d.ts +1 -1
  17. package/dist/widgets/ColorField.d.ts +1 -1
  18. package/dist/widgets/CurrencyField.d.ts +1 -1
  19. package/dist/widgets/DateField.d.ts +1 -1
  20. package/dist/widgets/DateTimeField.d.ts +1 -1
  21. package/dist/widgets/EmailField.d.ts +1 -1
  22. package/dist/widgets/FileField.d.ts +18 -2
  23. package/dist/widgets/FilterConditionField.d.ts +28 -1
  24. package/dist/widgets/FormulaField.d.ts +1 -1
  25. package/dist/widgets/FullscreenFieldEditor.d.ts +49 -152
  26. package/dist/widgets/GeolocationField.d.ts +1 -1
  27. package/dist/widgets/GridField.d.ts +68 -6
  28. package/dist/widgets/ImageField.d.ts +1 -1
  29. package/dist/widgets/LocationField.d.ts +1 -1
  30. package/dist/widgets/LookupField.d.ts +1 -1
  31. package/dist/widgets/MasterDetailField.d.ts +1 -1
  32. package/dist/widgets/MultiSelectField.d.ts +1 -1
  33. package/dist/widgets/NumberField.d.ts +1 -1
  34. package/dist/widgets/ObjectField.d.ts +1 -1
  35. package/dist/widgets/ObjectRefField.d.ts +1 -1
  36. package/dist/widgets/OptionsEmptyState.d.ts +21 -1
  37. package/dist/widgets/PasswordField.d.ts +1 -1
  38. package/dist/widgets/PercentField.d.ts +1 -1
  39. package/dist/widgets/PhoneField.d.ts +1 -1
  40. package/dist/widgets/QRCodeField.d.ts +1 -1
  41. package/dist/widgets/RadioField.d.ts +1 -1
  42. package/dist/widgets/RatingField.d.ts +1 -1
  43. package/dist/widgets/RecipientPickerField.d.ts +1 -1
  44. package/dist/widgets/RecordPickerDialog.d.ts +34 -10
  45. package/dist/widgets/RichTextField.d.ts +37 -1
  46. package/dist/widgets/SelectField.d.ts +26 -6
  47. package/dist/widgets/SignatureField.d.ts +32 -2
  48. package/dist/widgets/SliderField.d.ts +37 -2
  49. package/dist/widgets/SummaryField.d.ts +1 -1
  50. package/dist/widgets/TagsField.d.ts +1 -1
  51. package/dist/widgets/TextAreaField.d.ts +6 -2
  52. package/dist/widgets/TextField.d.ts +1 -1
  53. package/dist/widgets/TimeField.d.ts +1 -1
  54. package/dist/widgets/UrlField.d.ts +1 -1
  55. package/dist/widgets/UserField.d.ts +1 -1
  56. package/dist/widgets/VectorField.d.ts +1 -1
  57. package/dist/widgets/_cell-renderer-bridge.d.ts +1 -1
  58. package/dist/widgets/address-format.d.ts +88 -0
  59. package/dist/widgets/file-size-guard.d.ts +41 -0
  60. package/dist/widgets/lookupColumnDisplay.d.ts +93 -0
  61. package/dist/widgets/nativeDateValue.d.ts +37 -0
  62. package/dist/widgets/richTextDisplay.d.ts +80 -0
  63. package/dist/widgets/toDomProps.d.ts +30 -10
  64. package/dist/widgets/toHostGroupProps.d.ts +189 -0
  65. package/dist/widgets/types.d.ts +8 -3
  66. package/dist/widgets/useRecordQuery.d.ts +7 -1
  67. package/package.json +17 -14
  68. package/dist/FieldEditWidget.test.d.ts +0 -8
  69. package/dist/__tests__/DateCellRenderer.test.d.ts +0 -0
  70. package/dist/__tests__/NumberCellRenderer.test.d.ts +0 -0
  71. package/dist/__tests__/cell-truncation.test.d.ts +0 -0
  72. package/dist/__tests__/field-carrier-sdui.test.d.ts +0 -0
  73. package/dist/__tests__/lookupCellDisplayField.test.d.ts +0 -0
  74. package/dist/__tests__/lookupCellDisplayName.test.d.ts +0 -0
  75. package/dist/__tests__/lookupCellReferenceKey.test.d.ts +0 -0
  76. package/dist/__tests__/registry-collision.test.d.ts +0 -0
  77. package/dist/__tests__/resolveFieldCurrency.test.d.ts +0 -1
  78. package/dist/__tests__/spec-symbol-batch7.test.d.ts +0 -8
  79. package/dist/__tests__/validation-feedback.test.d.ts +0 -0
  80. package/dist/__tests__/widget-aria-invalid-e2e.test.d.ts +0 -0
  81. package/dist/__tests__/widget-aria-invalid-registry-e2e.test.d.ts +0 -0
  82. package/dist/__tests__/widget-dom-leak-e2e.test.d.ts +0 -0
  83. package/dist/__tests__/widget-props-contract.test.d.ts +0 -8
  84. package/dist/coerce-safe-value.test.d.ts +0 -1
  85. package/dist/complex-widgets.test.d.ts +0 -1
  86. package/dist/datetime-widgets.test.d.ts +0 -1
  87. package/dist/field-type-coverage.test.d.ts +0 -1
  88. package/dist/standard-widgets.test.d.ts +0 -0
  89. package/dist/widgets/AddressField.uniqueIds.test.d.ts +0 -0
  90. package/dist/widgets/CapabilityMultiSelectField.test.d.ts +0 -0
  91. package/dist/widgets/CharacterCount.d.ts +0 -56
  92. package/dist/widgets/CheckboxesField.cascade.test.d.ts +0 -0
  93. package/dist/widgets/GeolocationField.uniqueIds.test.d.ts +0 -0
  94. package/dist/widgets/GridField.test.d.ts +0 -1
  95. package/dist/widgets/ImageLightbox.test.d.ts +0 -1
  96. package/dist/widgets/LookupField.dependsOn.test.d.ts +0 -8
  97. package/dist/widgets/LookupField.gateHintLabel.test.d.ts +0 -8
  98. package/dist/widgets/LookupField.hydration.test.d.ts +0 -1
  99. package/dist/widgets/LookupField.idField.test.d.ts +0 -1
  100. package/dist/widgets/MultiSelectField.cascade.test.d.ts +0 -0
  101. package/dist/widgets/OptionsEmptyState.no-provider.test.d.ts +0 -0
  102. package/dist/widgets/OptionsEmptyState.test.d.ts +0 -0
  103. package/dist/widgets/PeoplePicker.test.d.ts +0 -8
  104. package/dist/widgets/PeoplePickerComponents.test.d.ts +0 -8
  105. package/dist/widgets/RadioField.cascade.test.d.ts +0 -0
  106. package/dist/widgets/RecipientPickerField.test.d.ts +0 -0
  107. package/dist/widgets/RecordPickerDialog.filterOptions.test.d.ts +0 -8
  108. package/dist/widgets/RecordPickerDialog.selectLabel.test.d.ts +0 -8
  109. package/dist/widgets/SelectField.cascade.test.d.ts +0 -0
  110. package/dist/widgets/TagsField.placeholder.no-provider.test.d.ts +0 -0
  111. package/dist/widgets/TagsField.placeholder.test.d.ts +0 -0
  112. package/dist/widgets/UserFieldPicker.test.d.ts +0 -8
  113. package/dist/widgets/__tests__/FilterConditionField.matchAll.test.d.ts +0 -8
  114. package/dist/widgets/__tests__/FilterConditionField.operators.test.d.ts +0 -8
  115. package/dist/widgets/__tests__/FullscreenFieldEditor.i18n.test.d.ts +0 -0
  116. package/dist/widgets/__tests__/FullscreenFieldEditor.no-provider.test.d.ts +0 -0
  117. package/dist/widgets/__tests__/RichTextField.fullscreenDisabled.test.d.ts +0 -0
  118. package/dist/widgets/__tests__/RichTextField.mobileFullscreen.test.d.ts +0 -0
  119. package/dist/widgets/__tests__/TextAreaField.characterCount.announcements.test.d.ts +0 -0
  120. package/dist/widgets/__tests__/TextAreaField.characterCount.i18n.test.d.ts +0 -0
  121. package/dist/widgets/__tests__/TextAreaField.characterCount.no-provider.test.d.ts +0 -0
  122. package/dist/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.d.ts +0 -0
  123. package/dist/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.d.ts +0 -0
  124. package/dist/widgets/__tests__/TextAreaField.fullscreenDisabled.test.d.ts +0 -0
  125. package/dist/widgets/__tests__/TextAreaField.mobileFullscreen.test.d.ts +0 -0
  126. package/dist/widgets/deriveLookupColumns.test.d.ts +0 -1
  127. package/dist/widgets/file-value.test.d.ts +0 -1
  128. package/dist/widgets/personDisplay.test.d.ts +0 -8
  129. package/dist/widgets/recentLookups.test.d.ts +0 -1
  130. package/dist/widgets/useRecordQuery.test.d.ts +0 -8
  131. package/dist/widgets/useUploadingSignal.test.d.ts +0 -1
  132. package/src/FieldEditWidget.test.ts +0 -133
  133. package/src/FieldEditWidget.tsx +0 -191
  134. package/src/__tests__/DateCellRenderer.test.tsx +0 -73
  135. package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
  136. package/src/__tests__/cell-truncation.test.tsx +0 -96
  137. package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
  138. package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
  139. package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
  140. package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
  141. package/src/__tests__/registry-collision.test.ts +0 -91
  142. package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
  143. package/src/__tests__/spec-symbol-batch7.test.ts +0 -126
  144. package/src/__tests__/validation-feedback.test.tsx +0 -361
  145. package/src/__tests__/widget-aria-invalid-e2e.test.tsx +0 -265
  146. package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +0 -338
  147. package/src/__tests__/widget-dom-leak-e2e.test.tsx +0 -598
  148. package/src/__tests__/widget-props-contract.test.tsx +0 -142
  149. package/src/coerce-safe-value.test.ts +0 -34
  150. package/src/complex-widgets.test.tsx +0 -908
  151. package/src/currency.ts +0 -14
  152. package/src/datetime-widgets.test.tsx +0 -93
  153. package/src/field-type-alias.ts +0 -100
  154. package/src/field-type-coverage.test.ts +0 -67
  155. package/src/index.tsx +0 -2503
  156. package/src/standard-widgets.test.tsx +0 -357
  157. package/src/widgets/AddressField.tsx +0 -132
  158. package/src/widgets/AddressField.uniqueIds.test.tsx +0 -92
  159. package/src/widgets/AutoNumberField.tsx +0 -18
  160. package/src/widgets/AvatarField.tsx +0 -117
  161. package/src/widgets/BooleanField.tsx +0 -52
  162. package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
  163. package/src/widgets/CapabilityMultiSelectField.tsx +0 -229
  164. package/src/widgets/CharacterCount.tsx +0 -263
  165. package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
  166. package/src/widgets/CheckboxesField.tsx +0 -136
  167. package/src/widgets/CodeField.tsx +0 -41
  168. package/src/widgets/ColorField.tsx +0 -53
  169. package/src/widgets/CurrencyField.tsx +0 -95
  170. package/src/widgets/DateField.tsx +0 -31
  171. package/src/widgets/DateTimeField.tsx +0 -37
  172. package/src/widgets/EmailField.tsx +0 -37
  173. package/src/widgets/FileField.tsx +0 -494
  174. package/src/widgets/FilterConditionField.tsx +0 -454
  175. package/src/widgets/FormulaField.tsx +0 -33
  176. package/src/widgets/FullscreenFieldEditor.tsx +0 -271
  177. package/src/widgets/GeolocationField.tsx +0 -167
  178. package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
  179. package/src/widgets/GridField.test.tsx +0 -406
  180. package/src/widgets/GridField.tsx +0 -966
  181. package/src/widgets/ImageCropperDialog.tsx +0 -245
  182. package/src/widgets/ImageField.tsx +0 -255
  183. package/src/widgets/ImageLightbox.test.tsx +0 -52
  184. package/src/widgets/ImageLightbox.tsx +0 -108
  185. package/src/widgets/LocationField.tsx +0 -57
  186. package/src/widgets/LookupField.dependsOn.test.tsx +0 -233
  187. package/src/widgets/LookupField.gateHintLabel.test.tsx +0 -135
  188. package/src/widgets/LookupField.hydration.test.tsx +0 -136
  189. package/src/widgets/LookupField.idField.test.tsx +0 -61
  190. package/src/widgets/LookupField.tsx +0 -1291
  191. package/src/widgets/MarkdownContent.tsx +0 -19
  192. package/src/widgets/MasterDetailField.tsx +0 -131
  193. package/src/widgets/MultiSelectField.cascade.test.tsx +0 -143
  194. package/src/widgets/MultiSelectField.tsx +0 -142
  195. package/src/widgets/NumberField.tsx +0 -49
  196. package/src/widgets/ObjectField.tsx +0 -94
  197. package/src/widgets/ObjectRefField.tsx +0 -121
  198. package/src/widgets/OptionsEmptyState.no-provider.test.tsx +0 -88
  199. package/src/widgets/OptionsEmptyState.test.tsx +0 -169
  200. package/src/widgets/OptionsEmptyState.tsx +0 -83
  201. package/src/widgets/PasswordField.tsx +0 -49
  202. package/src/widgets/PeoplePicker.test.tsx +0 -360
  203. package/src/widgets/PeoplePicker.tsx +0 -579
  204. package/src/widgets/PeoplePickerComponents.test.tsx +0 -104
  205. package/src/widgets/PercentField.tsx +0 -95
  206. package/src/widgets/PersonRow.tsx +0 -117
  207. package/src/widgets/PhoneField.tsx +0 -37
  208. package/src/widgets/QRCodeField.tsx +0 -103
  209. package/src/widgets/RadioField.cascade.test.tsx +0 -141
  210. package/src/widgets/RadioField.tsx +0 -111
  211. package/src/widgets/RatingField.tsx +0 -81
  212. package/src/widgets/RecipientPickerField.test.tsx +0 -157
  213. package/src/widgets/RecipientPickerField.tsx +0 -188
  214. package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
  215. package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
  216. package/src/widgets/RecordPickerDialog.tsx +0 -1316
  217. package/src/widgets/RichTextField.tsx +0 -176
  218. package/src/widgets/SelectField.cascade.test.tsx +0 -138
  219. package/src/widgets/SelectField.tsx +0 -162
  220. package/src/widgets/SelectionTray.tsx +0 -110
  221. package/src/widgets/SignatureField.tsx +0 -140
  222. package/src/widgets/SliderField.tsx +0 -39
  223. package/src/widgets/SummaryField.tsx +0 -31
  224. package/src/widgets/TagsField.placeholder.no-provider.test.tsx +0 -68
  225. package/src/widgets/TagsField.placeholder.test.tsx +0 -95
  226. package/src/widgets/TagsField.tsx +0 -88
  227. package/src/widgets/TextAreaField.tsx +0 -213
  228. package/src/widgets/TextField.tsx +0 -45
  229. package/src/widgets/TimeField.tsx +0 -31
  230. package/src/widgets/UrlField.tsx +0 -46
  231. package/src/widgets/UserField.tsx +0 -58
  232. package/src/widgets/UserFieldPicker.test.tsx +0 -78
  233. package/src/widgets/VectorField.tsx +0 -29
  234. package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +0 -66
  235. package/src/widgets/__tests__/FilterConditionField.operators.test.ts +0 -142
  236. package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +0 -248
  237. package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +0 -116
  238. package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +0 -244
  239. package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +0 -195
  240. package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +0 -417
  241. package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +0 -287
  242. package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +0 -160
  243. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +0 -161
  244. package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +0 -346
  245. package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +0 -246
  246. package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +0 -175
  247. package/src/widgets/_cell-renderer-bridge.ts +0 -19
  248. package/src/widgets/deriveLookupColumns.test.ts +0 -112
  249. package/src/widgets/deriveLookupColumns.ts +0 -108
  250. package/src/widgets/file-value.test.ts +0 -230
  251. package/src/widgets/file-value.ts +0 -246
  252. package/src/widgets/openNativePicker.ts +0 -18
  253. package/src/widgets/personDisplay.test.ts +0 -113
  254. package/src/widgets/personDisplay.ts +0 -92
  255. package/src/widgets/recentLookups.test.ts +0 -45
  256. package/src/widgets/recentLookups.ts +0 -47
  257. package/src/widgets/toDomProps.ts +0 -175
  258. package/src/widgets/types.ts +0 -242
  259. package/src/widgets/useCascadingOptions.ts +0 -46
  260. package/src/widgets/useFieldTranslation.ts +0 -150
  261. package/src/widgets/useRecordQuery.test.ts +0 -174
  262. package/src/widgets/useRecordQuery.ts +0 -264
  263. package/src/widgets/useUploadingSignal.test.tsx +0 -62
  264. package/src/widgets/useUploadingSignal.ts +0 -20
  265. 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
- });