@transferwise/components 0.0.0-experimental-bcfa03a → 0.0.0-experimental-5cd0315

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 (180) 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 +840 -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 +832 -0
  18. package/build/inputs/SelectInput.mjs.map +1 -0
  19. package/build/main.css +70 -65
  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/inputs/{SelectInput/SelectInput.css → SelectInput.css} +70 -65
  29. package/build/styles/main.css +70 -65
  30. package/build/types/inputs/{SelectInput/SelectInput.types.d.ts → SelectInput.d.ts} +7 -10
  31. package/build/types/inputs/SelectInput.d.ts.map +1 -0
  32. package/build/types/inputs/SelectInput.messages.d.ts.map +1 -0
  33. package/package.json +2 -2
  34. package/src/inputs/{SelectInput/SelectInput.css → SelectInput.css} +70 -65
  35. package/src/inputs/{SelectInput/SelectInput.docs.mdx → SelectInput.docs.mdx} +1 -0
  36. package/src/inputs/SelectInput.less +219 -0
  37. package/src/inputs/{SelectInput/SelectInput.story.tsx → SelectInput.story.tsx} +7 -7
  38. package/src/inputs/SelectInput.tsx +1209 -0
  39. package/src/main.css +70 -65
  40. package/src/main.less +1 -1
  41. package/build/inputs/SelectInput/DefaultTrigger/ClearButton/SelectInputClearButton.js +0 -26
  42. package/build/inputs/SelectInput/DefaultTrigger/ClearButton/SelectInputClearButton.js.map +0 -1
  43. package/build/inputs/SelectInput/DefaultTrigger/ClearButton/SelectInputClearButton.mjs +0 -24
  44. package/build/inputs/SelectInput/DefaultTrigger/ClearButton/SelectInputClearButton.mjs.map +0 -1
  45. package/build/inputs/SelectInput/DefaultTrigger/SelectInputDefaultTrigger.js +0 -54
  46. package/build/inputs/SelectInput/DefaultTrigger/SelectInputDefaultTrigger.js.map +0 -1
  47. package/build/inputs/SelectInput/DefaultTrigger/SelectInputDefaultTrigger.mjs +0 -52
  48. package/build/inputs/SelectInput/DefaultTrigger/SelectInputDefaultTrigger.mjs.map +0 -1
  49. package/build/inputs/SelectInput/OptionContent/SelectInputOptionContent.js +0 -41
  50. package/build/inputs/SelectInput/OptionContent/SelectInputOptionContent.js.map +0 -1
  51. package/build/inputs/SelectInput/OptionContent/SelectInputOptionContent.mjs +0 -38
  52. package/build/inputs/SelectInput/OptionContent/SelectInputOptionContent.mjs.map +0 -1
  53. package/build/inputs/SelectInput/Options/GroupItemView/SelectInputGroupItemView.js +0 -50
  54. package/build/inputs/SelectInput/Options/GroupItemView/SelectInputGroupItemView.js.map +0 -1
  55. package/build/inputs/SelectInput/Options/GroupItemView/SelectInputGroupItemView.mjs +0 -48
  56. package/build/inputs/SelectInput/Options/GroupItemView/SelectInputGroupItemView.mjs.map +0 -1
  57. package/build/inputs/SelectInput/Options/ItemView/Option/SelectInputOption.js +0 -45
  58. package/build/inputs/SelectInput/Options/ItemView/Option/SelectInputOption.js.map +0 -1
  59. package/build/inputs/SelectInput/Options/ItemView/Option/SelectInputOption.mjs +0 -41
  60. package/build/inputs/SelectInput/Options/ItemView/Option/SelectInputOption.mjs.map +0 -1
  61. package/build/inputs/SelectInput/Options/ItemView/SelectInputItemView.js +0 -50
  62. package/build/inputs/SelectInput/Options/ItemView/SelectInputItemView.js.map +0 -1
  63. package/build/inputs/SelectInput/Options/ItemView/SelectInputItemView.mjs +0 -48
  64. package/build/inputs/SelectInput/Options/ItemView/SelectInputItemView.mjs.map +0 -1
  65. package/build/inputs/SelectInput/Options/OptionsContainer/SelectInputOptionsContainer.js +0 -48
  66. package/build/inputs/SelectInput/Options/OptionsContainer/SelectInputOptionsContainer.js.map +0 -1
  67. package/build/inputs/SelectInput/Options/OptionsContainer/SelectInputOptionsContainer.mjs +0 -46
  68. package/build/inputs/SelectInput/Options/OptionsContainer/SelectInputOptionsContainer.mjs.map +0 -1
  69. package/build/inputs/SelectInput/Options/SelectInputOptions.js +0 -270
  70. package/build/inputs/SelectInput/Options/SelectInputOptions.js.map +0 -1
  71. package/build/inputs/SelectInput/Options/SelectInputOptions.mjs +0 -268
  72. package/build/inputs/SelectInput/Options/SelectInputOptions.mjs.map +0 -1
  73. package/build/inputs/SelectInput/SelectInput.constants.js +0 -6
  74. package/build/inputs/SelectInput/SelectInput.constants.js.map +0 -1
  75. package/build/inputs/SelectInput/SelectInput.constants.mjs +0 -4
  76. package/build/inputs/SelectInput/SelectInput.constants.mjs.map +0 -1
  77. package/build/inputs/SelectInput/SelectInput.helpers.js +0 -115
  78. package/build/inputs/SelectInput/SelectInput.helpers.js.map +0 -1
  79. package/build/inputs/SelectInput/SelectInput.helpers.mjs +0 -109
  80. package/build/inputs/SelectInput/SelectInput.helpers.mjs.map +0 -1
  81. package/build/inputs/SelectInput/SelectInput.js +0 -216
  82. package/build/inputs/SelectInput/SelectInput.js.map +0 -1
  83. package/build/inputs/SelectInput/SelectInput.messages.js.map +0 -1
  84. package/build/inputs/SelectInput/SelectInput.messages.mjs.map +0 -1
  85. package/build/inputs/SelectInput/SelectInput.mjs +0 -210
  86. package/build/inputs/SelectInput/SelectInput.mjs.map +0 -1
  87. package/build/inputs/SelectInput/TriggerButton/SelectInputTriggerButton.js +0 -41
  88. package/build/inputs/SelectInput/TriggerButton/SelectInputTriggerButton.js.map +0 -1
  89. package/build/inputs/SelectInput/TriggerButton/SelectInputTriggerButton.mjs +0 -34
  90. package/build/inputs/SelectInput/TriggerButton/SelectInputTriggerButton.mjs.map +0 -1
  91. package/build/styles/inputs/SelectInput/DefaultTrigger/SelectInputDefaultTrigger.css +0 -17
  92. package/build/styles/inputs/SelectInput/OptionContent/SelectInputOptionContent.css +0 -37
  93. package/build/styles/inputs/SelectInput/Options/ItemView/Option/SelectInputOption.css +0 -33
  94. package/build/styles/inputs/SelectInput/Options/ItemView/SelectInputItemView.css +0 -44
  95. package/build/styles/inputs/SelectInput/Options/SelectInputOptions.css +0 -125
  96. package/build/types/inputs/SelectInput/DefaultTrigger/ClearButton/SelectInputClearButton.d.ts +0 -5
  97. package/build/types/inputs/SelectInput/DefaultTrigger/ClearButton/SelectInputClearButton.d.ts.map +0 -1
  98. package/build/types/inputs/SelectInput/DefaultTrigger/ClearButton/index.d.ts +0 -2
  99. package/build/types/inputs/SelectInput/DefaultTrigger/ClearButton/index.d.ts.map +0 -1
  100. package/build/types/inputs/SelectInput/DefaultTrigger/SelectInputDefaultTrigger.d.ts +0 -9
  101. package/build/types/inputs/SelectInput/DefaultTrigger/SelectInputDefaultTrigger.d.ts.map +0 -1
  102. package/build/types/inputs/SelectInput/DefaultTrigger/index.d.ts +0 -2
  103. package/build/types/inputs/SelectInput/DefaultTrigger/index.d.ts.map +0 -1
  104. package/build/types/inputs/SelectInput/OptionContent/SelectInputOptionContent.d.ts +0 -9
  105. package/build/types/inputs/SelectInput/OptionContent/SelectInputOptionContent.d.ts.map +0 -1
  106. package/build/types/inputs/SelectInput/OptionContent/index.d.ts +0 -3
  107. package/build/types/inputs/SelectInput/OptionContent/index.d.ts.map +0 -1
  108. package/build/types/inputs/SelectInput/Options/GroupItemView/SelectInputGroupItemView.d.ts +0 -3
  109. package/build/types/inputs/SelectInput/Options/GroupItemView/SelectInputGroupItemView.d.ts.map +0 -1
  110. package/build/types/inputs/SelectInput/Options/GroupItemView/index.d.ts +0 -2
  111. package/build/types/inputs/SelectInput/Options/GroupItemView/index.d.ts.map +0 -1
  112. package/build/types/inputs/SelectInput/Options/ItemView/Option/SelectInputOption.d.ts +0 -10
  113. package/build/types/inputs/SelectInput/Options/ItemView/Option/SelectInputOption.d.ts.map +0 -1
  114. package/build/types/inputs/SelectInput/Options/ItemView/Option/index.d.ts +0 -2
  115. package/build/types/inputs/SelectInput/Options/ItemView/Option/index.d.ts.map +0 -1
  116. package/build/types/inputs/SelectInput/Options/ItemView/SelectInputItemView.d.ts +0 -3
  117. package/build/types/inputs/SelectInput/Options/ItemView/SelectInputItemView.d.ts.map +0 -1
  118. package/build/types/inputs/SelectInput/Options/ItemView/index.d.ts +0 -2
  119. package/build/types/inputs/SelectInput/Options/ItemView/index.d.ts.map +0 -1
  120. package/build/types/inputs/SelectInput/Options/OptionsContainer/SelectInputOptionsContainer.d.ts +0 -6
  121. package/build/types/inputs/SelectInput/Options/OptionsContainer/SelectInputOptionsContainer.d.ts.map +0 -1
  122. package/build/types/inputs/SelectInput/Options/OptionsContainer/index.d.ts +0 -2
  123. package/build/types/inputs/SelectInput/Options/OptionsContainer/index.d.ts.map +0 -1
  124. package/build/types/inputs/SelectInput/Options/SelectInputOptions.d.ts +0 -15
  125. package/build/types/inputs/SelectInput/Options/SelectInputOptions.d.ts.map +0 -1
  126. package/build/types/inputs/SelectInput/Options/index.d.ts +0 -2
  127. package/build/types/inputs/SelectInput/Options/index.d.ts.map +0 -1
  128. package/build/types/inputs/SelectInput/SelectInput.constants.d.ts +0 -2
  129. package/build/types/inputs/SelectInput/SelectInput.constants.d.ts.map +0 -1
  130. package/build/types/inputs/SelectInput/SelectInput.d.ts +0 -3
  131. package/build/types/inputs/SelectInput/SelectInput.d.ts.map +0 -1
  132. package/build/types/inputs/SelectInput/SelectInput.helpers.d.ts +0 -28
  133. package/build/types/inputs/SelectInput/SelectInput.helpers.d.ts.map +0 -1
  134. package/build/types/inputs/SelectInput/SelectInput.messages.d.ts.map +0 -1
  135. package/build/types/inputs/SelectInput/SelectInput.types.d.ts.map +0 -1
  136. package/build/types/inputs/SelectInput/TriggerButton/SelectInputTriggerButton.d.ts +0 -15
  137. package/build/types/inputs/SelectInput/TriggerButton/SelectInputTriggerButton.d.ts.map +0 -1
  138. package/build/types/inputs/SelectInput/TriggerButton/index.d.ts +0 -3
  139. package/build/types/inputs/SelectInput/TriggerButton/index.d.ts.map +0 -1
  140. package/build/types/inputs/SelectInput/index.d.ts +0 -5
  141. package/build/types/inputs/SelectInput/index.d.ts.map +0 -1
  142. package/src/inputs/SelectInput/DefaultTrigger/ClearButton/SelectInputClearButton.tsx +0 -25
  143. package/src/inputs/SelectInput/DefaultTrigger/ClearButton/index.ts +0 -1
  144. package/src/inputs/SelectInput/DefaultTrigger/SelectInputDefaultTrigger.css +0 -17
  145. package/src/inputs/SelectInput/DefaultTrigger/SelectInputDefaultTrigger.less +0 -15
  146. package/src/inputs/SelectInput/DefaultTrigger/SelectInputDefaultTrigger.tsx +0 -56
  147. package/src/inputs/SelectInput/DefaultTrigger/index.ts +0 -1
  148. package/src/inputs/SelectInput/OptionContent/SelectInputOptionContent.css +0 -37
  149. package/src/inputs/SelectInput/OptionContent/SelectInputOptionContent.less +0 -38
  150. package/src/inputs/SelectInput/OptionContent/SelectInputOptionContent.tsx +0 -67
  151. package/src/inputs/SelectInput/OptionContent/index.ts +0 -5
  152. package/src/inputs/SelectInput/Options/GroupItemView/SelectInputGroupItemView.tsx +0 -53
  153. package/src/inputs/SelectInput/Options/GroupItemView/index.ts +0 -1
  154. package/src/inputs/SelectInput/Options/ItemView/Option/SelectInputOption.css +0 -33
  155. package/src/inputs/SelectInput/Options/ItemView/Option/SelectInputOption.less +0 -32
  156. package/src/inputs/SelectInput/Options/ItemView/Option/SelectInputOption.tsx +0 -51
  157. package/src/inputs/SelectInput/Options/ItemView/Option/index.ts +0 -5
  158. package/src/inputs/SelectInput/Options/ItemView/SelectInputItemView.css +0 -44
  159. package/src/inputs/SelectInput/Options/ItemView/SelectInputItemView.less +0 -14
  160. package/src/inputs/SelectInput/Options/ItemView/SelectInputItemView.tsx +0 -37
  161. package/src/inputs/SelectInput/Options/ItemView/index.ts +0 -1
  162. package/src/inputs/SelectInput/Options/OptionsContainer/SelectInputOptionsContainer.tsx +0 -55
  163. package/src/inputs/SelectInput/Options/OptionsContainer/index.ts +0 -1
  164. package/src/inputs/SelectInput/Options/SelectInputOptions.css +0 -125
  165. package/src/inputs/SelectInput/Options/SelectInputOptions.less +0 -78
  166. package/src/inputs/SelectInput/Options/SelectInputOptions.tsx +0 -372
  167. package/src/inputs/SelectInput/Options/index.ts +0 -1
  168. package/src/inputs/SelectInput/SelectInput.constants.ts +0 -1
  169. package/src/inputs/SelectInput/SelectInput.helpers.ts +0 -152
  170. package/src/inputs/SelectInput/SelectInput.less +0 -40
  171. package/src/inputs/SelectInput/SelectInput.test.tsx +0 -606
  172. package/src/inputs/SelectInput/SelectInput.tsx +0 -247
  173. package/src/inputs/SelectInput/SelectInput.types.ts +0 -127
  174. package/src/inputs/SelectInput/TriggerButton/SelectInputTriggerButton.tsx +0 -39
  175. package/src/inputs/SelectInput/TriggerButton/index.ts +0 -5
  176. package/src/inputs/SelectInput/index.ts +0 -13
  177. /package/build/inputs/{SelectInput/SelectInput.messages.js → SelectInput.messages.js} +0 -0
  178. /package/build/inputs/{SelectInput/SelectInput.messages.mjs → SelectInput.messages.mjs} +0 -0
  179. /package/build/types/inputs/{SelectInput/SelectInput.messages.d.ts → SelectInput.messages.d.ts} +0 -0
  180. /package/src/inputs/{SelectInput/SelectInput.messages.ts → SelectInput.messages.ts} +0 -0
