@transferwise/components 46.97.5 → 46.98.1

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 (293) hide show
  1. package/build/alert/Alert.js +8 -0
  2. package/build/alert/Alert.js.map +1 -1
  3. package/build/alert/Alert.mjs +8 -0
  4. package/build/alert/Alert.mjs.map +1 -1
  5. package/build/common/closeButton/CloseButton.js +3 -1
  6. package/build/common/closeButton/CloseButton.js.map +1 -1
  7. package/build/common/closeButton/CloseButton.mjs +3 -1
  8. package/build/common/closeButton/CloseButton.mjs.map +1 -1
  9. package/build/dateInput/DateInput.js +4 -4
  10. package/build/dateInput/DateInput.js.map +1 -1
  11. package/build/dateInput/DateInput.mjs +4 -4
  12. package/build/dateInput/DateInput.mjs.map +1 -1
  13. package/build/dateLookup/DateLookup.js +4 -4
  14. package/build/dateLookup/DateLookup.js.map +1 -1
  15. package/build/dateLookup/DateLookup.mjs +4 -4
  16. package/build/dateLookup/DateLookup.mjs.map +1 -1
  17. package/build/dateLookup/dayCalendar/table/DayCalendarTable.js +3 -3
  18. package/build/dateLookup/dayCalendar/table/DayCalendarTable.js.map +1 -1
  19. package/build/dateLookup/dayCalendar/table/DayCalendarTable.mjs +3 -3
  20. package/build/dateLookup/dayCalendar/table/DayCalendarTable.mjs.map +1 -1
  21. package/build/i18n/cs.json +3 -2
  22. package/build/i18n/cs.json.js +3 -2
  23. package/build/i18n/cs.json.js.map +1 -1
  24. package/build/i18n/cs.json.mjs +3 -2
  25. package/build/i18n/cs.json.mjs.map +1 -1
  26. package/build/i18n/de.json +3 -2
  27. package/build/i18n/de.json.js +3 -2
  28. package/build/i18n/de.json.js.map +1 -1
  29. package/build/i18n/de.json.mjs +3 -2
  30. package/build/i18n/de.json.mjs.map +1 -1
  31. package/build/i18n/en.json +3 -2
  32. package/build/i18n/en.json.js +3 -2
  33. package/build/i18n/en.json.js.map +1 -1
  34. package/build/i18n/en.json.mjs +3 -2
  35. package/build/i18n/en.json.mjs.map +1 -1
  36. package/build/i18n/es.json +3 -2
  37. package/build/i18n/es.json.js +3 -2
  38. package/build/i18n/es.json.js.map +1 -1
  39. package/build/i18n/es.json.mjs +3 -2
  40. package/build/i18n/es.json.mjs.map +1 -1
  41. package/build/i18n/fr.json +3 -2
  42. package/build/i18n/fr.json.js +3 -2
  43. package/build/i18n/fr.json.js.map +1 -1
  44. package/build/i18n/fr.json.mjs +3 -2
  45. package/build/i18n/fr.json.mjs.map +1 -1
  46. package/build/i18n/hu.json +3 -2
  47. package/build/i18n/hu.json.js +3 -2
  48. package/build/i18n/hu.json.js.map +1 -1
  49. package/build/i18n/hu.json.mjs +3 -2
  50. package/build/i18n/hu.json.mjs.map +1 -1
  51. package/build/i18n/id.json +3 -2
  52. package/build/i18n/id.json.js +3 -2
  53. package/build/i18n/id.json.js.map +1 -1
  54. package/build/i18n/id.json.mjs +3 -2
  55. package/build/i18n/id.json.mjs.map +1 -1
  56. package/build/i18n/it.json +3 -2
  57. package/build/i18n/it.json.js +3 -2
  58. package/build/i18n/it.json.js.map +1 -1
  59. package/build/i18n/it.json.mjs +3 -2
  60. package/build/i18n/it.json.mjs.map +1 -1
  61. package/build/i18n/ja.json +3 -2
  62. package/build/i18n/ja.json.js +3 -2
  63. package/build/i18n/ja.json.js.map +1 -1
  64. package/build/i18n/ja.json.mjs +3 -2
  65. package/build/i18n/ja.json.mjs.map +1 -1
  66. package/build/i18n/nl.json +6 -5
  67. package/build/i18n/pl.json +3 -2
  68. package/build/i18n/pl.json.js +3 -2
  69. package/build/i18n/pl.json.js.map +1 -1
  70. package/build/i18n/pl.json.mjs +3 -2
  71. package/build/i18n/pl.json.mjs.map +1 -1
  72. package/build/i18n/pt.json +3 -2
  73. package/build/i18n/pt.json.js +3 -2
  74. package/build/i18n/pt.json.js.map +1 -1
  75. package/build/i18n/pt.json.mjs +3 -2
  76. package/build/i18n/pt.json.mjs.map +1 -1
  77. package/build/i18n/ro.json +3 -2
  78. package/build/i18n/ro.json.js +3 -2
  79. package/build/i18n/ro.json.js.map +1 -1
  80. package/build/i18n/ro.json.mjs +3 -2
  81. package/build/i18n/ro.json.mjs.map +1 -1
  82. package/build/i18n/ru.json +3 -2
  83. package/build/i18n/ru.json.js +3 -2
  84. package/build/i18n/ru.json.js.map +1 -1
  85. package/build/i18n/ru.json.mjs +3 -2
  86. package/build/i18n/ru.json.mjs.map +1 -1
  87. package/build/i18n/th.json +3 -2
  88. package/build/i18n/th.json.js +3 -2
  89. package/build/i18n/th.json.js.map +1 -1
  90. package/build/i18n/th.json.mjs +3 -2
  91. package/build/i18n/th.json.mjs.map +1 -1
  92. package/build/i18n/tr.json +3 -2
  93. package/build/i18n/tr.json.js +3 -2
  94. package/build/i18n/tr.json.js.map +1 -1
  95. package/build/i18n/tr.json.mjs +3 -2
  96. package/build/i18n/tr.json.mjs.map +1 -1
  97. package/build/i18n/zh-CN.json +3 -2
  98. package/build/i18n/zh-CN.json.js +3 -2
  99. package/build/i18n/zh-CN.json.js.map +1 -1
  100. package/build/i18n/zh-CN.json.mjs +3 -2
  101. package/build/i18n/zh-CN.json.mjs.map +1 -1
  102. package/build/i18n/zh-HK.json +3 -2
  103. package/build/i18n/zh-HK.json.js +3 -2
  104. package/build/i18n/zh-HK.json.js.map +1 -1
  105. package/build/i18n/zh-HK.json.mjs +3 -2
  106. package/build/i18n/zh-HK.json.mjs.map +1 -1
  107. package/build/image/Image.js +9 -10
  108. package/build/image/Image.js.map +1 -1
  109. package/build/image/Image.mjs +11 -11
  110. package/build/image/Image.mjs.map +1 -1
  111. package/build/index.js +0 -2
  112. package/build/index.js.map +1 -1
  113. package/build/index.mjs +0 -1
  114. package/build/index.mjs.map +1 -1
  115. package/build/main.css +16 -45
  116. package/build/moneyInput/MoneyInput.js +4 -8
  117. package/build/moneyInput/MoneyInput.js.map +1 -1
  118. package/build/moneyInput/MoneyInput.messages.js +3 -0
  119. package/build/moneyInput/MoneyInput.messages.js.map +1 -1
  120. package/build/moneyInput/MoneyInput.messages.mjs +3 -0
  121. package/build/moneyInput/MoneyInput.messages.mjs.map +1 -1
  122. package/build/moneyInput/MoneyInput.mjs +4 -8
  123. package/build/moneyInput/MoneyInput.mjs.map +1 -1
  124. package/build/phoneNumberInput/PhoneNumberInput.js +36 -2
  125. package/build/phoneNumberInput/PhoneNumberInput.js.map +1 -1
  126. package/build/phoneNumberInput/PhoneNumberInput.messages.js +6 -0
  127. package/build/phoneNumberInput/PhoneNumberInput.messages.js.map +1 -1
  128. package/build/phoneNumberInput/PhoneNumberInput.messages.mjs +6 -0
  129. package/build/phoneNumberInput/PhoneNumberInput.messages.mjs.map +1 -1
  130. package/build/phoneNumberInput/PhoneNumberInput.mjs +36 -2
  131. package/build/phoneNumberInput/PhoneNumberInput.mjs.map +1 -1
  132. package/build/snackbar/Snackbar.js +1 -1
  133. package/build/snackbar/Snackbar.js.map +1 -1
  134. package/build/snackbar/Snackbar.mjs +1 -1
  135. package/build/snackbar/Snackbar.mjs.map +1 -1
  136. package/build/styles/circularButton/CircularButton.css +1 -0
  137. package/build/styles/dateInput/DateInput.css +13 -0
  138. package/build/styles/main.css +16 -45
  139. package/build/styles/uploadInput/uploadItem/UploadItem.css +2 -1
  140. package/build/tabs/Tabs.js +3 -3
  141. package/build/tabs/Tabs.js.map +1 -1
  142. package/build/tabs/Tabs.mjs +3 -3
  143. package/build/tabs/Tabs.mjs.map +1 -1
  144. package/build/test-utils/assets/apple-pay-logo.svg +84 -0
  145. package/build/typeahead/Typeahead.js +2 -2
  146. package/build/typeahead/Typeahead.js.map +1 -1
  147. package/build/typeahead/Typeahead.mjs +2 -2
  148. package/build/typeahead/Typeahead.mjs.map +1 -1
  149. package/build/typeahead/typeaheadInput/TypeaheadInput.js +2 -2
  150. package/build/typeahead/typeaheadInput/TypeaheadInput.js.map +1 -1
  151. package/build/typeahead/typeaheadInput/TypeaheadInput.mjs +2 -2
  152. package/build/typeahead/typeaheadInput/TypeaheadInput.mjs.map +1 -1
  153. package/build/types/alert/Alert.d.ts.map +1 -1
  154. package/build/types/common/closeButton/CloseButton.d.ts +2 -0
  155. package/build/types/common/closeButton/CloseButton.d.ts.map +1 -1
  156. package/build/types/image/Image.d.ts +0 -1
  157. package/build/types/image/Image.d.ts.map +1 -1
  158. package/build/types/index.d.ts +0 -2
  159. package/build/types/index.d.ts.map +1 -1
  160. package/build/types/moneyInput/MoneyInput.d.ts.map +1 -1
  161. package/build/types/moneyInput/MoneyInput.messages.d.ts +5 -0
  162. package/build/types/moneyInput/MoneyInput.messages.d.ts.map +1 -1
  163. package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
  164. package/build/types/phoneNumberInput/PhoneNumberInput.messages.d.ts +8 -0
  165. package/build/types/phoneNumberInput/PhoneNumberInput.messages.d.ts.map +1 -1
  166. package/build/types/test-utils/fake-data.d.ts +2 -0
  167. package/build/types/test-utils/fake-data.d.ts.map +1 -1
  168. package/build/types/test-utils/index.d.ts +6 -4
  169. package/build/types/test-utils/index.d.ts.map +1 -1
  170. package/build/types/upload/Upload.d.ts +1 -2
  171. package/build/types/upload/Upload.d.ts.map +1 -1
  172. package/build/types/upload/steps/processingStep/processingStep.d.ts +1 -3
  173. package/build/types/upload/steps/processingStep/processingStep.d.ts.map +1 -1
  174. package/build/types/uploadInput/UploadInput.d.ts.map +1 -1
  175. package/build/types/uploadInput/uploadItem/UploadItem.d.ts +1 -1
  176. package/build/types/uploadInput/uploadItem/UploadItem.d.ts.map +1 -1
  177. package/build/types/withDisplayFormat/WithDisplayFormat.d.ts.map +1 -1
  178. package/build/upload/Upload.js +29 -45
  179. package/build/upload/Upload.js.map +1 -1
  180. package/build/upload/Upload.mjs +29 -45
  181. package/build/upload/Upload.mjs.map +1 -1
  182. package/build/upload/steps/processingStep/processingStep.js +1 -3
  183. package/build/upload/steps/processingStep/processingStep.js.map +1 -1
  184. package/build/upload/steps/processingStep/processingStep.mjs +1 -3
  185. package/build/upload/steps/processingStep/processingStep.mjs.map +1 -1
  186. package/build/upload/steps/uploadImageStep/uploadImageStep.js +1 -1
  187. package/build/upload/steps/uploadImageStep/uploadImageStep.js.map +1 -1
  188. package/build/upload/steps/uploadImageStep/uploadImageStep.mjs +1 -1
  189. package/build/upload/steps/uploadImageStep/uploadImageStep.mjs.map +1 -1
  190. package/build/uploadInput/UploadInput.js +54 -6
  191. package/build/uploadInput/UploadInput.js.map +1 -1
  192. package/build/uploadInput/UploadInput.mjs +54 -6
  193. package/build/uploadInput/UploadInput.mjs.map +1 -1
  194. package/build/uploadInput/uploadItem/UploadItem.js +12 -6
  195. package/build/uploadInput/uploadItem/UploadItem.js.map +1 -1
  196. package/build/uploadInput/uploadItem/UploadItem.mjs +12 -6
  197. package/build/uploadInput/uploadItem/UploadItem.mjs.map +1 -1
  198. package/build/withDisplayFormat/WithDisplayFormat.js +3 -2
  199. package/build/withDisplayFormat/WithDisplayFormat.js.map +1 -1
  200. package/build/withDisplayFormat/WithDisplayFormat.mjs +3 -2
  201. package/build/withDisplayFormat/WithDisplayFormat.mjs.map +1 -1
  202. package/package.json +14 -17
  203. package/src/alert/Alert.spec.tsx +11 -0
  204. package/src/alert/Alert.story.tsx +23 -9
  205. package/src/alert/Alert.tsx +14 -1
  206. package/src/circularButton/CircularButton.css +1 -0
  207. package/src/circularButton/CircularButton.less +1 -0
  208. package/src/circularButton/CircularButton.tests.story.tsx +23 -0
  209. package/src/common/closeButton/CloseButton.spec.tsx +13 -1
  210. package/src/common/closeButton/CloseButton.tsx +3 -0
  211. package/src/dateInput/DateInput.css +13 -0
  212. package/src/dateInput/DateInput.less +20 -0
  213. package/src/dateInput/DateInput.tests.story.tsx +14 -3
  214. package/src/dateInput/DateInput.tsx +4 -4
  215. package/src/i18n/cs.json +3 -2
  216. package/src/i18n/de.json +3 -2
  217. package/src/i18n/en.json +3 -2
  218. package/src/i18n/es.json +3 -2
  219. package/src/i18n/fr.json +3 -2
  220. package/src/i18n/hu.json +3 -2
  221. package/src/i18n/id.json +3 -2
  222. package/src/i18n/it.json +3 -2
  223. package/src/i18n/ja.json +3 -2
  224. package/src/i18n/nl.json +6 -5
  225. package/src/i18n/pl.json +3 -2
  226. package/src/i18n/pt.json +3 -2
  227. package/src/i18n/ro.json +3 -2
  228. package/src/i18n/ru.json +3 -2
  229. package/src/i18n/th.json +3 -2
  230. package/src/i18n/tr.json +3 -2
  231. package/src/i18n/zh-CN.json +3 -2
  232. package/src/i18n/zh-HK.json +3 -2
  233. package/src/image/Image.spec.tsx +3 -3
  234. package/src/image/Image.tsx +10 -12
  235. package/src/index.ts +0 -2
  236. package/src/legacylistItem/LegacyListItem.story.tsx +5 -5
  237. package/src/legacylistItem/LegacyListItem.tests.story.tsx +6 -6
  238. package/src/main.css +16 -45
  239. package/src/main.less +0 -1
  240. package/src/moneyInput/MoneyInput.messages.ts +5 -0
  241. package/src/moneyInput/MoneyInput.spec.tsx +42 -5
  242. package/src/moneyInput/MoneyInput.story.tsx +11 -2
  243. package/src/moneyInput/MoneyInput.tsx +5 -7
  244. package/src/phoneNumberInput/PhoneNumberInput.messages.ts +8 -0
  245. package/src/phoneNumberInput/PhoneNumberInput.spec.tsx +77 -43
  246. package/src/phoneNumberInput/PhoneNumberInput.tsx +34 -2
  247. package/src/promoCard/__snapshots__/PromoCard.spec.tsx.snap +1 -0
  248. package/src/promoCard/__snapshots__/PromoCardGroup.spec.tsx.snap +2 -0
  249. package/src/ssr.spec.tsx +0 -1
  250. package/src/test-utils/assets/apple-pay-logo.svg +84 -0
  251. package/src/test-utils/fake-data.ts +5 -0
  252. package/src/test-utils/jest.setup.ts +0 -4
  253. package/src/typeahead/Typeahead.spec.tsx +182 -0
  254. package/src/typeahead/typeaheadInput/TypeaheadInput.spec.tsx +103 -0
  255. package/src/typeahead/util/highlight.spec.tsx +43 -0
  256. package/src/upload/Upload.spec.tsx +63 -0
  257. package/src/upload/Upload.story.tsx +0 -51
  258. package/src/upload/Upload.tests.story.tsx +93 -0
  259. package/src/upload/Upload.tsx +28 -49
  260. package/src/upload/steps/processingStep/processingStep.tsx +2 -7
  261. package/src/uploadInput/UploadInput.tsx +74 -10
  262. package/src/uploadInput/uploadItem/UploadItem.css +2 -1
  263. package/src/uploadInput/uploadItem/UploadItem.less +1 -1
  264. package/src/uploadInput/uploadItem/UploadItem.tsx +11 -6
  265. package/src/withDisplayFormat/WithDisplayFormat.spec.js +11 -15
  266. package/src/withDisplayFormat/WithDisplayFormat.tsx +3 -2
  267. package/build/selectOption/SelectOption.js +0 -131
  268. package/build/selectOption/SelectOption.js.map +0 -1
  269. package/build/selectOption/SelectOption.messages.js +0 -17
  270. package/build/selectOption/SelectOption.messages.js.map +0 -1
  271. package/build/selectOption/SelectOption.messages.mjs +0 -13
  272. package/build/selectOption/SelectOption.messages.mjs.map +0 -1
  273. package/build/selectOption/SelectOption.mjs +0 -127
  274. package/build/selectOption/SelectOption.mjs.map +0 -1
  275. package/build/styles/selectOption/SelectOption.css +0 -44
  276. package/build/types/selectOption/SelectOption.d.ts +0 -21
  277. package/build/types/selectOption/SelectOption.d.ts.map +0 -1
  278. package/build/types/selectOption/SelectOption.messages.d.ts +0 -12
  279. package/build/types/selectOption/SelectOption.messages.d.ts.map +0 -1
  280. package/build/types/selectOption/index.d.ts +0 -3
  281. package/build/types/selectOption/index.d.ts.map +0 -1
  282. package/src/selectOption/SelectOption.css +0 -44
  283. package/src/selectOption/SelectOption.less +0 -40
  284. package/src/selectOption/SelectOption.messages.ts +0 -12
  285. package/src/selectOption/SelectOption.spec.tsx +0 -83
  286. package/src/selectOption/SelectOption.story.tsx +0 -277
  287. package/src/selectOption/SelectOption.tsx +0 -151
  288. package/src/selectOption/index.ts +0 -2
  289. package/src/typeahead/Typeahead.rtl.spec.tsx +0 -54
  290. package/src/typeahead/Typeahead.spec.js +0 -404
  291. package/src/typeahead/typeaheadInput/TypeaheadInput.spec.js +0 -74
  292. package/src/typeahead/typeaheadOption/TypeaheadOption.spec.js +0 -75
  293. package/src/typeahead/util/highlight.spec.js +0 -34
