@transferwise/components 0.0.0-experimental-e9426b6 → 0.0.0-experimental-ce46fbc

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 (172) hide show
  1. package/build/dateInput/DateInput.js +3 -6
  2. package/build/dateInput/DateInput.js.map +1 -1
  3. package/build/dateInput/DateInput.mjs +2 -5
  4. package/build/dateInput/DateInput.mjs.map +1 -1
  5. package/build/expressiveMoneyInput/currencySelector/CurrencySelector.js +3 -5
  6. package/build/expressiveMoneyInput/currencySelector/CurrencySelector.js.map +1 -1
  7. package/build/expressiveMoneyInput/currencySelector/CurrencySelector.mjs +1 -3
  8. package/build/expressiveMoneyInput/currencySelector/CurrencySelector.mjs.map +1 -1
  9. package/build/index.js +3 -5
  10. package/build/index.js.map +1 -1
  11. package/build/index.mjs +1 -3
  12. package/build/index.mjs.map +1 -1
  13. package/build/inputs/SelectInput.js +821 -0
  14. package/build/inputs/SelectInput.js.map +1 -0
  15. package/build/inputs/SelectInput.messages.js.map +1 -0
  16. package/build/inputs/SelectInput.messages.mjs.map +1 -0
  17. package/build/inputs/SelectInput.mjs +813 -0
  18. package/build/inputs/SelectInput.mjs.map +1 -0
  19. package/build/main.css +47 -47
  20. package/build/moneyInput/MoneyInput.js +2 -5
  21. package/build/moneyInput/MoneyInput.js.map +1 -1
  22. package/build/moneyInput/MoneyInput.mjs +1 -4
  23. package/build/moneyInput/MoneyInput.mjs.map +1 -1
  24. package/build/phoneNumberInput/PhoneNumberInput.js +2 -5
  25. package/build/phoneNumberInput/PhoneNumberInput.js.map +1 -1
  26. package/build/phoneNumberInput/PhoneNumberInput.mjs +1 -4
  27. package/build/phoneNumberInput/PhoneNumberInput.mjs.map +1 -1
  28. package/build/styles/main.css +47 -47
  29. package/build/types/inputs/{SelectInput/SelectInput.types.d.ts → SelectInput.d.ts} +7 -4
  30. package/build/types/inputs/SelectInput.d.ts.map +1 -0
  31. package/build/types/inputs/SelectInput.messages.d.ts.map +1 -0
  32. package/package.json +1 -1
  33. package/src/inputs/{SelectInput/SelectInput.docs.mdx → SelectInput.docs.mdx} +1 -0
  34. package/src/inputs/SelectInput.less +219 -0
  35. package/src/inputs/{SelectInput/SelectInput.story.tsx → SelectInput.story.tsx} +7 -7
  36. package/src/inputs/SelectInput.tsx +1190 -0
  37. package/src/listItem/_stories/ListItem.story.tsx +76 -1
  38. package/src/main.css +47 -47
  39. package/src/main.less +1 -1
  40. package/build/inputs/SelectInput/SelectInput.helpers.js +0 -115
  41. package/build/inputs/SelectInput/SelectInput.helpers.js.map +0 -1
  42. package/build/inputs/SelectInput/SelectInput.helpers.mjs +0 -109
  43. package/build/inputs/SelectInput/SelectInput.helpers.mjs.map +0 -1
  44. package/build/inputs/SelectInput/SelectInput.js +0 -216
  45. package/build/inputs/SelectInput/SelectInput.js.map +0 -1
  46. package/build/inputs/SelectInput/SelectInput.messages.js.map +0 -1
  47. package/build/inputs/SelectInput/SelectInput.messages.mjs.map +0 -1
  48. package/build/inputs/SelectInput/SelectInput.mjs +0 -210
  49. package/build/inputs/SelectInput/SelectInput.mjs.map +0 -1
  50. package/build/inputs/SelectInput/components/SelectInputClearButton/SelectInputClearButton.js +0 -26
  51. package/build/inputs/SelectInput/components/SelectInputClearButton/SelectInputClearButton.js.map +0 -1
  52. package/build/inputs/SelectInput/components/SelectInputClearButton/SelectInputClearButton.mjs +0 -24
  53. package/build/inputs/SelectInput/components/SelectInputClearButton/SelectInputClearButton.mjs.map +0 -1
  54. package/build/inputs/SelectInput/components/SelectInputDefaultTrigger/SelectInputDefaultTrigger.js +0 -54
  55. package/build/inputs/SelectInput/components/SelectInputDefaultTrigger/SelectInputDefaultTrigger.js.map +0 -1
  56. package/build/inputs/SelectInput/components/SelectInputDefaultTrigger/SelectInputDefaultTrigger.mjs +0 -52
  57. package/build/inputs/SelectInput/components/SelectInputDefaultTrigger/SelectInputDefaultTrigger.mjs.map +0 -1
  58. package/build/inputs/SelectInput/components/SelectInputGroupItemView/SelectInputGroupItemView.js +0 -50
  59. package/build/inputs/SelectInput/components/SelectInputGroupItemView/SelectInputGroupItemView.js.map +0 -1
  60. package/build/inputs/SelectInput/components/SelectInputGroupItemView/SelectInputGroupItemView.mjs +0 -48
  61. package/build/inputs/SelectInput/components/SelectInputGroupItemView/SelectInputGroupItemView.mjs.map +0 -1
  62. package/build/inputs/SelectInput/components/SelectInputItemView/SelectInputItemView.js +0 -47
  63. package/build/inputs/SelectInput/components/SelectInputItemView/SelectInputItemView.js.map +0 -1
  64. package/build/inputs/SelectInput/components/SelectInputItemView/SelectInputItemView.mjs +0 -45
  65. package/build/inputs/SelectInput/components/SelectInputItemView/SelectInputItemView.mjs.map +0 -1
  66. package/build/inputs/SelectInput/components/SelectInputOption/SelectInputOption.js +0 -45
  67. package/build/inputs/SelectInput/components/SelectInputOption/SelectInputOption.js.map +0 -1
  68. package/build/inputs/SelectInput/components/SelectInputOption/SelectInputOption.mjs +0 -41
  69. package/build/inputs/SelectInput/components/SelectInputOption/SelectInputOption.mjs.map +0 -1
  70. package/build/inputs/SelectInput/components/SelectInputOptionContent/SelectInputOptionContent.js +0 -41
  71. package/build/inputs/SelectInput/components/SelectInputOptionContent/SelectInputOptionContent.js.map +0 -1
  72. package/build/inputs/SelectInput/components/SelectInputOptionContent/SelectInputOptionContent.mjs +0 -38
  73. package/build/inputs/SelectInput/components/SelectInputOptionContent/SelectInputOptionContent.mjs.map +0 -1
  74. package/build/inputs/SelectInput/components/SelectInputOptions/SelectInputOptions.js +0 -270
  75. package/build/inputs/SelectInput/components/SelectInputOptions/SelectInputOptions.js.map +0 -1
  76. package/build/inputs/SelectInput/components/SelectInputOptions/SelectInputOptions.mjs +0 -268
  77. package/build/inputs/SelectInput/components/SelectInputOptions/SelectInputOptions.mjs.map +0 -1
  78. package/build/inputs/SelectInput/components/SelectInputOptionsContainer/SelectInputOptionsContainer.js +0 -48
  79. package/build/inputs/SelectInput/components/SelectInputOptionsContainer/SelectInputOptionsContainer.js.map +0 -1
  80. package/build/inputs/SelectInput/components/SelectInputOptionsContainer/SelectInputOptionsContainer.mjs +0 -46
  81. package/build/inputs/SelectInput/components/SelectInputOptionsContainer/SelectInputOptionsContainer.mjs.map +0 -1
  82. package/build/inputs/SelectInput/components/SelectInputTriggerButton/SelectInputTriggerButton.js +0 -41
  83. package/build/inputs/SelectInput/components/SelectInputTriggerButton/SelectInputTriggerButton.js.map +0 -1
  84. package/build/inputs/SelectInput/components/SelectInputTriggerButton/SelectInputTriggerButton.mjs +0 -34
  85. package/build/inputs/SelectInput/components/SelectInputTriggerButton/SelectInputTriggerButton.mjs.map +0 -1
  86. package/build/styles/inputs/SelectInput/components/SelectInputDefaultTrigger/SelectInputDefaultTrigger.css +0 -17
  87. package/build/styles/inputs/SelectInput/components/SelectInputItemView/SelectInputItemView.css +0 -16
  88. package/build/styles/inputs/SelectInput/components/SelectInputOption/SelectInputOption.css +0 -33
  89. package/build/styles/inputs/SelectInput/components/SelectInputOptionContent/SelectInputOptionContent.css +0 -37
  90. package/build/types/inputs/SelectInput/SelectInput.d.ts +0 -3
  91. package/build/types/inputs/SelectInput/SelectInput.d.ts.map +0 -1
  92. package/build/types/inputs/SelectInput/SelectInput.helpers.d.ts +0 -28
  93. package/build/types/inputs/SelectInput/SelectInput.helpers.d.ts.map +0 -1
  94. package/build/types/inputs/SelectInput/SelectInput.messages.d.ts.map +0 -1
  95. package/build/types/inputs/SelectInput/SelectInput.types.d.ts.map +0 -1
  96. package/build/types/inputs/SelectInput/components/SelectInputClearButton/SelectInputClearButton.d.ts +0 -5
  97. package/build/types/inputs/SelectInput/components/SelectInputClearButton/SelectInputClearButton.d.ts.map +0 -1
  98. package/build/types/inputs/SelectInput/components/SelectInputClearButton/index.d.ts +0 -2
  99. package/build/types/inputs/SelectInput/components/SelectInputClearButton/index.d.ts.map +0 -1
  100. package/build/types/inputs/SelectInput/components/SelectInputDefaultTrigger/SelectInputDefaultTrigger.d.ts +0 -9
  101. package/build/types/inputs/SelectInput/components/SelectInputDefaultTrigger/SelectInputDefaultTrigger.d.ts.map +0 -1
  102. package/build/types/inputs/SelectInput/components/SelectInputDefaultTrigger/index.d.ts +0 -2
  103. package/build/types/inputs/SelectInput/components/SelectInputDefaultTrigger/index.d.ts.map +0 -1
  104. package/build/types/inputs/SelectInput/components/SelectInputGroupItemView/SelectInputGroupItemView.d.ts +0 -9
  105. package/build/types/inputs/SelectInput/components/SelectInputGroupItemView/SelectInputGroupItemView.d.ts.map +0 -1
  106. package/build/types/inputs/SelectInput/components/SelectInputGroupItemView/index.d.ts +0 -2
  107. package/build/types/inputs/SelectInput/components/SelectInputGroupItemView/index.d.ts.map +0 -1
  108. package/build/types/inputs/SelectInput/components/SelectInputItemView/SelectInputItemView.d.ts +0 -8
  109. package/build/types/inputs/SelectInput/components/SelectInputItemView/SelectInputItemView.d.ts.map +0 -1
  110. package/build/types/inputs/SelectInput/components/SelectInputItemView/index.d.ts +0 -2
  111. package/build/types/inputs/SelectInput/components/SelectInputItemView/index.d.ts.map +0 -1
  112. package/build/types/inputs/SelectInput/components/SelectInputOption/SelectInputOption.d.ts +0 -10
  113. package/build/types/inputs/SelectInput/components/SelectInputOption/SelectInputOption.d.ts.map +0 -1
  114. package/build/types/inputs/SelectInput/components/SelectInputOption/index.d.ts +0 -2
  115. package/build/types/inputs/SelectInput/components/SelectInputOption/index.d.ts.map +0 -1
  116. package/build/types/inputs/SelectInput/components/SelectInputOptionContent/SelectInputOptionContent.d.ts +0 -9
  117. package/build/types/inputs/SelectInput/components/SelectInputOptionContent/SelectInputOptionContent.d.ts.map +0 -1
  118. package/build/types/inputs/SelectInput/components/SelectInputOptionContent/index.d.ts +0 -3
  119. package/build/types/inputs/SelectInput/components/SelectInputOptionContent/index.d.ts.map +0 -1
  120. package/build/types/inputs/SelectInput/components/SelectInputOptions/SelectInputOptions.d.ts +0 -15
  121. package/build/types/inputs/SelectInput/components/SelectInputOptions/SelectInputOptions.d.ts.map +0 -1
  122. package/build/types/inputs/SelectInput/components/SelectInputOptions/index.d.ts +0 -2
  123. package/build/types/inputs/SelectInput/components/SelectInputOptions/index.d.ts.map +0 -1
  124. package/build/types/inputs/SelectInput/components/SelectInputOptionsContainer/SelectInputOptionsContainer.d.ts +0 -6
  125. package/build/types/inputs/SelectInput/components/SelectInputOptionsContainer/SelectInputOptionsContainer.d.ts.map +0 -1
  126. package/build/types/inputs/SelectInput/components/SelectInputOptionsContainer/index.d.ts +0 -2
  127. package/build/types/inputs/SelectInput/components/SelectInputOptionsContainer/index.d.ts.map +0 -1
  128. package/build/types/inputs/SelectInput/components/SelectInputTriggerButton/SelectInputTriggerButton.d.ts +0 -15
  129. package/build/types/inputs/SelectInput/components/SelectInputTriggerButton/SelectInputTriggerButton.d.ts.map +0 -1
  130. package/build/types/inputs/SelectInput/components/SelectInputTriggerButton/index.d.ts +0 -3
  131. package/build/types/inputs/SelectInput/components/SelectInputTriggerButton/index.d.ts.map +0 -1
  132. package/build/types/inputs/SelectInput/index.d.ts +0 -5
  133. package/build/types/inputs/SelectInput/index.d.ts.map +0 -1
  134. package/src/inputs/SelectInput/SelectInput.helpers.ts +0 -152
  135. package/src/inputs/SelectInput/SelectInput.less +0 -42
  136. package/src/inputs/SelectInput/SelectInput.test.tsx +0 -606
  137. package/src/inputs/SelectInput/SelectInput.tsx +0 -247
  138. package/src/inputs/SelectInput/SelectInput.types.ts +0 -114
  139. package/src/inputs/SelectInput/components/SelectInputClearButton/SelectInputClearButton.tsx +0 -25
  140. package/src/inputs/SelectInput/components/SelectInputClearButton/index.ts +0 -1
  141. package/src/inputs/SelectInput/components/SelectInputDefaultTrigger/SelectInputDefaultTrigger.css +0 -17
  142. package/src/inputs/SelectInput/components/SelectInputDefaultTrigger/SelectInputDefaultTrigger.less +0 -15
  143. package/src/inputs/SelectInput/components/SelectInputDefaultTrigger/SelectInputDefaultTrigger.tsx +0 -56
  144. package/src/inputs/SelectInput/components/SelectInputDefaultTrigger/index.ts +0 -1
  145. package/src/inputs/SelectInput/components/SelectInputGroupItemView/SelectInputGroupItemView.tsx +0 -64
  146. package/src/inputs/SelectInput/components/SelectInputGroupItemView/index.ts +0 -1
  147. package/src/inputs/SelectInput/components/SelectInputItemView/SelectInputItemView.css +0 -16
  148. package/src/inputs/SelectInput/components/SelectInputItemView/SelectInputItemView.less +0 -17
  149. package/src/inputs/SelectInput/components/SelectInputItemView/SelectInputItemView.tsx +0 -55
  150. package/src/inputs/SelectInput/components/SelectInputItemView/index.ts +0 -1
  151. package/src/inputs/SelectInput/components/SelectInputOption/SelectInputOption.css +0 -33
  152. package/src/inputs/SelectInput/components/SelectInputOption/SelectInputOption.less +0 -32
  153. package/src/inputs/SelectInput/components/SelectInputOption/SelectInputOption.tsx +0 -51
  154. package/src/inputs/SelectInput/components/SelectInputOption/index.ts +0 -5
  155. package/src/inputs/SelectInput/components/SelectInputOptionContent/SelectInputOptionContent.css +0 -37
  156. package/src/inputs/SelectInput/components/SelectInputOptionContent/SelectInputOptionContent.less +0 -38
  157. package/src/inputs/SelectInput/components/SelectInputOptionContent/SelectInputOptionContent.tsx +0 -67
  158. package/src/inputs/SelectInput/components/SelectInputOptionContent/index.ts +0 -5
  159. package/src/inputs/SelectInput/components/SelectInputOptions/SelectInputOptions.less +0 -75
  160. package/src/inputs/SelectInput/components/SelectInputOptions/SelectInputOptions.tsx +0 -369
  161. package/src/inputs/SelectInput/components/SelectInputOptions/index.ts +0 -1
  162. package/src/inputs/SelectInput/components/SelectInputOptionsContainer/SelectInputOptionsContainer.tsx +0 -56
  163. package/src/inputs/SelectInput/components/SelectInputOptionsContainer/index.ts +0 -1
  164. package/src/inputs/SelectInput/components/SelectInputTriggerButton/SelectInputTriggerButton.tsx +0 -39
  165. package/src/inputs/SelectInput/components/SelectInputTriggerButton/index.ts +0 -5
  166. package/src/inputs/SelectInput/index.ts +0 -13
  167. package/build/inputs/{SelectInput/SelectInput.messages.js → SelectInput.messages.js} +0 -0
  168. package/build/inputs/{SelectInput/SelectInput.messages.mjs → SelectInput.messages.mjs} +0 -0
  169. package/{src/inputs/SelectInput → build/styles/inputs}/SelectInput.css +47 -47
  170. package/build/types/inputs/{SelectInput/SelectInput.messages.d.ts → SelectInput.messages.d.ts} +0 -0
  171. package/{build/styles/inputs/SelectInput → src/inputs}/SelectInput.css +47 -47
  172. /package/src/inputs/{SelectInput/SelectInput.messages.ts → SelectInput.messages.ts} +0 -0
