@transferwise/components 46.97.5 → 46.98.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 (221) 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/i18n/cs.json +3 -0
  10. package/build/i18n/cs.json.js +3 -0
  11. package/build/i18n/cs.json.js.map +1 -1
  12. package/build/i18n/cs.json.mjs +3 -0
  13. package/build/i18n/cs.json.mjs.map +1 -1
  14. package/build/i18n/de.json +3 -0
  15. package/build/i18n/de.json.js +3 -0
  16. package/build/i18n/de.json.js.map +1 -1
  17. package/build/i18n/de.json.mjs +3 -0
  18. package/build/i18n/de.json.mjs.map +1 -1
  19. package/build/i18n/en.json +3 -0
  20. package/build/i18n/en.json.js +3 -0
  21. package/build/i18n/en.json.js.map +1 -1
  22. package/build/i18n/en.json.mjs +3 -0
  23. package/build/i18n/en.json.mjs.map +1 -1
  24. package/build/i18n/es.json +3 -0
  25. package/build/i18n/es.json.js +3 -0
  26. package/build/i18n/es.json.js.map +1 -1
  27. package/build/i18n/es.json.mjs +3 -0
  28. package/build/i18n/es.json.mjs.map +1 -1
  29. package/build/i18n/fr.json +3 -0
  30. package/build/i18n/fr.json.js +3 -0
  31. package/build/i18n/fr.json.js.map +1 -1
  32. package/build/i18n/fr.json.mjs +3 -0
  33. package/build/i18n/fr.json.mjs.map +1 -1
  34. package/build/i18n/hu.json +3 -0
  35. package/build/i18n/hu.json.js +3 -0
  36. package/build/i18n/hu.json.js.map +1 -1
  37. package/build/i18n/hu.json.mjs +3 -0
  38. package/build/i18n/hu.json.mjs.map +1 -1
  39. package/build/i18n/id.json +3 -0
  40. package/build/i18n/id.json.js +3 -0
  41. package/build/i18n/id.json.js.map +1 -1
  42. package/build/i18n/id.json.mjs +3 -0
  43. package/build/i18n/id.json.mjs.map +1 -1
  44. package/build/i18n/it.json +3 -0
  45. package/build/i18n/it.json.js +3 -0
  46. package/build/i18n/it.json.js.map +1 -1
  47. package/build/i18n/it.json.mjs +3 -0
  48. package/build/i18n/it.json.mjs.map +1 -1
  49. package/build/i18n/ja.json +3 -0
  50. package/build/i18n/ja.json.js +3 -0
  51. package/build/i18n/ja.json.js.map +1 -1
  52. package/build/i18n/ja.json.mjs +3 -0
  53. package/build/i18n/ja.json.mjs.map +1 -1
  54. package/build/i18n/nl.json +6 -3
  55. package/build/i18n/pl.json +3 -0
  56. package/build/i18n/pl.json.js +3 -0
  57. package/build/i18n/pl.json.js.map +1 -1
  58. package/build/i18n/pl.json.mjs +3 -0
  59. package/build/i18n/pl.json.mjs.map +1 -1
  60. package/build/i18n/pt.json +3 -0
  61. package/build/i18n/pt.json.js +3 -0
  62. package/build/i18n/pt.json.js.map +1 -1
  63. package/build/i18n/pt.json.mjs +3 -0
  64. package/build/i18n/pt.json.mjs.map +1 -1
  65. package/build/i18n/ro.json +3 -0
  66. package/build/i18n/ro.json.js +3 -0
  67. package/build/i18n/ro.json.js.map +1 -1
  68. package/build/i18n/ro.json.mjs +3 -0
  69. package/build/i18n/ro.json.mjs.map +1 -1
  70. package/build/i18n/ru.json +3 -0
  71. package/build/i18n/ru.json.js +3 -0
  72. package/build/i18n/ru.json.js.map +1 -1
  73. package/build/i18n/ru.json.mjs +3 -0
  74. package/build/i18n/ru.json.mjs.map +1 -1
  75. package/build/i18n/th.json +3 -0
  76. package/build/i18n/th.json.js +3 -0
  77. package/build/i18n/th.json.js.map +1 -1
  78. package/build/i18n/th.json.mjs +3 -0
  79. package/build/i18n/th.json.mjs.map +1 -1
  80. package/build/i18n/tr.json +3 -0
  81. package/build/i18n/tr.json.js +3 -0
  82. package/build/i18n/tr.json.js.map +1 -1
  83. package/build/i18n/tr.json.mjs +3 -0
  84. package/build/i18n/tr.json.mjs.map +1 -1
  85. package/build/i18n/zh-CN.json +3 -0
  86. package/build/i18n/zh-CN.json.js +3 -0
  87. package/build/i18n/zh-CN.json.js.map +1 -1
  88. package/build/i18n/zh-CN.json.mjs +3 -0
  89. package/build/i18n/zh-CN.json.mjs.map +1 -1
  90. package/build/i18n/zh-HK.json +3 -0
  91. package/build/i18n/zh-HK.json.js +3 -0
  92. package/build/i18n/zh-HK.json.js.map +1 -1
  93. package/build/i18n/zh-HK.json.mjs +3 -0
  94. package/build/i18n/zh-HK.json.mjs.map +1 -1
  95. package/build/image/Image.js +9 -10
  96. package/build/image/Image.js.map +1 -1
  97. package/build/image/Image.mjs +11 -11
  98. package/build/image/Image.mjs.map +1 -1
  99. package/build/main.css +5 -2
  100. package/build/moneyInput/MoneyInput.js +2 -6
  101. package/build/moneyInput/MoneyInput.js.map +1 -1
  102. package/build/moneyInput/MoneyInput.messages.js +3 -0
  103. package/build/moneyInput/MoneyInput.messages.js.map +1 -1
  104. package/build/moneyInput/MoneyInput.messages.mjs +3 -0
  105. package/build/moneyInput/MoneyInput.messages.mjs.map +1 -1
  106. package/build/moneyInput/MoneyInput.mjs +2 -6
  107. package/build/moneyInput/MoneyInput.mjs.map +1 -1
  108. package/build/phoneNumberInput/PhoneNumberInput.js +36 -2
  109. package/build/phoneNumberInput/PhoneNumberInput.js.map +1 -1
  110. package/build/phoneNumberInput/PhoneNumberInput.messages.js +6 -0
  111. package/build/phoneNumberInput/PhoneNumberInput.messages.js.map +1 -1
  112. package/build/phoneNumberInput/PhoneNumberInput.messages.mjs +6 -0
  113. package/build/phoneNumberInput/PhoneNumberInput.messages.mjs.map +1 -1
  114. package/build/phoneNumberInput/PhoneNumberInput.mjs +36 -2
  115. package/build/phoneNumberInput/PhoneNumberInput.mjs.map +1 -1
  116. package/build/styles/circularButton/CircularButton.css +1 -0
  117. package/build/styles/dateInput/DateInput.css +2 -1
  118. package/build/styles/main.css +5 -2
  119. package/build/styles/uploadInput/uploadItem/UploadItem.css +2 -1
  120. package/build/types/alert/Alert.d.ts.map +1 -1
  121. package/build/types/common/closeButton/CloseButton.d.ts +2 -0
  122. package/build/types/common/closeButton/CloseButton.d.ts.map +1 -1
  123. package/build/types/image/Image.d.ts +0 -1
  124. package/build/types/image/Image.d.ts.map +1 -1
  125. package/build/types/moneyInput/MoneyInput.d.ts.map +1 -1
  126. package/build/types/moneyInput/MoneyInput.messages.d.ts +5 -0
  127. package/build/types/moneyInput/MoneyInput.messages.d.ts.map +1 -1
  128. package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
  129. package/build/types/phoneNumberInput/PhoneNumberInput.messages.d.ts +8 -0
  130. package/build/types/phoneNumberInput/PhoneNumberInput.messages.d.ts.map +1 -1
  131. package/build/types/test-utils/index.d.ts +6 -0
  132. package/build/types/test-utils/index.d.ts.map +1 -1
  133. package/build/types/upload/Upload.d.ts +1 -2
  134. package/build/types/upload/Upload.d.ts.map +1 -1
  135. package/build/types/upload/steps/processingStep/processingStep.d.ts +1 -3
  136. package/build/types/upload/steps/processingStep/processingStep.d.ts.map +1 -1
  137. package/build/types/uploadInput/UploadInput.d.ts.map +1 -1
  138. package/build/types/uploadInput/uploadItem/UploadItem.d.ts +1 -1
  139. package/build/types/uploadInput/uploadItem/UploadItem.d.ts.map +1 -1
  140. package/build/types/withDisplayFormat/WithDisplayFormat.d.ts.map +1 -1
  141. package/build/upload/Upload.js +27 -43
  142. package/build/upload/Upload.js.map +1 -1
  143. package/build/upload/Upload.mjs +27 -43
  144. package/build/upload/Upload.mjs.map +1 -1
  145. package/build/upload/steps/processingStep/processingStep.js +1 -3
  146. package/build/upload/steps/processingStep/processingStep.js.map +1 -1
  147. package/build/upload/steps/processingStep/processingStep.mjs +1 -3
  148. package/build/upload/steps/processingStep/processingStep.mjs.map +1 -1
  149. package/build/uploadInput/UploadInput.js +55 -6
  150. package/build/uploadInput/UploadInput.js.map +1 -1
  151. package/build/uploadInput/UploadInput.mjs +55 -6
  152. package/build/uploadInput/UploadInput.mjs.map +1 -1
  153. package/build/uploadInput/uploadItem/UploadItem.js +12 -6
  154. package/build/uploadInput/uploadItem/UploadItem.js.map +1 -1
  155. package/build/uploadInput/uploadItem/UploadItem.mjs +12 -6
  156. package/build/uploadInput/uploadItem/UploadItem.mjs.map +1 -1
  157. package/build/withDisplayFormat/WithDisplayFormat.js +3 -2
  158. package/build/withDisplayFormat/WithDisplayFormat.js.map +1 -1
  159. package/build/withDisplayFormat/WithDisplayFormat.mjs +3 -2
  160. package/build/withDisplayFormat/WithDisplayFormat.mjs.map +1 -1
  161. package/package.json +6 -9
  162. package/src/alert/Alert.spec.tsx +11 -0
  163. package/src/alert/Alert.story.tsx +23 -9
  164. package/src/alert/Alert.tsx +14 -1
  165. package/src/circularButton/CircularButton.css +1 -0
  166. package/src/circularButton/CircularButton.less +1 -0
  167. package/src/circularButton/CircularButton.tests.story.tsx +23 -0
  168. package/src/common/closeButton/CloseButton.spec.tsx +13 -1
  169. package/src/common/closeButton/CloseButton.tsx +3 -0
  170. package/src/dateInput/DateInput.css +2 -1
  171. package/src/dateInput/DateInput.less +7 -4
  172. package/src/i18n/cs.json +3 -0
  173. package/src/i18n/de.json +3 -0
  174. package/src/i18n/en.json +3 -0
  175. package/src/i18n/es.json +3 -0
  176. package/src/i18n/fr.json +3 -0
  177. package/src/i18n/hu.json +3 -0
  178. package/src/i18n/id.json +3 -0
  179. package/src/i18n/it.json +3 -0
  180. package/src/i18n/ja.json +3 -0
  181. package/src/i18n/nl.json +6 -3
  182. package/src/i18n/pl.json +3 -0
  183. package/src/i18n/pt.json +3 -0
  184. package/src/i18n/ro.json +3 -0
  185. package/src/i18n/ru.json +3 -0
  186. package/src/i18n/th.json +3 -0
  187. package/src/i18n/tr.json +3 -0
  188. package/src/i18n/zh-CN.json +3 -0
  189. package/src/i18n/zh-HK.json +3 -0
  190. package/src/image/Image.spec.tsx +3 -3
  191. package/src/image/Image.tsx +10 -12
  192. package/src/main.css +5 -2
  193. package/src/moneyInput/MoneyInput.messages.ts +5 -0
  194. package/src/moneyInput/MoneyInput.spec.tsx +42 -5
  195. package/src/moneyInput/MoneyInput.story.tsx +11 -2
  196. package/src/moneyInput/MoneyInput.tsx +5 -7
  197. package/src/phoneNumberInput/PhoneNumberInput.messages.ts +8 -0
  198. package/src/phoneNumberInput/PhoneNumberInput.spec.tsx +77 -43
  199. package/src/phoneNumberInput/PhoneNumberInput.tsx +34 -2
  200. package/src/promoCard/__snapshots__/PromoCard.spec.tsx.snap +1 -0
  201. package/src/promoCard/__snapshots__/PromoCardGroup.spec.tsx.snap +2 -0
  202. package/src/test-utils/jest.setup.ts +0 -4
  203. package/src/typeahead/Typeahead.spec.tsx +182 -0
  204. package/src/typeahead/typeaheadInput/TypeaheadInput.spec.tsx +103 -0
  205. package/src/typeahead/util/highlight.spec.tsx +43 -0
  206. package/src/upload/Upload.spec.tsx +63 -0
  207. package/src/upload/Upload.story.tsx +0 -51
  208. package/src/upload/Upload.tests.story.tsx +93 -0
  209. package/src/upload/Upload.tsx +28 -49
  210. package/src/upload/steps/processingStep/processingStep.tsx +2 -7
  211. package/src/uploadInput/UploadInput.tsx +81 -8
  212. package/src/uploadInput/uploadItem/UploadItem.css +2 -1
  213. package/src/uploadInput/uploadItem/UploadItem.less +1 -1
  214. package/src/uploadInput/uploadItem/UploadItem.tsx +11 -6
  215. package/src/withDisplayFormat/WithDisplayFormat.spec.js +11 -15
  216. package/src/withDisplayFormat/WithDisplayFormat.tsx +3 -2
  217. package/src/typeahead/Typeahead.rtl.spec.tsx +0 -54
  218. package/src/typeahead/Typeahead.spec.js +0 -404
  219. package/src/typeahead/typeaheadInput/TypeaheadInput.spec.js +0 -74
  220. package/src/typeahead/typeaheadOption/TypeaheadOption.spec.js +0 -75
  221. package/src/typeahead/util/highlight.spec.js +0 -34