@@ -0,0 +1,182 @@
1
+ import { Field } from '../field/Field';
2
+ import { mockMatchMedia, render, screen, fireEvent } from '../test-utils';
3
+ import { wait } from '../test-utils/wait';
4
+ import Typeahead from './Typeahead';
5
+
6
+ mockMatchMedia();
7
+
8
+ describe('Typeahead', () => {
9
+ it('supports `Field` for labeling', () => {
10
+ render(
11
+ <Field id="test" label="Tags">
12
+ <Typeahead id="test" name="test" options={[{ label: 'Test' }]} onChange={() => {}} />
13
+ </Field>,
14
+ );
15
+ expect(screen.getAllByRole('group')[0]).toHaveAccessibleName(/^Tags/);
16
+ });
17
+
18
+ describe('when no options are provided', () => {
19
+ it('does not render a dropdown when no options and no footer are provided', () => {
20
+ render(
21
+ <Field id="test" label="Tags">
22
+ <Typeahead id="test" name="test" options={[]} onChange={() => {}} />
23
+ </Field>,
24
+ );
25
+ expect(screen.queryByRole('menu')).not.toBeInTheDocument();
26
+ });
27
+ it('does render a dropdown when only a footer is provided', () => {
28
+ render(
29
+ <Field id="test" label="Tags">
30
+ <Typeahead id="test" name="test" options={[]} footer={<p>hello</p>} onChange={() => {}} />
31
+ </Field>,
32
+ );
33
+ expect(screen.getByRole('menu')).toBeInTheDocument();
34
+ });
35
+ });
36
+
37
+ it('renders input with placeholder', () => {
38
+ render(
39
+ <Typeahead id="test" name="test" options={[]} placeholder="Type here" onChange={() => {}} />,
40
+ );
41
+ expect(screen.getByPlaceholderText('Type here')).toBeInTheDocument();
42
+ });
43
+
44
+ it('renders chips when multiple is true and selected has items', () => {
45
+ const initialValue = [{ label: 'Chip 1' }, { label: 'Chip 2' }];
46
+ render(
47
+ <Typeahead
48
+ id="test"
49
+ name="test"
50
+ options={[]}
51
+ multiple
52
+ initialValue={initialValue}
53
+ onChange={() => {}}
54
+ />,
55
+ );
56
+ expect(screen.getByText('Chip 1')).toBeInTheDocument();
57
+ expect(screen.getByText('Chip 2')).toBeInTheDocument();
58
+ });
59
+
60
+ it('calls onChange when selecting an option', () => {
61
+ const onChange = jest.fn();
62
+ render(
63
+ <Typeahead
64
+ id="test"
65
+ name="test"
66
+ options={[{ label: 'Option 1' }, { label: 'Option 2' }]}
67
+ minQueryLength={0}
68
+ onChange={onChange}
69
+ />,
70
+ );
71
+ const input = screen.getByRole('combobox');
72
+ fireEvent.change(input, { target: { value: 'Option 1' } });
73
+ fireEvent.click(screen.getByText('Option 1'));
74
+ expect(onChange).toHaveBeenCalled();
75
+ });
76
+
77
+ it('shows clear button when there is a value', () => {
78
+ render(
79
+ <Typeahead
80
+ id="test"
81
+ name="test"
82
+ options={[]}
83
+ initialValue={[{ label: 'Chip' }]}
84
+ onChange={() => {}}
85
+ />,
86
+ );
87
+ expect(screen.getByRole('button', { name: /clear/i })).toBeInTheDocument();
88
+ });
89
+
90
+ it('clears value when clear button is clicked', () => {
91
+ const onChange = jest.fn();
92
+ render(
93
+ <Typeahead
94
+ id="test"
95
+ name="test"
96
+ options={[]}
97
+ initialValue={[{ label: 'Chip' }]}
98
+ onChange={onChange}
99
+ />,
100
+ );
101
+ fireEvent.click(screen.getByRole('button', { name: /clear/i }));
102
+ expect(onChange).toHaveBeenCalledWith([]);
103
+ });
104
+
105
+ it('shows InlineAlert when alert prop is provided', () => {
106
+ render(
107
+ <Typeahead
108
+ id="test"
109
+ name="test"
110
+ options={[]}
111
+ alert={{ message: 'Something went wrong', type: 'error' }}
112
+ onChange={() => {}}
113
+ />,
114
+ );
115
+ expect(screen.getByText('Something went wrong')).toBeInTheDocument();
116
+ });
117
+
118
+ it('calls onFocus when input is focused', () => {
119
+ const onFocus = jest.fn();
120
+ render(<Typeahead id="test" name="test" options={[]} onChange={() => {}} onFocus={onFocus} />);
121
+ fireEvent.focus(screen.getByRole('combobox'));
122
+ expect(onFocus).toHaveBeenCalled();
123
+ });
124
+
125
+ it('calls onInputChange when input value changes', () => {
126
+ const onInputChange = jest.fn();
127
+ render(
128
+ <Typeahead
129
+ id="test"
130
+ name="test"
131
+ options={[]}
132
+ minQueryLength={0}
133
+ onChange={() => {}}
134
+ onInputChange={onInputChange}
135
+ />,
136
+ );
137
+ fireEvent.change(screen.getByRole('combobox'), { target: { value: 'abc' } });
138
+ expect(onInputChange).toHaveBeenCalledWith('abc');
139
+ });
140
+
141
+ it('calls onSearch when input value changes', async () => {
142
+ const onSearch = jest.fn();
143
+ render(
144
+ <Typeahead
145
+ id="test"
146
+ name="test"
147
+ options={[]}
148
+ minQueryLength={0}
149
+ onChange={() => {}}
150
+ onSearch={onSearch}
151
+ />,
152
+ );
153
+ fireEvent.change(screen.getByRole('combobox'), { target: { value: 'abc' } });
154
+ // wait for debounce
155
+ await wait(500);
156
+ expect(onSearch).toHaveBeenCalledWith('abc');
157
+ });
158
+
159
+ it('adds a new chip when allowNew and multiple are true and separator is pasted', () => {
160
+ const onChange = jest.fn();
161
+ render(
162
+ <Typeahead
163
+ id="test"
164
+ name="test"
165
+ options={[]}
166
+ allowNew
167
+ multiple
168
+ chipSeparators={[',']}
169
+ onChange={onChange}
170
+ />,
171
+ );
172
+ const input = screen.getByRole('combobox');
173
+ fireEvent.paste(input, {
174
+ clipboardData: {
175
+ getData: () => 'foo,bar',
176
+ },
177
+ });
178
+ expect(onChange).toHaveBeenCalledWith(
179
+ expect.arrayContaining([{ label: 'foo' }, { label: 'bar' }]),
180
+ );
181
+ });
182
+ });
@@ -0,0 +1,103 @@
1
+ import { render, fireEvent, screen } from '../../test-utils';
2
+ import TypeaheadInput, { TypeaheadInputProps } from './TypeaheadInput';
3
+ import { TypeaheadOption } from '../Typeahead';
4
+
5
+ const defaultProps: TypeaheadInputProps<number> = {
6
+ id: 'test-id',
7
+ name: 'test-name',
8
+ typeaheadId: 'test-id',
9
+ value: '',
10
+ selected: [],
11
+ onChange: jest.fn(),
12
+ onKeyDown: jest.fn(),
13
+ onFocus: jest.fn(),
14
+ onPaste: jest.fn(),
15
+ autoComplete: 'off',
16
+ placeholder: 'Search...',
17
+ renderChip: jest.fn(),
18
+ };
19
+
20
+ describe('TypeaheadInput', () => {
21
+ afterEach(() => {
22
+ jest.clearAllMocks();
23
+ });
24
+
25
+ it('renders input with placeholder', () => {
26
+ render(<TypeaheadInput {...defaultProps} />);
27
+ expect(screen.getByPlaceholderText('Search...')).toBeInTheDocument();
28
+ });
29
+
30
+ it('renders with given value', () => {
31
+ render(<TypeaheadInput {...defaultProps} value="hello" />);
32
+ expect(screen.getByDisplayValue('hello')).toBeInTheDocument();
33
+ });
34
+
35
+ it('calls onChange when input value changes', () => {
36
+ const onChange = jest.fn();
37
+ render(<TypeaheadInput {...defaultProps} onChange={onChange} />);
38
+ const input = screen.getByPlaceholderText('Search...');
39
+ fireEvent.change(input, { target: { value: 'test' } });
40
+ expect(onChange).toHaveBeenCalled();
41
+ });
42
+
43
+ it('calls onFocus when input is focused', () => {
44
+ const onFocus = jest.fn();
45
+ render(<TypeaheadInput {...defaultProps} onFocus={onFocus} />);
46
+ const input = screen.getByPlaceholderText('Search...');
47
+ fireEvent.focus(input);
48
+ expect(onFocus).toHaveBeenCalled();
49
+ });
50
+
51
+ it('calls onPaste when input is pasted into', () => {
52
+ const onPaste = jest.fn();
53
+ render(<TypeaheadInput {...defaultProps} onPaste={onPaste} />);
54
+ const input = screen.getByPlaceholderText('Search...');
55
+ fireEvent.paste(input, { clipboardData: { getData: () => 'pasted' } });
56
+ expect(onPaste).toHaveBeenCalled();
57
+ });
58
+
59
+ it('calls onKeyDown when key is pressed', () => {
60
+ const onKeyDown = jest.fn();
61
+ render(<TypeaheadInput {...defaultProps} onKeyDown={onKeyDown} />);
62
+ const input = screen.getByPlaceholderText('Search...');
63
+ fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown' });
64
+ expect(onKeyDown).toHaveBeenCalled();
65
+ });
66
+
67
+ it('renders chips when multiple is true and selected has items', () => {
68
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
69
+ const renderChip = jest.fn((chip, idx) => <span key={idx}>{chip.label}</span>);
70
+ const selected = [
71
+ { label: 'Chip 1', value: 1 },
72
+ { label: 'Chip 2', value: 2 },
73
+ ];
74
+ render(
75
+ <TypeaheadInput {...defaultProps} multiple selected={selected} renderChip={renderChip} />,
76
+ );
77
+ expect(screen.getByText('Chip 1')).toBeInTheDocument();
78
+ expect(screen.getByText('Chip 2')).toBeInTheDocument();
79
+ });
80
+
81
+ it('does not show placeholder if multiple is true and selected has items', () => {
82
+ const selected: TypeaheadOption<number>[] = [{ label: 'Chip', value: 1 }];
83
+ render(<TypeaheadInput<number> {...defaultProps} multiple selected={selected} />);
84
+ expect(screen.queryByPlaceholderText('Search...')).not.toBeInTheDocument();
85
+ });
86
+
87
+ it('applies input width style when multiple and selected has items', () => {
88
+ const selected = [{ label: 'Chip', value: 1 }];
89
+ const { container } = render(
90
+ <TypeaheadInput {...defaultProps} multiple selected={selected} value="test" />,
91
+ );
92
+ const input = container.querySelector('input');
93
+ expect(input?.style.width).not.toBe('');
94
+ });
95
+
96
+ it('sets aria attributes correctly', () => {
97
+ render(<TypeaheadInput {...defaultProps} dropdownOpen ariaActivedescendant="option-1" />);
98
+ const input = screen.getByRole('combobox');
99
+ expect(input).toHaveAttribute('aria-expanded', 'true');
100
+ expect(input).toHaveAttribute('aria-haspopup', 'listbox');
101
+ expect(input).toHaveAttribute('aria-activedescendant', expect.stringContaining('option-1'));
102
+ });
103
+ });
@@ -0,0 +1,43 @@
1
+ import { render, screen } from '../../test-utils';
2
+ import Highlight from './highlight';
3
+
4
+ describe('Highlight', () => {
5
+ it('renders value with highlighted query (case-insensitive)', () => {
6
+ render(<Highlight value="Hello World" query="world" />);
7
+ expect(screen.getByText('World')).toBeInTheDocument();
8
+ expect(screen.getByText('World').tagName).toBe('STRONG');
9
+ expect(screen.getByText('Hello', { exact: false })).toBeInTheDocument();
10
+ });
11
+
12
+ it('renders value with highlighted query (case-sensitive in output)', () => {
13
+ render(<Highlight value="Hello World" query="HELLO" />);
14
+ expect(screen.getByText('Hello').tagName).toBe('STRONG');
15
+ expect(screen.getByText('World', { exact: false })).toBeInTheDocument();
16
+ });
17
+
18
+ it('renders value without highlight if query not found', () => {
19
+ render(<Highlight value="Hello World" query="foo" />);
20
+ expect(screen.getByText('Hello World')).toBeInTheDocument();
21
+ expect(screen.queryByRole('strong')).not.toBeInTheDocument();
22
+ });
23
+
24
+ it('renders value as is if query is empty', () => {
25
+ render(<Highlight value="Hello World" query="" />);
26
+ expect(screen.getByText('Hello World')).toBeInTheDocument();
27
+ expect(screen.queryByRole('strong')).not.toBeInTheDocument();
28
+ });
29
+
30
+ it('renders value as is if value is empty', () => {
31
+ render(<Highlight value="" query="test" />);
32
+ expect(screen.queryByText(/./)).not.toBeInTheDocument();
33
+ expect(screen.queryByRole('strong')).not.toBeInTheDocument();
34
+ });
35
+
36
+ it('wraps content in span if className is provided', () => {
37
+ const { container } = render(
38
+ <Highlight value="Hello World" query="world" className="highlighted" />,
39
+ );
40
+ expect(container.querySelector('span.highlighted')).toBeInTheDocument();
41
+ expect(container.querySelector('strong')).toBeInTheDocument();
42
+ });
43
+ });
@@ -6,6 +6,14 @@ import { postData } from './utils/postData';
6
6
 