@@ -1,372 +0,0 @@
1
- import { useEffect, useId, useMemo, useRef, useState } from 'react';
2
- import { Virtualizer, type VirtualizerHandle } from 'virtua';
3
- import { ListboxOptions } from '@headlessui/react';
4
- import { CrossCircle } from '@transferwise/icons';
5
- import { useIntl } from 'react-intl';
6
- import { clsx } from 'clsx';
7
-
8
- import { SearchInput } from '../../SearchInput';
9
- import messages from '../SelectInput.messages';
10
- import type {
11
- SelectInputProps,
12
- SelectInputOptionItem,
13
- SelectInputItem,
14
- } from '../SelectInput.types';
15
- import {
16
- searchableString,
17
- dedupeSelectInputItems,
18
- filterSelectInputItems,
19
- selectInputOptionItemIncludesNeedle,
20
- sortSelectInputItems,
21
- } from '../SelectInput.helpers';
22
- import { SELECT_INPUT_MAX_ITEMS_WITHOUT_VIRTUALIZATION } from '../SelectInput.constants';
23
-
24
- import { SelectInputOptionsContainer } from './OptionsContainer';
25
- import { SelectInputItemView } from './ItemView';
26
- import { SelectInputItemsCountContext, SelectInputItemPositionContext } from './ItemView/Option';
27
-
28
- interface SelectInputOptionsProps<T = string> extends Pick<
29
- SelectInputProps<T>,
30
- | 'items'
31
- | 'renderValue'
32
- | 'renderFooter'
33
- | 'filterable'
34
- | 'filterPlaceholder'
35
- | 'id'
36
- | 'parentId'
37
- | 'compareValues'
38
- | 'sortFilteredOptions'
39
- > {
40
- searchInputRef: React.MutableRefObject<HTMLInputElement | null>;
41
- listboxRef: React.MutableRefObject<HTMLDivElement | null>;
42
- filterQuery: string;
43
- onFilterChange: (query: string) => void;
44
- listBoxLabel?: string;
45
- listBoxLabelledBy?: string;
46
- autocomplete?: string;
47
- name?: string;
48
- onAutocompleteSelect?: (value: T) => void;
49
- }
50
-
51
- export function SelectInputOptions<T = string>({
52
- id,
53
- parentId,
54
- items,
55
- compareValues: compareValuesProp,
56
- renderValue = String,
57
- renderFooter,
58
- filterable = false,
59
- filterPlaceholder,
60
- sortFilteredOptions,
61
- searchInputRef,
62
- listboxRef,
63
- filterQuery,
64
- onFilterChange,
65
- listBoxLabel,
66
- listBoxLabelledBy,
67
- autocomplete,
68
- name,
69
- onAutocompleteSelect,
70
- }: SelectInputOptionsProps<T>) {
71
- const intl = useIntl();
72
- const virtualiserHandlerRef = useRef<VirtualizerHandle>(null);
73
- const controllerRef = filterable ? searchInputRef : listboxRef;
74
- const [initialRender, setInitialRender] = useState(true);
75
-
76
- const needle = useMemo(() => {
77
- if (filterable) {
78
- return filterQuery ? searchableString(filterQuery) : null;
79
- }
80
- return undefined;
81
- }, [filterQuery, filterable]);
82
-
83
- useEffect(() => {
84
- if (needle) {
85
- // Ensure having an active option while filtering.
86
- // Without `requestAnimationFrame` upon which React depends for scheduling
87
- // updates, the active status would only show for a split second and then
88
- // disappear inadvertently.
89
- requestAnimationFrame(() => {
90
- if (
91
- controllerRef.current != null &&
92
- !controllerRef.current.hasAttribute('aria-activedescendant')
93
- ) {
94
- // Activate first option via synthetic key press
95
- controllerRef.current.dispatchEvent(
96
- new KeyboardEvent('keydown', { key: 'Home', bubbles: true }),
97
- );
98
- }
99
- });
100
- }
101
- }, [controllerRef, needle]);
102
-
103
- const compareValues = useMemo(() => {
104
- if (!compareValuesProp) {
105
- return undefined;
106
- }
107
-
108
- if (typeof compareValuesProp === 'function') {
109
- return (a: NonNullable<T>, b: NonNullable<T>) => compareValuesProp(a, b);
110
- }
111
-
112
- const key = compareValuesProp;
113
- return (a: NonNullable<T>, b: NonNullable<T>) => {
114
- if (typeof a === 'object' && a != null && typeof b === 'object' && b != null) {
115
- return (a as Record<string, unknown>)[key] === (b as Record<string, unknown>)[key];
116
- }
117
- return a === b;
118
- };
119
- }, [compareValuesProp]);
120
-
121
- const filteredItems: readonly SelectInputItem<NonNullable<T> | undefined>[] = useMemo(() => {
122
- if (needle == null) {
123
- return items;
124
- }
125
-
126
- const filtered = filterSelectInputItems(dedupeSelectInputItems(items, compareValues), (item) =>
127
- selectInputOptionItemIncludesNeedle(item, needle),
128
- );
129
-
130
- if (sortFilteredOptions) {
131
- return sortSelectInputItems(filtered, sortFilteredOptions, filterQuery);
132
- }
133
-
134
- return filtered;
135
- // eslint-disable-next-line react-hooks/exhaustive-deps
136
- }, [needle, items, compareValues]);
137
- const resultsEmpty = needle != null && filteredItems.length === 0;
138
-
139
- const virtualized = filteredItems.length > SELECT_INPUT_MAX_ITEMS_WITHOUT_VIRTUALIZATION;
140
-
141
- // Items shown once shall be kept mounted until the needle changes, otherwise
142
- // the scroll position may jump around inadvertently. Pattern adopted from:
143
- // https://inokawa.github.io/virtua/?path=/story/advanced-keep-offscreen-items--append-only
144
- const [mountedIndexes, setMountedIndexes] = useState<number[]>([]);
145
- useEffect(() => {
146
- // Ensure the 'End' key works as intended by keeping the last item mounted
147
- setMountedIndexes((prevMountedIndexes) => {
148
- const indexes = new Set(prevMountedIndexes);
149
- indexes.add(filteredItems.length - 1);
150
- return [...indexes]; // Sorting is redundant by nature here
151
- });
152
- }, [
153
- needle, // Needed as `filteredItems.length` may be equal between two updates
154
- filteredItems.length,
155
- ]);
156
-
157
- const listboxContainerRef = useRef<HTMLDivElement>(null);
158
- useEffect(() => {
159
- if (listboxContainerRef.current != null) {
160
- listboxContainerRef.current.style.setProperty(
161
- '--initial-height',
162
- `${listboxContainerRef.current.offsetHeight}px`,
163
- );
164
- }
165
- }, []);
166
-
167
- useEffect(() => {
168
- setInitialRender(false);
169
- }, []);
170
-
171
- const showStatus = resultsEmpty;
172
- const statusId = useId();
173
- const listboxId = useId();
174
-
175
- const getItemNode = (index: number) => {
176
- const item = filteredItems[index];
177
- return (
178
- <SelectInputItemView key={index} item={item} renderValue={renderValue} needle={needle} />
179
- );
180
- };
181
-
182
- const findMatchingItem = (autocompleteValue: string): T | null => {
183
- const flatOptions = items
184
- .flatMap((item) =>
185
- item.type === 'group' ? item.options : item.type === 'option' ? [item] : [],
186
- )
187
- .filter(
188
- (item): item is SelectInputOptionItem<NonNullable<T>> =>
189
- item.type === 'option' && item.value != null,
190
- );
191
-
192
- const exactMatch = flatOptions.find(
193
- (option) =>
194
- String(option.value) === autocompleteValue ||
195
- option.filterMatchers?.some((matcher) => matcher === autocompleteValue),
196
- );
197
-
198
- if (exactMatch) {
199
- return exactMatch.value;
200
- }
201
-
202
- const fuzzyMatch = flatOptions.find((option) =>
203
- option.filterMatchers?.some((matcher) =>
204
- matcher.toLowerCase().includes(autocompleteValue.toLowerCase()),
205
- ),
206
- );
207
-
208
- return fuzzyMatch ? fuzzyMatch.value : null;
209
- };
210
-
211
- return (
212
- <ListboxOptions
213
- modal
214
- as={SelectInputOptionsContainer}
215
- static
216
- className="np-select-input-options-container"
217
- onAriaActiveDescendantChange={(value: React.AriaAttributes['aria-activedescendant']) => {
218
- if (controllerRef.current != null) {
219
- if (!initialRender && value != null) {
220
- controllerRef.current.setAttribute('aria-activedescendant', value);
221
- } else {
222
- controllerRef.current.removeAttribute('aria-activedescendant');
223
- }
224
- }
225
- }}
226
- >
227
- {filterable ? (
228
- <div className="np-select-input-query-container">
229
- <SearchInput
230
- ref={searchInputRef}
231
- id={id}
232
- name={name}
233
- autoComplete={autocomplete}
234
- role="combobox"
235
- shape="rectangle"
236
- placeholder={filterPlaceholder}
237
- aria-label={filterPlaceholder}
238
- defaultValue={filterQuery}
239
- aria-autocomplete="list"
240
- aria-expanded
241
- aria-controls={listboxId}
242
- aria-describedby={showStatus ? statusId : undefined}
243
- onKeyDown={(event) => {
244
- // Prevent interfering with the matcher of Headless UI
245
- // https://mathiasbynens.be/notes/javascript-unicode#regex
246
- if (/^.$/u.test(event.key)) {
247
- event.stopPropagation();
248
- }
249
- }}
250
- onChange={(event) => {
251
- // Free up resources and ensure not to go out of bounds when the
252
- // resulting item count is less than before
253
- const inputValue = event.currentTarget.value;
254
-
255
- // Free up resources and ensure not to go out of bounds
256
- setMountedIndexes([]);
257
- onFilterChange(inputValue);
258
- }}
259
- onInput={(event) => {
260
- const inputValue = event.currentTarget.value;
261
- const inputElement = event.currentTarget;
262
-
263
- if (autocomplete && onAutocompleteSelect && inputValue) {
264
- setTimeout(() => {
265
- if (inputElement.value === inputValue && inputValue.length > 2) {
266
- const matchedValue = findMatchingItem(inputValue);
267
- if (matchedValue !== null) {
268
- onAutocompleteSelect(matchedValue);
269
- }
270
- }
271
- }, 50);
272
- }
273
- }}
274
- />
275
- </div>
276
- ) : null}
277
-
278
- <section
279
- ref={listboxContainerRef}
280
- tabIndex={-1}
281
- className={clsx(
282
- 'np-select-input-listbox-container',
283
- virtualized && 'np-select-input-listbox-container--virtualized',
284
- needle == null && // Groups aren't shown when filtering
285
- items.some((item) => item.type === 'group') &&
286
- 'np-select-input-listbox-container--has-group',
287
- )}
288
- data-wds-parent={parentId ?? undefined}
289
- >
290
- {resultsEmpty ? (
291
- <div id={statusId} className="np-select-input-options-status">
292
- <CrossCircle size={16} className="np-select-input-options-status-icon" />
293
- {intl.formatMessage(messages.noResultsFound)}
294
- </div>
295
- ) : null}
296
-
297
- <div
298
- ref={listboxRef}
299
- id={listboxId}
300
- role="listbox"
301
- aria-orientation="vertical"
302
- aria-label={listBoxLabel}
303
- aria-labelledby={listBoxLabelledBy}
304
- tabIndex={0}
305
- className="np-select-input-listbox"
306
- >
307
- {!virtualized ? (
308
- filteredItems.map((_, index) => getItemNode(index))
309
- ) : (
310
- <Virtualizer
311
- ref={virtualiserHandlerRef}
312
- key={needle}
313
- data={filteredItems}
314
- keepMounted={mountedIndexes}
315
- scrollRef={listboxRef} // `VList` doesn't expose this
316
- onScroll={async () => {
317
- if (!virtualiserHandlerRef.current) return;
318
-
319
- const startIndex = virtualiserHandlerRef.current.findItemIndex(
320
- virtualiserHandlerRef.current.scrollOffset,
321
- );
322
- const endIndex = virtualiserHandlerRef.current.findItemIndex(
323
- virtualiserHandlerRef.current.scrollOffset +
324
- virtualiserHandlerRef.current.viewportSize,
325
- );
326
-
327
- setMountedIndexes((prevMountedIndexes) => {
328
- const indexes = new Set(prevMountedIndexes);
329
-
330
- for (let index = startIndex; index <= endIndex; index += 1) {
331
- indexes.add(index);
332
- }
333
-
334
- return [...indexes].sort((a, b) => a - b);
335
- });
336
- }}
337
- >
338
- {(item, index) => (
339
- // The position of each item can't be inferred by browsers when
340
- // virtualizing, as some of the items may not be in the DOM
341
- <SelectInputItemsCountContext.Provider value={filteredItems.length}>
342
- <SelectInputItemPositionContext.Provider value={index + 1}>
343
- {getItemNode(index)}
344
- </SelectInputItemPositionContext.Provider>
345
- </SelectInputItemsCountContext.Provider>
346
- )}
347
- </Virtualizer>
348
- )}
349
- </div>
350
-
351
- {renderFooter != null ? (
352
- <footer className="np-select-input-footer">
353
- <div
354
- role="none"
355
- onKeyDown={(event) => {
356
- // Prevent interfering with Headless UI
357
- if (event.key !== 'Escape') {
358
- event.stopPropagation();
359
- }
360
- }}
361
- >
362
- {renderFooter({
363
- resultsEmpty,
364
- queryNormalized: needle,
365
- })}
366
- </div>
367
- </footer>
368
- ) : null}
369
- </section>
370
- </ListboxOptions>
371
- );
372
- }
@@ -1 +0,0 @@
1
- export { SelectInputOptions } from './SelectInputOptions';
@@ -1 +0,0 @@
1
- export const SELECT_INPUT_MAX_ITEMS_WITHOUT_VIRTUALIZATION = 50;
@@ -1,152 +0,0 @@
1
- import type { SelectInputOptionItem, SelectInputItem } from './SelectInput.types';
2
-
3
- /**
4
- * Normalises a string for searching by trimming, normalising whitespace,
5
- * removing diacritics, and converting to lowercase.
6
- */
7
- export function searchableString(value: string) {
8
- return (
9
- value
10
- .trim()
11
- .replace(/\s+/gu, ' ')
12
- // NFD converts an Å to A + ̊ (and other special characters)
13
- .normalize('NFD')
14
- // and then this replaces the ̊ with nothing (and other special characters)
15
- .replace(/[\u0300-\u036f]/g, '')
16
- .toLowerCase()
17
- );
18
- }
19
-
20
- /**
21
- * Infers searchable strings from a value.
22
- * Extracts string values from objects or returns the string itself.
23
- */
24
- function inferSearchableStrings(value: unknown) {
25
- if (typeof value === 'string') {
26
- return [searchableString(value)];
27
- }
28
-
29
- if (typeof value === 'object' && value != null) {
30
- return Object.values(value)
31
- .filter((innerValue) => typeof innerValue === 'string')
32
- .map((innerValue) => searchableString(innerValue));
33
- }
34
-
35
- return [];
36
- }
37
-
38
- /**
39
- * Checks if a select input option item matches the search needle.
40
- */
41
- export function selectInputOptionItemIncludesNeedle<T>(
42
- item: SelectInputOptionItem<T>,
43
- needle: string,
44
- ) {
45
- return inferSearchableStrings(item.filterMatchers ?? item.value).some((haystack) =>
46
- haystack.includes(needle),
47
- );
48
- }
49
-
50
- /**
51
- * Deduplicates a single option item by checking against existing values.
52
- * Returns the item with value set to undefined if it's a duplicate.
53
- */
54
- function dedupeSelectInputOptionItem<T>(
55
- item: SelectInputOptionItem<T>,
56
- existingValues: Set<T>,
57
- compareValues?: (a: T, b: T) => boolean,
58
- ): SelectInputOptionItem<T | undefined> {
59
- const isDuplicate = compareValues
60
- ? Array.from(existingValues).some((existingValue) => compareValues(item.value, existingValue))
61
- : existingValues.has(item.value);
62
-
63
- if (!isDuplicate) {
64
- existingValues.add(item.value);
65
- return item;
66
- }
67
- return { ...item, value: undefined };
68
- }
69
-
70
- /**
71
- * Sets the `value` of duplicate option items to `undefined`, hiding them when
72
- * rendered. Indexes are kept intact within groups to preserve the active item
73
- * between filter changes when possible.
74
- */
75
- export function dedupeSelectInputItems<T>(
76
- items: readonly SelectInputItem<T>[],
77
- compareValues?: (a: T, b: T) => boolean,
78
- ): SelectInputItem<T | undefined>[] {
79
- const existingValues = new Set<T>();
80
-
81
- return items.map((item) => {
82
- switch (item.type) {
83
- case 'option': {
84
- return dedupeSelectInputOptionItem(item, existingValues, compareValues);
85
- }
86
- case 'group': {
87
- return {
88
- ...item,
89
- options: item.options.map((option) =>
90
- dedupeSelectInputOptionItem(option, existingValues, compareValues),
91
- ),
92
- };
93
- }
94
- default:
95
- }
96
- return item;
97
- });
98
- }
99
-
100
- /**
101
- * Filters select input items based on a predicate function.
102
- * Groups are included if at least one option matches the predicate.
103
- */
104
- export function filterSelectInputItems<T>(
105
- items: readonly SelectInputItem<T>[],
106
- predicate: (item: SelectInputOptionItem<T>) => boolean,
107
- ) {
108
- return items.filter((item) => {
109
- switch (item.type) {
110
- case 'option': {
111
- return predicate(item);
112
- }
113
- case 'group': {
114
- return item.options.some((option) => predicate(option));
115
- }
116
- default:
117
- }
118
- return false;
119
- });
120
- }
121
-
122
- /**
123
- * Flattens and sorts filtered options using the provided comparator.
124
- * Extracts all options from groups, filters out undefined values (deduplicated items),
125
- * sorts them, and returns as a flat list of option items.
126
- */
127
- export function sortSelectInputItems<T>(
128
- items: readonly SelectInputItem<T | undefined>[],
129
- compareFn: (
130
- a: SelectInputOptionItem<NonNullable<T>>,
131
- b: SelectInputOptionItem<NonNullable<T>>,
132
- searchQuery: string,
133
- ) => number,
134
- searchQuery: string,
135
- ): SelectInputItem<NonNullable<T>>[] {
136
- const flattenedOption = items.flatMap((item) => {
137
- if (item.type === 'option') {
138
- return item.value !== undefined ? [item as SelectInputOptionItem<NonNullable<T>>] : [];
139
- }
140
-
141
- if (item.type === 'group') {
142
- return item.options.filter(
143
- (option): option is SelectInputOptionItem<NonNullable<T>> => option.value !== undefined,
144
- );
145
- }
146
-
147
- return [];
148
- });
149
-
150
- // eslint-disable-next-line functional/immutable-data
151
- return flattenedOption.sort((a, b) => compareFn(a, b, searchQuery));
152
- }
@@ -1,40 +0,0 @@
1
- @import (reference) "../../../node_modules/@transferwise/neptune-css/src/less/ring.less";
2
- @import "../_BottomSheet.less";
3
- @import "../_ButtonInput.less";
4
- @import "../_Popover.less";
5
- @import "./Options/SelectInputOptions.less";
6
- @import "./OptionContent/SelectInputOptionContent.less";
7
- @import "./DefaultTrigger/SelectInputDefaultTrigger.less";
8
-
9
- .np-select-input-content {
10
- overflow: hidden;
11
- text-overflow: ellipsis;
12
- white-space: nowrap;
13
- }
14
-
15
- .np-select-input-placeholder {
16
- color: var(--color-content-tertiary);
17
- }
18
-
19
- .np-select-input-addon {
20
- border-width: 0;
21
- background: none;
22
-
23
- display: inline-flex;
24
- height: var(--size-24);
25
- width: var(--size-24);
26
- align-items: center;
27
- justify-content: center;
28
- border-radius: 0.125rem /* 2px */; /* TODO: Tokenize */
29
-
30
- &--interactive {
31
- pointer-events: auto;
32
- color: var(--color-interactive-secondary);
33
-
34
- &:hover {
35
- color: var(--color-interactive-secondary-hover);
36
- }
37
-
38
- .focus-ring();
39
- }
40
- }