@@ -21,28 +21,30 @@ describe('PhoneNumberInput', () => {
21
21
  const customRender = (overrides: Partial<PhoneNumberInputProps> = {}, locale?: string) =>
22
22
  render(<PhoneNumberInput {...props} {...overrides} />, { locale });
23
23
 
24
- const getPrefixEl = () => screen.getByRole('combobox');
25
- const getInputEl = () => screen.getByRole('textbox');
24
+ const getCountryCodeSelect = () => screen.getByRole('combobox');
25
+ const getCountryCodeLabel = () => screen.getByText('Country code');
26
+ const getPhoneNumberInput = () => screen.getByRole('textbox');
27
+ const getPhoneNumberLabel = () => screen.getByText('Phone number');
26
28
 
27
29
  describe('defaults', () => {
28
30
  it('should set prefix control to default UK value', () => {
29
31
  customRender();
30
- expect(getPrefixEl()).toHaveTextContent('+44');
32
+ expect(getCountryCodeSelect()).toHaveTextContent('+44');
31
33
  });
32
34
 
33
35
  it('should set number control to empty', () => {
34
36
  customRender();
35
- expect(getInputEl()).toHaveValue('');
37
+ expect(getPhoneNumberInput()).toHaveValue('');
36
38
  });
37
39
 
38
40
  it('should not disable the select', () => {
39
41
  customRender();
40
- expect(getPrefixEl()).toBeEnabled();
42
+ expect(getCountryCodeSelect()).toBeEnabled();
41
43
  });
42
44
 
43
45
  it('should not disable the input', () => {
44
46
  customRender();
45
- expect(getInputEl()).toBeEnabled();
47
+ expect(getPhoneNumberInput()).toBeEnabled();
46
48
  });
47
49
  });
48
50
 
@@ -50,22 +52,54 @@ describe('PhoneNumberInput', () => {
50
52
  const prefix = '+39';
51
53
  const number = '123456789';
52
54
  customRender({ initialValue: `${prefix}${number}` });
53
- expect(getPrefixEl()).toHaveTextContent(prefix);
54
- expect(getInputEl()).toHaveValue(number);
55
+ expect(getCountryCodeSelect()).toHaveTextContent(prefix);
56
+ expect(getPhoneNumberInput()).toHaveValue(number);
55
57
  });
56
58
 
57
59
  describe('id prop', () => {
58
- it('should not render id by default', () => {
60
+ it('should render sensible default IDs', () => {
59
61
  customRender();
60
- expect(getPrefixEl()).not.toHaveAttribute('id');
61
- expect(getInputEl()).not.toHaveAttribute('id');
62
+ const countryCodeSelectID = getCountryCodeSelect().getAttribute('id');
63
+ expect(countryCodeSelectID).toMatch(/^country-code-select-[a-z0-9]{6}$/);
64
+ const countryCodeLabelID = getCountryCodeLabel().getAttribute('id');
65
+ expect(countryCodeLabelID).toMatch(/^country-code-label-[a-z0-9]{6}$/);
66
+ const phoneNumberInputID = getPhoneNumberInput().getAttribute('id');
67
+ expect(phoneNumberInputID).toMatch(/^phone-number-input-[a-z0-9]{6}$/);
68
+ const phoneNumberLabelID = getPhoneNumberLabel().getAttribute('id');
69
+ expect(phoneNumberLabelID).toMatch(/^phone-number-label-[a-z0-9]{6}$/);
62
70
  });
63
71
 
64
- it('should respect `id` for the input and ignore the select', () => {
72
+ it('should use the custom `id` as-is for the input but with slight modification for select', () => {
65
73
  const id = 'component-id';
66
74
  customRender({ id });
67
- expect(getPrefixEl()).not.toHaveAttribute('id');
68
- expect(getInputEl()).toHaveAttribute('id', id);
75
+ expect(getPhoneNumberInput()).toHaveAttribute('id', id);
76
+ expect(getPhoneNumberLabel()).toHaveAttribute('id', `${id}-phone-number-label`);
77
+ expect(getCountryCodeSelect()).toHaveAttribute('id', `${id}-country-code-select`);
78
+ expect(getCountryCodeLabel()).toHaveAttribute('id', `${id}-country-code-label`);
79
+ });
80
+
81
+ it('should set correct ARIA attributes on the country code select when id is provided', () => {
82
+ const id = 'my-id';
83
+ customRender({ id });
84
+ expect(getCountryCodeSelect()).toHaveAttribute('aria-labelledby', `${id}-country-code-label`);
85
+ });
86
+
87
+ it('should set correct ARIA attributes on the country code select when id is not provided', () => {
88
+ customRender();
89
+ const selectLabelledBy = getCountryCodeSelect().getAttribute('aria-labelledby');
90
+ expect(selectLabelledBy).toMatch(/^country-code-label-[a-z0-9]{6}$/);
91
+ });
92
+
93
+ it('should set correct ARIA attributes on the phone number input when id is provided', () => {
94
+ const id = 'my-id';
95
+ customRender({ id });
96
+ expect(getPhoneNumberInput()).toHaveAttribute('aria-labelledby', `${id}-phone-number-label`);
97
+ });
98
+
99
+ it('should set correct ARIA attributes on the phone number input when id is not provided', () => {
100
+ customRender();
101
+ const inputLabelledBy = getPhoneNumberInput().getAttribute('aria-labelledby');
102
+ expect(inputLabelledBy).toMatch(/^phone-number-label-[a-z0-9]{6}$/);
69
103
  });
70
104
  });
71
105
 
@@ -94,38 +128,38 @@ describe('PhoneNumberInput', () => {
94
128
  it(`'${number}' number should update the value properly`, async () => {
95
129
  await renderAndPaste(number);
96
130
 
97
- expect(getPrefixEl()).toHaveTextContent(countryCode);
98
- expect(getInputEl()).toHaveValue(localNumber);
131
+ expect(getCountryCodeSelect()).toHaveTextContent(countryCode);
132
+ expect(getPhoneNumberInput()).toHaveValue(localNumber);
99
133
  expect(props.onChange).toHaveBeenCalledWith(number.replace(/[\s-]+/g, ''), countryCode);
100
134
  });
101
135
  });
102
136
 
103
137
  it('should not paste invalid characters', async () => {
104
138
  await renderAndPaste('+36asdasdasd');
105
- expect(getPrefixEl()).toHaveTextContent(initialPrefix);
106
- expect(getInputEl()).toHaveValue(initialNumber);
139
+ expect(getCountryCodeSelect()).toHaveTextContent(initialPrefix);
140
+ expect(getPhoneNumberInput()).toHaveValue(initialNumber);
107
141
  expect(props.onChange).not.toHaveBeenCalled();
108
142
  });
109
143
 
110
144
  it('should not paste countries which are not in the select', async () => {
111
145
  await renderAndPaste('+9992342343423');
112
- expect(getPrefixEl()).toHaveTextContent(initialPrefix);
113
- expect(getInputEl()).toHaveValue(initialNumber);
146
+ expect(getCountryCodeSelect()).toHaveTextContent(initialPrefix);
147
+ expect(getPhoneNumberInput()).toHaveValue(initialNumber);
114
148
  expect(props.onChange).not.toHaveBeenCalled();
115
149
  });
116
150
 
117
151
  it("should not paste numbers which doesn't start with the country code", async () => {
118
152
  await renderAndPaste('0+36303932551');
119
- expect(getPrefixEl()).toHaveTextContent(initialPrefix);
120
- expect(getInputEl()).toHaveValue(initialNumber);
153
+ expect(getCountryCodeSelect()).toHaveTextContent(initialPrefix);
154
+ expect(getPhoneNumberInput()).toHaveValue(initialNumber);
121
155
  expect(props.onChange).not.toHaveBeenCalled();
122
156
  });
123
157
 
124
158
  it("should allow pasting numbers which don't contain a country code", async () => {
125
159
  const newNumber = '06303932551';
126
160
  await renderAndPaste(newNumber);
127
- expect(getPrefixEl()).toHaveTextContent(initialPrefix);
128
- expect(getInputEl()).toHaveValue(newNumber);
161
+ expect(getCountryCodeSelect()).toHaveTextContent(initialPrefix);
162
+ expect(getPhoneNumberInput()).toHaveValue(newNumber);
129
163
  expect(props.onChange).toHaveBeenCalledWith(`${initialPrefix}${newNumber}`, initialPrefix);
130
164
  });
131
165
  });
@@ -136,12 +170,12 @@ describe('PhoneNumberInput', () => {
136
170
 
137
171
  it('should set the select to the longest matching prefix', () => {
138
172
  customRender(initialProps);
139
- expect(getPrefixEl()).toHaveTextContent('+1868');
173
+ expect(getCountryCodeSelect()).toHaveTextContent('+1868');
140
174
  });
141
175
 
142
176
  it('should set the number input to the rest of the number', () => {
143
177
  customRender(initialProps);
144
- expect(getInputEl()).toHaveValue('123456789');
178
+ expect(getPhoneNumberInput()).toHaveValue('123456789');
145
179
  });
146
180
  });
147
181
 
@@ -150,20 +184,20 @@ describe('PhoneNumberInput', () => {
150
184
 
151
185
  it('should empty the select', () => {
152
186
  customRender(initialProps);
153
- expect(getPrefixEl()).toHaveTextContent('Select an option...');
187
+ expect(getCountryCodeSelect()).toHaveTextContent('Select an option...');
154
188
  });
155
189
 
156
190
  it('should put the whole value in the input without the plus', () => {
157
191
  customRender(initialProps);
158
- expect(getInputEl()).toHaveValue('999123456789');
192
+ expect(getPhoneNumberInput()).toHaveValue('999123456789');
159
193
  });
160
194
  });
161
195
 
162
196
  describe('when an partial model is supplied (with a matching prefix)', () => {
163
197
  it('should set the select to the matching prefix and put the rest of the number in the suffix', () => {
164
198
  customRender({ initialValue: '+123' });
165
- expect(getPrefixEl()).toHaveTextContent('+1');
166
- expect(getInputEl()).toHaveValue('23');
199
+ expect(getCountryCodeSelect()).toHaveTextContent('+1');
200
+ expect(getPhoneNumberInput()).toHaveValue('23');
167
201
  });
168
202
  });
169
203
  });
@@ -171,8 +205,8 @@ describe('PhoneNumberInput', () => {
171
205
  describe('when disabled is true', () => {
172
206
  it('should disable both controls', () => {
173
207
  customRender({ disabled: true });
174
- expect(getPrefixEl()).toBeDisabled();
175
- expect(getInputEl()).toBeDisabled();
208
+ expect(getCountryCodeSelect()).toBeDisabled();
209
+ expect(getPhoneNumberInput()).toBeDisabled();
176
210
  });
177
211
  });
178
212
 
@@ -180,13 +214,13 @@ describe('PhoneNumberInput', () => {
180
214
  it('should use the provided placeholder', () => {
181
215
  const placeholder = 'custom placeholder';
182
216
  customRender({ placeholder });
183
- expect(getInputEl()).toHaveAttribute('placeholder', placeholder);
217
+ expect(getPhoneNumberInput()).toHaveAttribute('placeholder', placeholder);
184
218
  });
185
219
 
186
220
  it('should use the provided searchPlaceholder', async () => {
187
221
  const searchPlaceholder = 'search placeholder';
188
222
  customRender({ searchPlaceholder });
189
- await userEvent.click(getPrefixEl());
223
+ await userEvent.click(getCountryCodeSelect());
190
224
  expect(screen.getByRole('combobox', { name: searchPlaceholder })).toBeInTheDocument();
191
225
  });
192
226
  });
@@ -195,7 +229,7 @@ describe('PhoneNumberInput', () => {
195
229
  describe('and a value', () => {
196
230
  it('should use the prefix of the supplied value', () => {
197
231
  customRender({ initialValue: '+12345678' }, 'es');
198
- expect(getPrefixEl()).toHaveTextContent('+1');
232
+ expect(getCountryCodeSelect()).toHaveTextContent('+1');
199
233
  });
200
234
  });
201
235
 
@@ -203,14 +237,14 @@ describe('PhoneNumberInput', () => {
203
237
  describe('and no country code', () => {
204
238
  it('should default the prefix to the local country', () => {
205
239
  customRender(undefined, 'es');
206
- expect(getPrefixEl()).toHaveTextContent('+34');
240
+ expect(getCountryCodeSelect()).toHaveTextContent('+34');
207
241
  });
208
242
  });
209
243
 
210
244
  describe('and country code', () => {
211
245
  it('should override locale prefix with country specific prefix', () => {
212
246
  customRender({ countryCode: 'US' }, 'es');
213
- expect(getPrefixEl()).toHaveTextContent('+1');
247
+ expect(getCountryCodeSelect()).toHaveTextContent('+1');
214
248
  });
215
249
  });
216
250
  });
@@ -220,7 +254,7 @@ describe('PhoneNumberInput', () => {
220
254
  describe('valid number', () => {
221
255
  it('should trigger onChange handler', async () => {
222
256
  customRender();
223
- await userEvent.type(getInputEl(), '123');
257
+ await userEvent.type(getPhoneNumberInput(), '123');
224
258
  expect(props.onChange).toHaveBeenCalledWith('+44123', '+44');
225
259
  });
226
260
  });
@@ -228,7 +262,7 @@ describe('PhoneNumberInput', () => {
228
262
  describe('invalid number', () => {
229
263
  it('should trigger onChange with null value', async () => {
230
264
  customRender({ initialValue: '+1234' });
231
- await userEvent.type(getInputEl(), '{Backspace}{Backspace}{Backspace}1');
265
+ await userEvent.type(getPhoneNumberInput(), '{Backspace}{Backspace}{Backspace}1');
232
266
  expect(props.onChange).toHaveBeenCalledWith(null, '+1');
233
267
  });
234
268
  });
@@ -236,7 +270,7 @@ describe('PhoneNumberInput', () => {
236
270
  describe('when user insert invalid character', () => {
237
271
  it('should strip them', async () => {
238
272
  customRender({ initialValue: '+12345678' });
239
- await userEvent.type(getInputEl(), '123--');
273
+ await userEvent.type(getPhoneNumberInput(), '123--');
240
274
  expect(props.onChange).toHaveBeenCalledWith('+12345678123', '+1');
241
275
  });
242
276
  });
@@ -244,8 +278,8 @@ describe('PhoneNumberInput', () => {
244
278
  describe('overlapping prefix and suffix numbers', () => {
245
279
  it("shouldn't change the prefix number on matching suffix input", async () => {
246
280
  customRender({ countryCode: 'eg' });
247
- await userEvent.type(getInputEl(), '1111111');
248
- expect(getInputEl()).toHaveValue('1111111');
281
+ await userEvent.type(getPhoneNumberInput(), '1111111');
282
+ expect(getPhoneNumberInput()).toHaveValue('1111111');
249
283
  expect(props.onChange).toHaveBeenCalledWith('+201111111', '+20');
250
284
  });
251
285
  });
@@ -254,7 +288,7 @@ describe('PhoneNumberInput', () => {
254
288
  describe('when selectProps is supplied', () => {
255
289
  it('renders Select component with expected props', () => {
256
290
  customRender({ selectProps: { className: 'custom-class' } });
257
- expect(getPrefixEl().parentElement).toHaveClass('custom-class');
291
+ expect(getCountryCodeSelect().parentElement).toHaveClass('custom-class');
258
292
  });
259
293
  });
260
294
 
@@ -4,7 +4,6 @@ import { useIntl } from 'react-intl';
4
4
  import { Size, SizeLarge, SizeMedium, SizeSmall } from '../common';
5
5
  import { useInputAttributes } from '../inputs/contexts';
6
6
  import { SelectInput, SelectInputOptionContent, SelectInputProps } from '../inputs/SelectInput';
7
-
8
7
  import messages from './PhoneNumberInput.messages';
9
8
  import countries from './data/countries';
10
9
  import {
@@ -64,6 +63,25 @@ const PhoneNumberInput = ({
64
63
 
65
64
  const { locale, formatMessage } = useIntl();
66
65
 
66
+ const createId = (customID: string | undefined, backup: string): string => {
67
+ if (customID) {
68
+ return customID + (backup ? `-${backup}` : '');
69
+ }
70
+ const random = Math.random().toString(36).slice(2, 8);
71
+ return `${backup}-${random}`;
72
+ };
73
+
74
+ const ids = {
75
+ countryCode: {
76
+ label: createId(id, 'country-code-label'),
77
+ select: createId(id, 'country-code-select'),
78
+ },
79
+ phoneNumber: {
80
+ label: createId(id, 'phone-number-label'),
81
+ input: createId(id, id ? '' : 'phone-number-input'),
82
+ },
83
+ };
84
+
67
85
  const [internalValue, setInternalValue] = useState<PhoneNumber>(() => {
68
86
  const cleanValue = initialValue ? cleanNumber(initialValue) : null;
69
87
 
@@ -151,6 +169,9 @@ const PhoneNumberInput = ({
151
169
  aria-labelledby={ariaLabelledBy}
152
170
  className="tw-telephone"
153
171
  >
172
+ <label className="sr-only" id={ids.countryCode.label}>
173
+ {formatMessage(messages.countryCodeLabel)}
174
+ </label>
154
175
  <div className="tw-telephone__country-select">
155
176
  <SelectInput
156
177
  placeholder={formatMessage(messages.selectInputPlaceholder)}
@@ -181,6 +202,13 @@ const PhoneNumberInput = ({
181
202
  filterPlaceholder={searchPlaceholder}
182
203
  disabled={disabled}
183
204
  size={size}
205
+ id={ids.countryCode.select}
206
+ UNSAFE_triggerButtonProps={{
207
+ id: ids.countryCode.select,
208
+ 'aria-labelledby': ids.countryCode.label,
209
+ 'aria-describedby': undefined,
210
+ 'aria-invalid': undefined,
211
+ }}
184
212
  onChange={(prefix) => {
185
213
  const country = prefix != null ? findCountryByPrefix(prefix) : null;
186
214
  setInternalValue((prev) => ({ ...prev, prefix, format: country?.phoneFormat }));
@@ -193,10 +221,13 @@ const PhoneNumberInput = ({
193
221
  {...selectProps}
194
222
  />
195
223
  </div>
224
+ <label className="sr-only" id={ids.phoneNumber.label} htmlFor={ids.phoneNumber.input}>
225
+ {formatMessage(messages.phoneNumberLabel)}
226
+ </label>
196
227
  <div className="tw-telephone__number-input">
197
228
  <div className={`input-group input-group-${size}`}>
198
229
  <Input
199
- id={id}
230
+ id={ids.phoneNumber.input}
200
231
  autoComplete="tel-national"
201
232
  name="phoneNumber"
202
233
  inputMode="numeric"
@@ -204,6 +235,7 @@ const PhoneNumberInput = ({
204
235
  disabled={disabled}
205
236
  required={required}
206
237
  placeholder={placeholder}
238
+ aria-labelledby={ids.phoneNumber.label}
207
239
  onChange={onSuffixChange}
208
240
  onPaste={onPaste}
209
241
  onFocus={onFocus}
@@ -23,6 +23,7 @@ exports[`PromoCard matches snapshot 1`] = `
23
23
  <img
24
24
  alt="Test Image"
25
25
  class="tw-image tw-image__stretch tw-image__shrink"
26
+ loading="lazy"
26
27
  src="test.jpg"
27
28
  />
28
29
  </div>
@@ -33,6 +33,7 @@ exports[`PromoCardGroup matches snapshot 1`] = `
33
33
  <img
34
34
  alt=""
35
35
  class="tw-image tw-image__stretch tw-image__shrink"
36
+ loading="lazy"
36
37
  src="https://via.placeholder.com/150"
37
38
  />
38
39
  </div>
@@ -67,6 +68,7 @@ exports[`PromoCardGroup matches snapshot 1`] = `
67
68
  <img
68
69
  alt=""
69
70
  class="tw-image tw-image__stretch tw-image__shrink"
71
+ loading="lazy"
70
72
  src="https://via.placeholder.com/150"
71
73
  />
72
74
  </div>
@@ -1,14 +1,10 @@
1
1
  import util from 'node:util';
2
2
  import '@testing-library/jest-dom';
3
3
 
4
- import Adapter from '@cfaester/enzyme-adapter-react-18';
5
- import Enzyme from 'enzyme';
6
4
  import fetchMock from 'jest-fetch-mock';
7
5
 
8
6
  global.fetch = fetchMock as unknown as typeof global.fetch;
9
7
 
10
- Enzyme.configure({ adapter: new Adapter() });
11
-
12
8
  global.requestAnimationFrame = (callback: (time: number) => void): number => {
13
9
  callback(performance.now());
14
10
  return 0;
@@ -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
+ });