7
7
  jest.mock('./utils/asyncFileRead');
8
8
  jest.mock('./utils/postData');
9
+ jest.mock('commonmark', () => ({
10
+ Parser: jest.fn().mockImplementation(() => ({
11
+ parse: jest.fn(() => ({})),
12
+ })),
13
+ HtmlRenderer: jest.fn().mockImplementation(() => ({
14
+ render: jest.fn(() => ''),
15
+ })),
16
+ }));
9
17
 
10
18
  const TEST_FILE = new File(['test content'], 'test.png', { type: 'image/png' });
11
19
  const INVALID_FILE = new File(['invalid content'], 'invalid.txt', { type: 'text/plain' });
@@ -76,6 +84,11 @@ describe('Upload Component', () => {
76
84
  dispatchEvent: jest.fn(),
77
85
  })),
78
86
  });
87
+ props.onStart.mockReset();
88
+ props.onSuccess.mockReset();
89
+ props.onFailure.mockReset();
90
+ props.onCancel.mockReset();
91
+ props.onChange.mockReset();
79
92
  });
80
93
 
81
94
  afterEach(async () => {
@@ -292,4 +305,54 @@ describe('Upload Component', () => {
292
305
  const errorIcon = await screen.findByLabelText(/Custom error label/i);
293
306
  expect(errorIcon).toBeInTheDocument();
294
307
  });
308
+
309
+ function runAnimationDelayTest(label: string, delay: number) {
310
+ test(`should respect a ${label} animationDelay`, async () => {
311
+ (asyncFileRead as jest.Mock).mockResolvedValue('mockBase64Image');
312
+ (postData as jest.Mock).mockResolvedValue('mockSuccessResponse');
313
+
314
+ const { container, unmount } = render(<Upload {...props} animationDelay={delay} />);
315
+
316
+ await act(async () => {
317
+ const droppableElement = await waitFor(() => container.querySelector('.droppable-area'));
318
+ const dataTransfer = new DataTransfer();
319
+ dataTransfer.items.add(TEST_FILE);
320
+ fireEvent.drop(droppableElement!, { dataTransfer });
321
+ });
322
+
323
+ // Advance half the delay, should still be processing
324
+ await act(async () => {
325
+ jest.advanceTimersByTime(delay / 2);
326
+ });
327
+ expect(container.querySelector('.droppable-processing')).toBeInTheDocument();
328
+ expect(container.querySelector('.droppable-complete')).not.toBeInTheDocument();
329
+
330
+ // Advance the rest of the delay
331
+ await act(async () => {
332
+ jest.advanceTimersByTime(delay / 2 + 10);
333
+ });
334
+ expect(container.querySelector('.droppable-complete')).toBeInTheDocument();
335
+ expect(container.querySelector('.droppable-processing')).not.toBeInTheDocument();
336
+
337
+ unmount();
338
+ });
339
+ }
340
+
341
+ runAnimationDelayTest('short', 50);
342
+ runAnimationDelayTest('long', 500);
343
+
344
+ test('should handle disabled state correctly', async () => {
345
+ const { container } = render(<Upload {...props} usDisabled />);
346
+ const droppableElement = await waitFor(() => container.querySelector('.droppable-area'));
347
+
348
+ const dataTransfer = new DataTransfer();
349
+ dataTransfer.items.add(TEST_FILE);
350
+
351
+ await act(async () => {
352
+ fireEvent.drop(droppableElement!, { dataTransfer });
353
+ });
354
+
355
+ expect(container.querySelector('.droppable-processing')).not.toBeInTheDocument();
356
+ expect(props.onStart).not.toHaveBeenCalled();
357
+ });
295
358
  });