@@ -1,606 +0,0 @@
1
- import { screen, waitFor, within } from '@testing-library/react';
2
- import { userEvent } from '@testing-library/user-event';
3
- import { mockAnimationsApi } from 'jsdom-testing-mocks';
4
-
5
- import { render, mockMatchMedia, mockResizeObserver } from '../../test-utils';
6
-
7
- import { SelectInput, type SelectInputOptionItem, type SelectInputProps } from '.';
8
- import { Field } from '../../field/Field';
9
-
10
- mockMatchMedia();
11
- mockResizeObserver();
12
- mockAnimationsApi();
13
-
14
- describe('SelectInput', () => {
15
- it('renders placeholder', () => {
16
- render(
17
- <SelectInput
18
- placeholder="Currency"
19
- items={[
20
- { type: 'option', value: 'USD' },
21
- { type: 'option', value: 'EUR' },
22
- ]}
23
- />,
24
- );
25
-
26
- expect(screen.getByText('Currency')).toBeInTheDocument();
27
- });
28
-
29
- it('renders footer', async () => {
30
- render(
31
- <SelectInput
32
- items={[
33
- { type: 'option', value: 'USD' },
34
- { type: 'option', value: 'EUR' },
35
- ]}
36
- renderFooter={({ queryNormalized: normalizedQuery }) =>
37
- normalizedQuery != null ? (
38
- <>Showing results for ‘{normalizedQuery}’</>
39
- ) : (
40
- <>All items shown</>
41
- )
42
- }
43
- filterable
44
- />,
45
- );
46
-
47
- await userEvent.tab();
48
- await userEvent.keyboard(' ');
49
-
50
- const footer = screen.getByText('All items shown');
51
- expect(footer).toBeInTheDocument();
52
-
53
- await userEvent.keyboard('u');
54
- expect(footer).toHaveTextContent(/‘u’$/);
55
-
56
- await userEvent.keyboard('r');
57
- expect(footer).toHaveTextContent(/‘ur’$/);
58
-
59
- await userEvent.keyboard('x');
60
- expect(footer).toHaveTextContent(/‘urx’$/);
61
-
62
- await userEvent.keyboard('{Backspace}');
63
- expect(footer).toHaveTextContent(/‘ur’$/);
64
- });
65
-
66
- it('allows navigating the listbox with cursors', async () => {
67
- render(
68
- <SelectInput
69
- items={[
70
- { type: 'option', value: 'GBP' },
71
- { type: 'option', value: 'EUR' },
72
- { type: 'option', value: 'USD' },
73
- ]}
74
- renderFooter={({ queryNormalized: normalizedQuery }) => (
75
- <button type="button">Footer button</button>
76
- )}
77
- filterable
78
- />,
79
- );
80
-
81
- // opened the dropbox, with search focused
82
- await userEvent.tab();
83
- await userEvent.keyboard(' ');
84
- expect(screen.getByRole('combobox')).toHaveFocus();
85
-
86
- // search still focused but listbox can be navigated via keyboard
87
- await userEvent.keyboard('{ArrowDown}');
88
- expect(screen.getByRole('combobox')).toHaveFocus();
89
- expect(screen.getByRole('option', { name: 'EUR' })).toHaveClass(
90
- 'np-select-input-option-container--active',
91
- );
92
-
93
- // tab moves focus to listbox
94
- await userEvent.tab();
95
- expect(screen.getByRole('listbox')).toHaveFocus();
96
- expect(screen.getByRole('combobox')).not.toHaveFocus();
97
-
98
- // arrows still navigate within listbox
99
- await userEvent.keyboard('{ArrowDown}');
100
- expect(screen.getByRole('option', { name: 'USD' })).toHaveClass(
101
- 'np-select-input-option-container--active',
102
- );
103
-
104
- // tab moves focus to footer but highlighted option is retained
105
- await userEvent.tab();
106
- expect(screen.getByRole('listbox')).not.toHaveFocus();
107
- expect(screen.getByRole('combobox')).not.toHaveFocus();
108
- expect(screen.getByText('Footer button')).toHaveFocus();
109
- expect(screen.getByRole('option', { name: 'USD' })).toHaveClass(
110
- 'np-select-input-option-container--active',
111
- );
112
-
113
- // shift+tab moves focus back to listbox
114
- await userEvent.tab({ shift: true });
115
- expect(screen.getByRole('listbox')).toHaveFocus();
116
- expect(screen.getByRole('combobox')).not.toHaveFocus();
117
- expect(screen.getByText('Footer button')).not.toHaveFocus();
118
-
119
- // previously highlighted option is still active within listbox
120
- expect(screen.getByRole('option', { name: 'USD' })).toHaveClass(
121
- 'np-select-input-option-container--active',
122
- );
123
-
124
- // arrows continue to navigate within listbox
125
- await userEvent.keyboard('{ArrowUp}');
126
- expect(screen.getByRole('option', { name: 'EUR' })).toHaveClass(
127
- 'np-select-input-option-container--active',
128
- );
129
-
130
- // shift+tab moves focus back to search input
131
- await userEvent.tab({ shift: true });
132
- expect(screen.getByRole('combobox')).toHaveFocus();
133
-
134
- // arrows continue to navigate within listbox
135
- await userEvent.keyboard('{ArrowUp}');
136
- expect(screen.getByRole('option', { name: 'GBP' })).toHaveClass(
137
- 'np-select-input-option-container--active',
138
- );
139
- });
140
-
141
- it('shows item selected via mouse', async () => {
142
- const handleClose = jest.fn();
143
-
144
- render(
145
- <SelectInput
146
- items={[
147
- { type: 'option', value: 'USD' },
148
- { type: 'option', value: 'EUR' },
149
- ]}
150
- onClose={handleClose}
151
- />,
152
- );
153
-
154
- expect(screen.queryByText('EUR')).not.toBeInTheDocument();
155
-
156
- const trigger = screen.getByRole('combobox');
157
- await userEvent.click(trigger);
158
-
159
- expect(handleClose).not.toHaveBeenCalled();
160
-
161
- const listbox = screen.getByRole('listbox');
162
- const option = within(listbox).getByRole('option', { name: 'EUR' });
163
- await userEvent.click(option);
164
-
165
- expect(handleClose).toHaveBeenCalledTimes(1);
166
- expect(trigger).toHaveTextContent('EUR');
167
- });
168
-
169
- it('filters items via keyboard', async () => {
170
- const handleClose = jest.fn();
171
-
172
- render(
173
- <SelectInput
174
- items={[
175
- {
176
- type: 'group',
177
- label: 'Popular currencies',
178
- options: [
179
- { type: 'option', value: 'USD' },
180
- { type: 'option', value: 'EUR' },
181
- { type: 'option', value: 'GBP' },
182
- ],
183
- },
184
- ]}
185
- filterable
186
- onClose={handleClose}
187
- />,
188
- );
189
-
190
- const trigger = screen.getByRole('combobox');
191
- await userEvent.tab();
192
- await userEvent.keyboard(' ');
193
-
194
- expect(handleClose).not.toHaveBeenCalled();
195
-
196
- const listbox = screen.getByRole('listbox');
197
- expect(within(listbox).getAllByRole('option')).toHaveLength(3);
198
-
199
- await userEvent.keyboard('u');
200
- expect(within(listbox).getAllByRole('option')).toHaveLength(2);
201
-
202
- await userEvent.keyboard('r');
203
- expect(within(listbox).getByRole('option')).toBeInTheDocument();
204
-
205
- await userEvent.keyboard('x');
206
- expect(within(listbox).queryByRole('option')).not.toBeInTheDocument();
207
-
208
- await userEvent.keyboard('{Backspace}');
209
- expect(within(listbox).getByRole('option')).toBeInTheDocument();
210
-
211
- const option = within(listbox).getAllByRole('option')[0];
212
- await userEvent.click(option);
213
-
214
- expect(handleClose).toHaveBeenCalledTimes(1);
215
- expect(trigger).toHaveTextContent('EUR');
216
- });
217
-
218
- it('clears filter query on close', async () => {
219
- const handleFilterChange = jest.fn();
220
-
221
- render(
222
- <SelectInput
223
- items={[
224
- { type: 'option', value: 'USD' },
225
- { type: 'option', value: 'EUR' },
226
- ]}
227
- filterable
228
- onFilterChange={handleFilterChange}
229
- />,
230
- );
231
-
232
- const trigger = screen.getByRole('combobox');
233
- await userEvent.tab();
234
- await userEvent.keyboard(' ');
235
-
236
- expect(handleFilterChange).not.toHaveBeenCalled();
237
-
238
- await userEvent.keyboard(' x');
239
- expect(handleFilterChange).toHaveBeenLastCalledWith({
240
- query: ' x',
241
- queryNormalized: 'x',
242
- });
243
-
244
- await userEvent.keyboard('{Escape}');
245
- await waitFor(() => {
246
- expect(handleFilterChange).toHaveBeenLastCalledWith({
247
- query: '',
248
- queryNormalized: null,
249
- });
250
- });
251
-
252
- await userEvent.click(trigger);
253
-
254
- const listbox = screen.getByRole('listbox');
255
- expect(within(listbox).getAllByRole('option')).toHaveLength(2);
256
- });
257
-
258
- it('filters items ignoring diacritics/accents', async () => {
259
- render(
260
- <SelectInput
261
- items={[
262
- { type: 'option', value: 'AX', filterMatchers: ['Åland Islands'] },
263
- { type: 'option', value: 'AL', filterMatchers: ['Albania'] },
264
- { type: 'option', value: 'DZ', filterMatchers: ['Algeria'] },
265
- { type: 'option', value: 'RE', filterMatchers: ['Réunion'] },
266
- ]}
267
- filterable
268
- />,
269
- );
270
-
271
- await userEvent.tab();
272
- await userEvent.keyboard(' ');
273
-
274
- const listbox = screen.getByRole('listbox');
275
- expect(within(listbox).getAllByRole('option')).toHaveLength(4);
276
-
277
- await userEvent.keyboard('aland');
278
- expect(within(listbox).getAllByRole('option')).toHaveLength(1);
279
- expect(within(listbox).getByRole('option')).toHaveTextContent('AX');
280
-
281
- const searchInput = screen.getByRole('combobox', { expanded: true });
282
- await userEvent.clear(searchInput);
283
- await userEvent.keyboard('reunion');
284
- expect(within(listbox).getAllByRole('option')).toHaveLength(1);
285
- expect(within(listbox).getByRole('option')).toHaveTextContent('RE');
286
-
287
- await userEvent.clear(searchInput);
288
- await userEvent.keyboard('Åland');
289
- expect(within(listbox).getAllByRole('option')).toHaveLength(1);
290
- expect(within(listbox).getByRole('option')).toHaveTextContent('AX');
291
-
292
- await userEvent.clear(searchInput);
293
- await userEvent.keyboard('Rèunion');
294
- expect(within(listbox).getAllByRole('option')).toHaveLength(1);
295
- expect(within(listbox).getByRole('option')).toHaveTextContent('RE');
296
- });
297
-
298
- it('selects multiple options', async () => {
299
- render(
300
- <SelectInput
301
- multiple
302
- items={[
303
- { type: 'option', value: 'USD' },
304
- { type: 'option', value: 'EUR' },
305
- ]}
306
- />,
307
- );
308
-
309
- const trigger = screen.getByRole('combobox');
310
- await userEvent.click(trigger);
311
-
312
- const listbox = screen.getByRole('listbox');
313
- const options = within(listbox).getAllByRole('option');
314
- for (const option of options) {
315
- await userEvent.click(option);
316
- }
317
-
318
- expect(trigger).toHaveTextContent('USD, EUR');
319
- });
320
-
321
- it('supports custom `id` attribute', () => {
322
- render(<SelectInput id="custom" items={[]} />);
323
-
324
- const trigger = screen.getByRole('combobox');
325
- expect(trigger).toHaveAttribute('id', 'custom');
326
- });
327
-
328
- it('supports `Field` for labeling', () => {
329
- render(
330
- <Field label="Currency">
331
- <SelectInput items={[{ type: 'option', value: 'USD' }]} value="USD" />
332
- </Field>,
333
- );
334
- expect(screen.getByLabelText(/Currency/)).toHaveAttribute('aria-haspopup');
335
- });
336
-
337
- it('deduplicates search results across groups using compareValues as key', async () => {
338
- interface Currency {
339
- code: string;
340
- name: string;
341
- }
342
-
343
- const usdInGroup1: Currency = { code: 'USD', name: 'US Dollar' };
344
- const usdInGroup2: Currency = { code: 'USD', name: 'US Dollar' };
345
- const eur: Currency = { code: 'EUR', name: 'Euro' };
346
- const gbp: Currency = { code: 'GBP', name: 'British Pound' };
347
-
348
- render(
349
- <SelectInput<Currency>
350
- items={[
351
- {
352
- type: 'group',
353
- label: 'Popular',
354
- options: [
355
- { type: 'option', value: usdInGroup1 },
356
- { type: 'option', value: eur },
357
- ],
358
- },
359
- {
360
- type: 'group',
361
- label: 'All currencies',
362
- options: [
363
- { type: 'option', value: usdInGroup2 },
364
- { type: 'option', value: gbp },
365
- ],
366
- },
367
- ]}
368
- compareValues="code"
369
- renderValue={(currency) => currency.name}
370
- filterable
371
- />,
372
- );
373
-
374
- const trigger = screen.getByRole('combobox');
375
- await userEvent.click(trigger);
376
-
377
- const listbox = screen.getByRole('listbox');
378
-
379
- // Before filtering, should show all 4 options (no deduplication yet)
380
- let options = within(listbox).getAllByRole('option');
381
- expect(options).toHaveLength(4);
382
-
383
- const usdOptions = within(listbox).getAllByText('US Dollar');
384
- expect(usdOptions).toHaveLength(2);
385
-
386
- // Start filtering - type a search query to trigger deduplication
387
- const searchInput = screen.getByRole('combobox', { expanded: true });
388
- await userEvent.type(searchInput, 'u');
389
-
390
- // After filtering with 'u', should show 3 unique options (USD deduplicated, EUR, GBP)
391
- options = within(listbox).getAllByRole('option');
392
- expect(options).toHaveLength(3);
393
- expect(within(listbox).getByText('Euro')).toBeInTheDocument();
394
- expect(within(listbox).getByText('British Pound')).toBeInTheDocument();
395
-
396
- // Filter more specifically for 'dollar'
397
- await userEvent.clear(searchInput);
398
- await userEvent.type(searchInput, 'dollar');
399
-
400
- const filteredOptions = within(listbox).getAllByRole('option');
401
- // Should only show 1 USD option, not 2
402
- expect(filteredOptions).toHaveLength(1);
403
- expect(within(listbox).getByText('US Dollar')).toBeInTheDocument();
404
- });
405
-
406
- it('deduplicates search results across groups using compareValues as function', async () => {
407
- interface Item {
408
- id: number;
409
- label: string;
410
- }
411
-
412
- const item1Group1: Item = { id: 1, label: 'Item One' };
413
- const item2Group1: Item = { id: 2, label: 'Item Two' };
414
-
415
- const item1Group2: Item = { id: 1, label: 'Item One' };
416
-
417
- render(
418
- <SelectInput<Item>
419
- items={[
420
- {
421
- type: 'group',
422
- label: 'Group A',
423
- options: [
424
- { type: 'option', value: item1Group1 },
425
- { type: 'option', value: item2Group1 },
426
- ],
427
- },
428
- {
429
- type: 'group',
430
- label: 'Group B',
431
- options: [{ type: 'option', value: item1Group2 }],
432
- },
433
- ]}
434
- compareValues={(a, b) => a?.id === b?.id}
435
- renderValue={(item) => item.label}
436
- filterable
437
- />,
438
- );
439
-
440
- const trigger = screen.getByRole('combobox');
441
- await userEvent.click(trigger);
442
-
443
- const listbox = screen.getByRole('listbox');
444
-
445
- // Before filtering, should show all 3 options (no deduplication yet)
446
- let options = within(listbox).getAllByRole('option');
447
- expect(options).toHaveLength(3);
448
-
449
- // Start filtering - type a search query to trigger deduplication
450
- const searchInput = screen.getByRole('combobox', { expanded: true });
451
- await userEvent.type(searchInput, 'item');
452
-
453
- // After filtering, should show 2 unique options (item with id:1 deduplicated, item with id:2)
454
- options = within(listbox).getAllByRole('option');
455
- expect(options).toHaveLength(2);
456
- expect(within(listbox).getByText('Item One')).toBeInTheDocument();
457
- expect(within(listbox).getByText('Item Two')).toBeInTheDocument();
458
- });
459
-
460
- it('sorts filtered options using sortFilteredOptions prop', async () => {
461
- interface Country {
462
- code: string;
463
- name: string;
464
- keywords: string[];
465
- }
466
-
467
- const countries: Country[] = [
468
- { code: 'AD', name: 'Andorra', keywords: ['united states dollar'] },
469
- { code: 'DE', name: 'Germany', keywords: ['EUR'] },
470
- { code: 'US', name: 'United States', keywords: ['United States dollar', 'USD'] },
471
- { code: 'ZM', name: 'Zambia', keywords: ['USD', 'united states dollar'] },
472
- ];
473
-
474
- render(
475
- <SelectInput<Country>
476
- items={countries.map((country) => ({
477
- type: 'option',
478
- value: country,
479
- filterMatchers: country.keywords,
480
- }))}
481
- renderValue={(country) => country.name}
482
- filterable
483
- sortFilteredOptions={(a, b, searchQuery) => {
484
- const query = searchQuery.toLowerCase();
485
- const nameA = a.value.name.toLowerCase();
486
- const nameB = b.value.name.toLowerCase();
487
-
488
- const aMatch = nameA.includes(query);
489
- const bMatch = nameB.includes(query);
490
-
491
- if (aMatch && !bMatch) return -1;
492
- if (!aMatch && bMatch) return 1;
493
-
494
- return nameA.localeCompare(nameB);
495
- }}
496
- />,
497
- );
498
-
499
- const trigger = screen.getByRole('combobox');
500
- await userEvent.click(trigger);
501
-
502
- const searchInput = screen.getByRole('combobox', { expanded: true });
503
- await userEvent.type(searchInput, 'united');
504
-
505
- const listbox = screen.getByRole('listbox');
506
- const options = within(listbox).getAllByRole('option');
507
-
508
- expect(options).toHaveLength(3);
509
- expect(options[0]).toHaveTextContent('United States');
510
- expect(options[1]).toHaveTextContent('Andorra');
511
- expect(options[2]).toHaveTextContent('Zambia');
512
- });
513
-
514
- describe('listbox label', () => {
515
- const fieldLabel = 'Fruits';
516
- const triggerLabel = 'Select fruit';
517
- const options: SelectInputOptionItem[] = [
518
- { type: 'option', value: 'Banana' },
519
- { type: 'option', value: 'Orange' },
520
- { type: 'option', value: 'Olive' },
521
- ];
522
- const requiredTriggerButtonProps = {
523
- id: undefined,
524
- 'aria-labelledby': undefined,
525
- 'aria-describedby': undefined,
526
- 'aria-invalid': undefined,
527
- 'aria-label': undefined,
528
- };
529
-
530
- const renderSelectInput = (props: Omit<SelectInputProps<string | null>, 'items'> = {}) =>
531
- render(
532
- <Field label={fieldLabel} id="selectId">
533
- <SelectInput {...props} items={options} />
534
- </Field>,
535
- );
536
-
537
- it("should propagate trigger's label if nothing is selected", async () => {
538
- renderSelectInput({
539
- UNSAFE_triggerButtonProps: {
540
- ...requiredTriggerButtonProps,
541
- 'aria-label': triggerLabel,
542
- },
543
- });
544
- const trigger = screen.getByRole('combobox');
545
- await userEvent.click(trigger);
546
- expect(screen.getByRole('listbox', { name: triggerLabel })).toBeInTheDocument();
547
- });
548
-
549
- it("should propagate trigger's label if an option is selected", async () => {
550
- renderSelectInput({
551
- UNSAFE_triggerButtonProps: {
552
- ...requiredTriggerButtonProps,
553
- 'aria-label': triggerLabel,
554
- },
555
- value: options[1].value,
556
- });
557
- const trigger = screen.getByRole('combobox');
558
- await userEvent.click(trigger);
559
- expect(screen.getByRole('listbox', { name: triggerLabel })).toBeInTheDocument();
560
- });
561
-
562
- it("should propagate trigger's label by id", async () => {
563
- const customLabelId = 'customLabelId';
564
- renderSelectInput({
565
- UNSAFE_triggerButtonProps: {
566
- ...requiredTriggerButtonProps,
567
- 'aria-labelledby': customLabelId,
568
- },
569
- });
570
- const trigger = screen.getByRole('combobox');
571
- await userEvent.click(trigger);
572
- expect(screen.getByRole('listbox')).toHaveAttribute('aria-labelledby', customLabelId);
573
- });
574
-
575
- it("should propagate input's label by id", async () => {
576
- renderSelectInput();
577
- const trigger = screen.getByRole('combobox');
578
- await userEvent.click(trigger);
579
- expect(screen.getByRole('listbox', { name: fieldLabel })).toBeInTheDocument();
580
- });
581
-
582
- it('should prefer explicit label over label ids', async () => {
583
- const customLabelId = 'customLabelId';
584
- renderSelectInput({
585
- UNSAFE_triggerButtonProps: {
586
- ...requiredTriggerButtonProps,
587
- 'aria-labelledby': customLabelId,
588
- 'aria-label': triggerLabel,
589
- },
590
- });
591
- const trigger = screen.getByRole('combobox');
592
- await userEvent.click(trigger);
593
- expect(screen.getByRole('listbox', { name: triggerLabel })).toBeInTheDocument();
594
- expect(screen.getByRole('listbox')).not.toHaveAttribute('aria-labelledby');
595
- });
596
-
597
- it('should have no label if none of the above are provided', async () => {
598
- render(<SelectInput items={options} />);
599
- const trigger = screen.getByRole('combobox');
600
- await userEvent.click(trigger);
601
- const listBox = screen.getByRole('listbox');
602
- expect(listBox).not.toHaveAttribute('aria-label');
603
- expect(listBox).not.toHaveAttribute('aria-labelledby');
604
- });
605
- });
606
- });