@@ -38,54 +38,3 @@ export const Basic: Story = {
38
38
  onCancel: fn(),
39
39
  },
40
40
  } satisfies Story;
41
-
42
- export const MaxSizes = () => {
43
- const bKB = 1024;
44
- const bMB = 1024 * bKB;
45
-
46
- const dKB = 1000;
47
- const dMB = 1000 * dKB;
48
-
49
- const binarySizes = [
50
- 10 * bMB,
51
- 5 * bMB,
52
- 1 * bMB,
53
- 500 * bKB,
54
- 100 * bKB,
55
- 50 * bKB,
56
- 10 * bKB,
57
- 5 * bKB,
58
- 1 * bKB,
59
- ];
60
-
61
- const decimalSizes = [
62
- 10 * dMB,
63
- 5 * dMB,
64
- 1 * dMB,
65
- 500 * dKB,
66
- 100 * dKB,
67
- 50 * dKB,
68
- 10 * dKB,
69
- 5 * dKB,
70
- 1 * dKB,
71
- ];
72
-
73
- return (
74
- <div style={{ display: 'flex', gap: '1rem' }}>
75
- <div style={{ flex: 1 }}>
76
- {binarySizes.map((maxSize) => (
77
- <Field key={maxSize} label={`Max size: ${maxSize} bytes`}>
78
- <Upload usLabel="Pick a file, any file" maxSize={maxSize} />
79
- </Field>
80
- ))}
81
- </div>
82
- <div style={{ flex: 1 }}>
83
- {decimalSizes.map((maxSize) => (
84
- <Field key={maxSize} label={`Max size: ${maxSize} bytes`}>
85
- <Upload usLabel="Pick a file, any file" maxSize={maxSize} />
86
- </Field>
87
- ))}
88
- </div>
89
- </div>
90
- );
91
- };
@@ -0,0 +1,93 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { fn } from '@storybook/test';
3
+
4
+ import Upload from '.';
5
+ import { MAX_SIZE_DEFAULT } from './Upload';
6
+ import { Field } from '../field/Field';
7
+
8
+ const meta = {
9
+ component: Upload,
10
+ title: 'Forms/Upload/Tests',
11
+ argTypes: {
12
+ maxSize: {
13
+ control: {
14
+ type: 'number',
15
+ min: 0,
16
+ },
17
+ },
18
+ },
19
+ } satisfies Meta<typeof Upload>;
20
+
21
+ export default meta;
22
+
23
+ export const MaxSizes = () => {
24
+ const bKB = 1024;
25
+ const bMB = 1024 * bKB;
26
+
27
+ const dKB = 1000;
28
+ const dMB = 1000 * dKB;
29
+
30
+ const binarySizes = [
31
+ 10 * bMB,
32
+ 5 * bMB,
33
+ 1 * bMB,
34
+ 500 * bKB,
35
+ 100 * bKB,
36
+ 50 * bKB,
37
+ 10 * bKB,
38
+ 5 * bKB,
39
+ 1 * bKB,
40
+ ];
41
+
42
+ const decimalSizes = [
43
+ 10 * dMB,
44
+ 5 * dMB,
45
+ 1 * dMB,
46
+ 500 * dKB,
47
+ 100 * dKB,
48
+ 50 * dKB,
49
+ 10 * dKB,
50
+ 5 * dKB,
51
+ 1 * dKB,
52
+ ];
53
+
54
+ return (
55
+ <div style={{ display: 'flex', gap: '1rem' }}>
56
+ <div style={{ flex: 1 }}>
57
+ {binarySizes.map((maxSize) => (
58
+ <Field key={maxSize} label={`Max size: ${maxSize} bytes`}>
59
+ <Upload usLabel="Pick a file, any file" maxSize={maxSize} />
60
+ </Field>
61
+ ))}
62
+ </div>
63
+ <div style={{ flex: 1 }}>
64
+ {decimalSizes.map((maxSize) => (
65
+ <Field key={maxSize} label={`Max size: ${maxSize} bytes`}>
66
+ <Upload usLabel="Pick a file, any file" maxSize={maxSize} />
67
+ </Field>
68
+ ))}
69
+ </div>
70
+ </div>
71
+ );
72
+ };
73
+
74
+ export const AllVariants = () => {
75
+ return (
76
+ <div style={{ display: 'flex', gap: '1rem' }}>
77
+ <div style={{ flex: 1 }}>
78
+ <Field key="small" label="Small Upload">
79
+ <Upload usLabel="Pick a file, any file" size="sm" />
80
+ </Field>
81
+ <Field key="medium" label="Medium Upload">
82
+ <Upload usLabel="Pick a file, any file" size="md" />
83
+ </Field>
84
+ <Field key="large" label="Large Upload">
85
+ <Upload usLabel="Pick a file, any file" size="lg" />
86
+ </Field>
87
+ <Field key="disabled" label="Disabled Upload">
88
+ <Upload usLabel="Pick a file, any file" usDisabled />
89
+ </Field>
90
+ </div>
91
+ </div>
92
+ );
93
+ };
@@ -12,11 +12,6 @@ import { postData, asyncFileRead, isSizeValid, isTypeValid, getFileType } from '
12
12
  import { PostDataFetcher, PostDataHTTPOptions, ResponseError } from './utils/postData/postData';
13
13
  import { ProcessIndicatorStatus } from '../processIndicator';
14
14
 
15
- /*
16
- * This delay is required for the isError/isSuccess to be fired after isProcessing so the processIndicator, will be
17
- * rendered first and then updated with the right status.
18
- */
19
- const ANIMATION_FIX = 10;
20
15
  export const MAX_SIZE_DEFAULT = 5000000;
21
16
 
22
17
  export enum UploadStep {
@@ -85,7 +80,7 @@ export class Upload extends Component<UploadProps, UploadState> {
85
80
  declare props: UploadProps & Required<Pick<UploadProps, keyof typeof Upload.defaultProps>>;
86
81
 
87
82
  static defaultProps = {
88
- animationDelay: 700,
83
+ animationDelay: 300,
89
84
  maxSize: MAX_SIZE_DEFAULT,
90
85
  psButtonDisabled: false,
91
86
  size: 'md',
@@ -160,41 +155,6 @@ export class Upload extends Component<UploadProps, UploadState> {
160
155
  }
161
156
  }
162
157
 
163
- onAnimationCompleted = async (status: ProcessIndicatorStatus) => {
164
- const { response, isProcessing, fileName } = this.state;
165
- const { animationDelay } = this.props;
166
-
167
- if (isProcessing && status === 'succeeded') {
168
- const { onSuccess } = this.props;
169
- this.timeouts = window.setTimeout(() => {
170
- this.setState(
171
- {
172
- isProcessing: false,
173
- isComplete: true,
174
- },
175
- onSuccess
176
- ? () => {
177
- onSuccess(response as string | Response, fileName);
178
- }
179
- : undefined,
180
- );
181
- }, animationDelay);
182
- }
183
-
184
- if (isProcessing && status === 'failed') {
185
- const { onFailure } = this.props;
186
- this.timeouts = window.setTimeout(() => {
187
- this.setState(
188
- {
189
- isProcessing: false,
190
- isComplete: true,
191
- },
192
- onFailure ? () => onFailure(response) : undefined,
193
- );
194
- }, animationDelay);
195
- }
196
- };
197
-
198
158
  asyncPost = async (file: File) => {
199
159
  const { httpOptions, fetcher } = this.props;
200
160
  if (httpOptions == null) {
@@ -209,15 +169,37 @@ export class Upload extends Component<UploadProps, UploadState> {
209
169
  return postData(httpOptions, formData, fetcher);
210
170
  };
211
171
 
212
- asyncResponse = (response: unknown, type: 'success' | 'error') => {
213
- // Gives time to the animation callback to fire.
172
+ handleUploadComplete = (type: 'success' | 'error', response: unknown) => {
173
+ const { animationDelay, onSuccess, onFailure } = this.props;
174
+ const { fileName } = this.state;
175
+
176
+ window.clearTimeout(this.timeouts);
214
177
  this.timeouts = window.setTimeout(() => {
215
- this.setState({
178
+ this.setState(
179
+ {
180
+ isProcessing: false,
181
+ isComplete: true,
182
+ },
183
+ () => {
184
+ if (type === 'success') {
185
+ onSuccess?.(response as string | Response, fileName);
186
+ } else {
187
+ onFailure?.(response);
188
+ }
189
+ },
190
+ );
191
+ }, animationDelay);
192
+ };
193
+
194
+ asyncResponse = (response: unknown, type: 'success' | 'error') => {
195
+ this.setState(
196
+ {
216
197
  response,
217
198
  isError: type === 'error',
218
199
  isSuccess: type === 'success',
219
- });
220
- }, ANIMATION_FIX);
200
+ },
201
+ () => this.handleUploadComplete(type, response),
202
+ );
221
203
  };
222
204
 
223
205
  handleOnClear: React.MouseEventHandler<HTMLButtonElement> = (event) => {
@@ -395,8 +377,6 @@ export class Upload extends Component<UploadProps, UploadState> {
395
377
  />
396
378
  )}
397
379
 
398
- {/* Starts render the step when isSuccess is true so markup is there when css transition kicks in
399
- css transition to work properly */}
400
380
  {(isSuccess || isComplete) && !isError && (
401
381
  <CompleteStep
402
382
  fileName={fileName}
@@ -443,7 +423,6 @@ export class Upload extends Component<UploadProps, UploadState> {
443
423
  psButtonText={psButtonText || intl.formatMessage(messages.psButtonText)}
444
424
  psProcessingText={psProcessingText || intl.formatMessage(messages.psProcessingText)}
445
425
  psButtonDisabled={psButtonDisabled}
446
- onAnimationCompleted={async (status) => this.onAnimationCompleted(status)}
447
426
  onClear={(event) => this.handleOnClear(event)}
448
427
  />
449
428
  )}