@transferwise/components 46.130.2 → 46.130.3

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 (236) hide show
  1. package/build/dateInput/DateInput.js +12 -5
  2. package/build/dateInput/DateInput.js.map +1 -1
  3. package/build/dateInput/DateInput.mjs +11 -4
  4. package/build/dateInput/DateInput.mjs.map +1 -1
  5. package/build/expressiveMoneyInput/currencySelector/CurrencySelector.js +16 -8
  6. package/build/expressiveMoneyInput/currencySelector/CurrencySelector.js.map +1 -1
  7. package/build/expressiveMoneyInput/currencySelector/CurrencySelector.mjs +14 -6
  8. package/build/expressiveMoneyInput/currencySelector/CurrencySelector.mjs.map +1 -1
  9. package/build/index.js +12 -7
  10. package/build/index.js.map +1 -1
  11. package/build/index.mjs +9 -3
  12. package/build/index.mjs.map +1 -1
  13. package/build/inputs/{_BottomSheet.js → SelectInput/BottomSheet/SelectInputBottomSheet.js} +7 -7
  14. package/build/inputs/SelectInput/BottomSheet/SelectInputBottomSheet.js.map +1 -0
  15. package/build/inputs/{_BottomSheet.mjs → SelectInput/BottomSheet/SelectInputBottomSheet.mjs} +7 -7
  16. package/build/inputs/SelectInput/BottomSheet/SelectInputBottomSheet.mjs.map +1 -0
  17. package/build/inputs/{_ButtonInput.js → SelectInput/ButtonInput/SelectInputButtonInput.js} +5 -5
  18. package/build/inputs/SelectInput/ButtonInput/SelectInputButtonInput.js.map +1 -0
  19. package/build/inputs/{_ButtonInput.mjs → SelectInput/ButtonInput/SelectInputButtonInput.mjs} +5 -5
  20. package/build/inputs/SelectInput/ButtonInput/SelectInputButtonInput.mjs.map +1 -0
  21. package/build/inputs/SelectInput/ClearButton/SelectInputClearButton.js +26 -0
  22. package/build/inputs/SelectInput/ClearButton/SelectInputClearButton.js.map +1 -0
  23. package/build/inputs/SelectInput/ClearButton/SelectInputClearButton.mjs +24 -0
  24. package/build/inputs/SelectInput/ClearButton/SelectInputClearButton.mjs.map +1 -0
  25. package/build/inputs/SelectInput/DefaultRenderTrigger/SelectInputDefaultRenderTrigger.js +59 -0
  26. package/build/inputs/SelectInput/DefaultRenderTrigger/SelectInputDefaultRenderTrigger.js.map +1 -0
  27. package/build/inputs/SelectInput/DefaultRenderTrigger/SelectInputDefaultRenderTrigger.mjs +56 -0
  28. package/build/inputs/SelectInput/DefaultRenderTrigger/SelectInputDefaultRenderTrigger.mjs.map +1 -0
  29. package/build/inputs/SelectInput/ItemView/GroupItemView/SelectInputGroupItemView.js +50 -0
  30. package/build/inputs/SelectInput/ItemView/GroupItemView/SelectInputGroupItemView.js.map +1 -0
  31. package/build/inputs/SelectInput/ItemView/GroupItemView/SelectInputGroupItemView.mjs +48 -0
  32. package/build/inputs/SelectInput/ItemView/GroupItemView/SelectInputGroupItemView.mjs.map +1 -0
  33. package/build/inputs/SelectInput/ItemView/SelectInputItemView.js +47 -0
  34. package/build/inputs/SelectInput/ItemView/SelectInputItemView.js.map +1 -0
  35. package/build/inputs/SelectInput/ItemView/SelectInputItemView.mjs +45 -0
  36. package/build/inputs/SelectInput/ItemView/SelectInputItemView.mjs.map +1 -0
  37. package/build/inputs/SelectInput/Option/SelectInputOption.js +42 -0
  38. package/build/inputs/SelectInput/Option/SelectInputOption.js.map +1 -0
  39. package/build/inputs/SelectInput/Option/SelectInputOption.mjs +40 -0
  40. package/build/inputs/SelectInput/Option/SelectInputOption.mjs.map +1 -0
  41. package/build/inputs/SelectInput/OptionContent/SelectInputOptionContent.js +40 -0
  42. package/build/inputs/SelectInput/OptionContent/SelectInputOptionContent.js.map +1 -0
  43. package/build/inputs/SelectInput/OptionContent/SelectInputOptionContent.mjs +38 -0
  44. package/build/inputs/SelectInput/OptionContent/SelectInputOptionContent.mjs.map +1 -0
  45. package/build/inputs/SelectInput/Options/OptionsContainer/SelectInputOptionsContainer.js +48 -0
  46. package/build/inputs/SelectInput/Options/OptionsContainer/SelectInputOptionsContainer.js.map +1 -0
  47. package/build/inputs/SelectInput/Options/OptionsContainer/SelectInputOptionsContainer.mjs +46 -0
  48. package/build/inputs/SelectInput/Options/OptionsContainer/SelectInputOptionsContainer.mjs.map +1 -0
  49. package/build/inputs/SelectInput/Options/SelectInputOptions.js +300 -0
  50. package/build/inputs/SelectInput/Options/SelectInputOptions.js.map +1 -0
  51. package/build/inputs/SelectInput/Options/SelectInputOptions.mjs +298 -0
  52. package/build/inputs/SelectInput/Options/SelectInputOptions.mjs.map +1 -0
  53. package/build/inputs/{_Popover.js → SelectInput/Popover/SelectInputPopover.js} +7 -7
  54. package/build/inputs/SelectInput/Popover/SelectInputPopover.js.map +1 -0
  55. package/build/inputs/{_Popover.mjs → SelectInput/Popover/SelectInputPopover.mjs} +7 -7
  56. package/build/inputs/SelectInput/Popover/SelectInputPopover.mjs.map +1 -0
  57. package/build/inputs/SelectInput/SelectInput.contexts.js +29 -0
  58. package/build/inputs/SelectInput/SelectInput.contexts.js.map +1 -0
  59. package/build/inputs/SelectInput/SelectInput.contexts.mjs +24 -0
  60. package/build/inputs/SelectInput/SelectInput.contexts.mjs.map +1 -0
  61. package/build/inputs/SelectInput/SelectInput.js +222 -0
  62. package/build/inputs/SelectInput/SelectInput.js.map +1 -0
  63. package/build/inputs/SelectInput/SelectInput.messages.js.map +1 -0
  64. package/build/inputs/SelectInput/SelectInput.messages.mjs.map +1 -0
  65. package/build/inputs/SelectInput/SelectInput.mjs +216 -0
  66. package/build/inputs/SelectInput/SelectInput.mjs.map +1 -0
  67. package/build/inputs/SelectInput/SelectInput.utils.js +164 -0
  68. package/build/inputs/SelectInput/SelectInput.utils.js.map +1 -0
  69. package/build/inputs/SelectInput/SelectInput.utils.mjs +154 -0
  70. package/build/inputs/SelectInput/SelectInput.utils.mjs.map +1 -0
  71. package/build/inputs/SelectInput/TriggerButton/SelectInputTriggerButton.js +42 -0
  72. package/build/inputs/SelectInput/TriggerButton/SelectInputTriggerButton.js.map +1 -0
  73. package/build/inputs/SelectInput/TriggerButton/SelectInputTriggerButton.mjs +36 -0
  74. package/build/inputs/SelectInput/TriggerButton/SelectInputTriggerButton.mjs.map +1 -0
  75. package/build/main.css +90 -90
  76. package/build/moneyInput/MoneyInput.js +9 -2
  77. package/build/moneyInput/MoneyInput.js.map +1 -1
  78. package/build/moneyInput/MoneyInput.mjs +8 -1
  79. package/build/moneyInput/MoneyInput.mjs.map +1 -1
  80. package/build/phoneNumberInput/PhoneNumberInput.js +10 -3
  81. package/build/phoneNumberInput/PhoneNumberInput.js.map +1 -1
  82. package/build/phoneNumberInput/PhoneNumberInput.mjs +9 -2
  83. package/build/phoneNumberInput/PhoneNumberInput.mjs.map +1 -1
  84. package/build/styles/inputs/SelectInput/BottomSheet/SelectInputBottomSheet.css +96 -0
  85. package/build/styles/inputs/SelectInput/ButtonInput/SelectInputButtonInput.css +16 -0
  86. package/build/styles/inputs/SelectInput/ClearButton/SelectInputClearButton.css +46 -0
  87. package/build/styles/inputs/SelectInput/ItemView/SelectInputItemView.css +16 -0
  88. package/build/styles/inputs/SelectInput/Option/SelectInputOption.css +33 -0
  89. package/build/styles/inputs/SelectInput/OptionContent/SelectInputOptionContent.css +37 -0
  90. package/build/styles/inputs/SelectInput/Options/SelectInputOptions.css +81 -0
  91. package/build/styles/inputs/SelectInput/Popover/SelectInputPopover.css +46 -0
  92. package/build/styles/main.css +90 -90
  93. package/build/types/index.d.ts +1 -1
  94. package/build/types/index.d.ts.map +1 -1
  95. package/build/types/inputs/{_BottomSheet.d.ts → SelectInput/BottomSheet/SelectInputBottomSheet.d.ts} +3 -3
  96. package/build/types/inputs/SelectInput/BottomSheet/SelectInputBottomSheet.d.ts.map +1 -0
  97. package/build/types/inputs/SelectInput/BottomSheet/index.d.ts +3 -0
  98. package/build/types/inputs/SelectInput/BottomSheet/index.d.ts.map +1 -0
  99. package/build/types/inputs/SelectInput/ButtonInput/SelectInputButtonInput.d.ts +5 -0
  100. package/build/types/inputs/SelectInput/ButtonInput/SelectInputButtonInput.d.ts.map +1 -0
  101. package/build/types/inputs/SelectInput/ButtonInput/index.d.ts +3 -0
  102. package/build/types/inputs/SelectInput/ButtonInput/index.d.ts.map +1 -0
  103. package/build/types/inputs/SelectInput/ClearButton/SelectInputClearButton.d.ts +7 -0
  104. package/build/types/inputs/SelectInput/ClearButton/SelectInputClearButton.d.ts.map +1 -0
  105. package/build/types/inputs/SelectInput/ClearButton/index.d.ts +3 -0
  106. package/build/types/inputs/SelectInput/ClearButton/index.d.ts.map +1 -0
  107. package/build/types/inputs/SelectInput/DefaultRenderTrigger/SelectInputDefaultRenderTrigger.d.ts +16 -0
  108. package/build/types/inputs/SelectInput/DefaultRenderTrigger/SelectInputDefaultRenderTrigger.d.ts.map +1 -0
  109. package/build/types/inputs/SelectInput/DefaultRenderTrigger/index.d.ts +2 -0
  110. package/build/types/inputs/SelectInput/DefaultRenderTrigger/index.d.ts.map +1 -0
  111. package/build/types/inputs/SelectInput/ItemView/GroupItemView/SelectInputGroupItemView.d.ts +9 -0
  112. package/build/types/inputs/SelectInput/ItemView/GroupItemView/SelectInputGroupItemView.d.ts.map +1 -0
  113. package/build/types/inputs/SelectInput/ItemView/GroupItemView/index.d.ts +3 -0
  114. package/build/types/inputs/SelectInput/ItemView/GroupItemView/index.d.ts.map +1 -0
  115. package/build/types/inputs/SelectInput/ItemView/SelectInputItemView.d.ts +11 -0
  116. package/build/types/inputs/SelectInput/ItemView/SelectInputItemView.d.ts.map +1 -0
  117. package/build/types/inputs/SelectInput/ItemView/index.d.ts +4 -0
  118. package/build/types/inputs/SelectInput/ItemView/index.d.ts.map +1 -0
  119. package/build/types/inputs/SelectInput/Option/SelectInputOption.d.ts +11 -0
  120. package/build/types/inputs/SelectInput/Option/SelectInputOption.d.ts.map +1 -0
  121. package/build/types/inputs/SelectInput/Option/index.d.ts +3 -0
  122. package/build/types/inputs/SelectInput/Option/index.d.ts.map +1 -0
  123. package/build/types/inputs/SelectInput/OptionContent/SelectInputOptionContent.d.ts +13 -0
  124. package/build/types/inputs/SelectInput/OptionContent/SelectInputOptionContent.d.ts.map +1 -0
  125. package/build/types/inputs/SelectInput/OptionContent/index.d.ts +3 -0
  126. package/build/types/inputs/SelectInput/OptionContent/index.d.ts.map +1 -0
  127. package/build/types/inputs/SelectInput/Options/OptionsContainer/SelectInputOptionsContainer.d.ts +9 -0
  128. package/build/types/inputs/SelectInput/Options/OptionsContainer/SelectInputOptionsContainer.d.ts.map +1 -0
  129. package/build/types/inputs/SelectInput/Options/OptionsContainer/index.d.ts +3 -0
  130. package/build/types/inputs/SelectInput/Options/OptionsContainer/index.d.ts.map +1 -0
  131. package/build/types/inputs/SelectInput/Options/SelectInputOptions.d.ts +21 -0
  132. package/build/types/inputs/SelectInput/Options/SelectInputOptions.d.ts.map +1 -0
  133. package/build/types/inputs/SelectInput/Options/index.d.ts +4 -0
  134. package/build/types/inputs/SelectInput/Options/index.d.ts.map +1 -0
  135. package/build/types/inputs/{_Popover.d.ts → SelectInput/Popover/SelectInputPopover.d.ts} +3 -3
  136. package/build/types/inputs/SelectInput/Popover/SelectInputPopover.d.ts.map +1 -0
  137. package/build/types/inputs/SelectInput/Popover/index.d.ts +3 -0
  138. package/build/types/inputs/SelectInput/Popover/index.d.ts.map +1 -0
  139. package/build/types/inputs/SelectInput/SelectInput.contexts.d.ts +33 -0
  140. package/build/types/inputs/SelectInput/SelectInput.contexts.d.ts.map +1 -0
  141. package/build/types/inputs/SelectInput/SelectInput.d.ts +10 -0
  142. package/build/types/inputs/SelectInput/SelectInput.d.ts.map +1 -0
  143. package/build/types/inputs/SelectInput/SelectInput.messages.d.ts.map +1 -0
  144. package/build/types/inputs/{SelectInput.d.ts → SelectInput/SelectInput.types.d.ts} +12 -38
  145. package/build/types/inputs/SelectInput/SelectInput.types.d.ts.map +1 -0
  146. package/build/types/inputs/SelectInput/SelectInput.utils.d.ts +60 -0
  147. package/build/types/inputs/SelectInput/SelectInput.utils.d.ts.map +1 -0
  148. package/build/types/inputs/SelectInput/TriggerButton/SelectInputTriggerButton.d.ts +12 -0
  149. package/build/types/inputs/SelectInput/TriggerButton/SelectInputTriggerButton.d.ts.map +1 -0
  150. package/build/types/inputs/SelectInput/TriggerButton/index.d.ts +3 -0
  151. package/build/types/inputs/SelectInput/TriggerButton/index.d.ts.map +1 -0
  152. package/build/types/inputs/SelectInput/components.d.ts +10 -0
  153. package/build/types/inputs/SelectInput/components.d.ts.map +1 -0
  154. package/build/types/inputs/SelectInput/index.d.ts +10 -0
  155. package/build/types/inputs/SelectInput/index.d.ts.map +1 -0
  156. package/package.json +1 -1
  157. package/src/index.ts +0 -1
  158. package/src/inputs/SelectInput/BottomSheet/SelectInputBottomSheet.css +96 -0
  159. package/src/inputs/{_BottomSheet.tsx → SelectInput/BottomSheet/SelectInputBottomSheet.tsx} +7 -7
  160. package/src/inputs/SelectInput/BottomSheet/index.ts +2 -0
  161. package/src/inputs/SelectInput/ButtonInput/SelectInputButtonInput.css +16 -0
  162. package/src/inputs/{_ButtonInput.tsx → SelectInput/ButtonInput/SelectInputButtonInput.tsx} +5 -5
  163. package/src/inputs/SelectInput/ButtonInput/index.ts +2 -0
  164. package/src/inputs/SelectInput/ClearButton/SelectInputClearButton.css +46 -0
  165. package/src/inputs/SelectInput/ClearButton/SelectInputClearButton.less +39 -0
  166. package/src/inputs/SelectInput/ClearButton/SelectInputClearButton.tsx +27 -0
  167. package/src/inputs/SelectInput/ClearButton/index.ts +2 -0
  168. package/src/inputs/SelectInput/DefaultRenderTrigger/SelectInputDefaultRenderTrigger.tsx +74 -0
  169. package/src/inputs/SelectInput/DefaultRenderTrigger/index.ts +5 -0
  170. package/src/inputs/SelectInput/ItemView/GroupItemView/SelectInputGroupItemView.tsx +61 -0
  171. package/src/inputs/SelectInput/ItemView/GroupItemView/index.ts +2 -0
  172. package/src/inputs/SelectInput/ItemView/SelectInputItemView.css +16 -0
  173. package/src/inputs/SelectInput/ItemView/SelectInputItemView.less +17 -0
  174. package/src/inputs/SelectInput/ItemView/SelectInputItemView.tsx +48 -0
  175. package/src/inputs/SelectInput/ItemView/index.ts +3 -0
  176. package/src/inputs/SelectInput/Option/SelectInputOption.css +33 -0
  177. package/src/inputs/SelectInput/Option/SelectInputOption.less +32 -0
  178. package/src/inputs/SelectInput/Option/SelectInputOption.tsx +57 -0
  179. package/src/inputs/SelectInput/Option/index.ts +2 -0
  180. package/src/inputs/SelectInput/OptionContent/SelectInputOptionContent.css +37 -0
  181. package/src/inputs/SelectInput/OptionContent/SelectInputOptionContent.less +38 -0
  182. package/src/inputs/SelectInput/OptionContent/SelectInputOptionContent.tsx +72 -0
  183. package/src/inputs/SelectInput/OptionContent/index.ts +2 -0
  184. package/src/inputs/SelectInput/Options/OptionsContainer/SelectInputOptionsContainer.tsx +59 -0
  185. package/src/inputs/SelectInput/Options/OptionsContainer/index.ts +2 -0
  186. package/src/inputs/SelectInput/Options/SelectInputOptions.css +81 -0
  187. package/src/inputs/SelectInput/Options/SelectInputOptions.less +77 -0
  188. package/src/inputs/SelectInput/Options/SelectInputOptions.tsx +411 -0
  189. package/src/inputs/SelectInput/Options/index.ts +3 -0
  190. package/src/inputs/SelectInput/Popover/SelectInputPopover.css +46 -0
  191. package/src/inputs/{_Popover.tsx → SelectInput/Popover/SelectInputPopover.tsx} +7 -7
  192. package/src/inputs/SelectInput/Popover/index.ts +2 -0
  193. package/src/inputs/SelectInput/SelectInput.contexts.tsx +40 -0
  194. package/src/inputs/SelectInput/SelectInput.less +22 -0
  195. package/src/inputs/{SelectInput.test.tsx → SelectInput/SelectInput.test.tsx} +9 -11
  196. package/src/inputs/SelectInput/SelectInput.tsx +257 -0
  197. package/src/inputs/SelectInput/SelectInput.types.ts +113 -0
  198. package/src/inputs/SelectInput/SelectInput.utils.ts +205 -0
  199. package/src/inputs/SelectInput/TriggerButton/SelectInputTriggerButton.tsx +36 -0
  200. package/src/inputs/SelectInput/TriggerButton/index.ts +5 -0
  201. package/src/inputs/{SelectInput.docs.mdx → SelectInput/_stories/SelectInput.docs.mdx} +0 -1
  202. package/src/inputs/{SelectInput.story.tsx → SelectInput/_stories/SelectInput.story.tsx} +11 -8
  203. package/src/inputs/{SelectInput.test.story.tsx → SelectInput/_stories/SelectInput.test.story.tsx} +6 -10
  204. package/src/inputs/SelectInput/components.ts +10 -0
  205. package/src/inputs/SelectInput/index.ts +12 -0
  206. package/src/main.css +90 -90
  207. package/src/main.less +1 -1
  208. package/build/inputs/SelectInput.js +0 -890
  209. package/build/inputs/SelectInput.js.map +0 -1
  210. package/build/inputs/SelectInput.messages.js.map +0 -1
  211. package/build/inputs/SelectInput.messages.mjs.map +0 -1
  212. package/build/inputs/SelectInput.mjs +0 -881
  213. package/build/inputs/SelectInput.mjs.map +0 -1
  214. package/build/inputs/_BottomSheet.js.map +0 -1
  215. package/build/inputs/_BottomSheet.mjs.map +0 -1
  216. package/build/inputs/_ButtonInput.js.map +0 -1
  217. package/build/inputs/_ButtonInput.mjs.map +0 -1
  218. package/build/inputs/_Popover.js.map +0 -1
  219. package/build/inputs/_Popover.mjs.map +0 -1
  220. package/build/types/inputs/SelectInput.d.ts.map +0 -1
  221. package/build/types/inputs/SelectInput.messages.d.ts.map +0 -1
  222. package/build/types/inputs/_BottomSheet.d.ts.map +0 -1
  223. package/build/types/inputs/_ButtonInput.d.ts +0 -5
  224. package/build/types/inputs/_ButtonInput.d.ts.map +0 -1
  225. package/build/types/inputs/_Popover.d.ts.map +0 -1
  226. package/src/inputs/SelectInput.less +0 -219
  227. package/src/inputs/SelectInput.tsx +0 -1269
  228. package/build/inputs/{SelectInput.messages.js → SelectInput/SelectInput.messages.js} +0 -0
  229. package/build/inputs/{SelectInput.messages.mjs → SelectInput/SelectInput.messages.mjs} +0 -0
  230. package/build/styles/inputs/{SelectInput.css → SelectInput/SelectInput.css} +90 -90
  231. package/build/types/inputs/{SelectInput.messages.d.ts → SelectInput/SelectInput.messages.d.ts} +0 -0
  232. package/src/inputs/{_BottomSheet.less → SelectInput/BottomSheet/SelectInputBottomSheet.less} +0 -0
  233. package/src/inputs/{_ButtonInput.less → SelectInput/ButtonInput/SelectInputButtonInput.less} +0 -0
  234. package/src/inputs/{_Popover.less → SelectInput/Popover/SelectInputPopover.less} +0 -0
  235. package/src/inputs/{SelectInput.css → SelectInput/SelectInput.css} +90 -90
  236. /package/src/inputs/{SelectInput.messages.ts → SelectInput/SelectInput.messages.ts} +0 -0
@@ -0,0 +1,40 @@
1
+ import React, { createContext } from 'react';
2
+
3
+ /**
4
+ * Context for passing props to the SelectInputTriggerButton component.
5
+ */
6
+ export interface SelectInputTriggerButtonPropsContextValue {
7
+ ref?: React.ForwardedRef<HTMLButtonElement | null>;
8
+ id?: string;
9
+ onClick?: (event: React.MouseEvent) => void;
10
+ onKeyDown?: (event: React.KeyboardEvent) => void;
11
+ size?: 'sm' | 'md' | 'lg';
12
+ [key: string]: unknown;
13
+ }
14
+
15
+ /**
16
+ * Context for passing props to the SelectInputTriggerButton component.
17
+ */
18
+ export const SelectInputTriggerButtonPropsContext =
19
+ createContext<SelectInputTriggerButtonPropsContextValue>({});
20
+
21
+ /**
22
+ * Context for providing the total count of items in a SelectInput.
23
+ * Used for ARIA accessibility to inform screen readers about the total number of options.
24
+ */
25
+ export const SelectInputItemsCountContext = createContext<number | undefined>(undefined);
26
+
27
+ /**
28
+ * Context for providing the current item position in a SelectInput.
29
+ * Used for ARIA accessibility to inform screen readers about the position of the option.
30
+ */
31
+ export const SelectInputItemPositionContext = createContext<number | undefined>(undefined);
32
+
33
+ /**
34
+ * Context indicating whether an option's content is rendered within the trigger button.
35
+ * When true, certain styling adjustments are applied to make the content fit better in the trigger.
36
+ */
37
+ export const SelectInputOptionContentWithinTriggerContext = createContext(false);
38
+
39
+ // Re-export types from the original contexts module for backward compatibility
40
+ export type { WithInputAttributesProps } from '../contexts';
@@ -0,0 +1,22 @@
1
+ @import (reference) "../../../node_modules/@transferwise/neptune-css/src/less/ring.less";
2
+
3
+ // Import component styles
4
+ @import "./BottomSheet/SelectInputBottomSheet.less";
5
+ @import "./ButtonInput/SelectInputButtonInput.less";
6
+ @import "./Popover/SelectInputPopover.less";
7
+ @import "./Option/SelectInputOption.less";
8
+ @import "./OptionContent/SelectInputOptionContent.less";
9
+ @import "./ItemView/SelectInputItemView.less";
10
+ @import "./Options/SelectInputOptions.less";
11
+ @import "./ClearButton/SelectInputClearButton.less";
12
+
13
+ // Main SelectInput styles
14
+ .np-select-input-content {
15
+ overflow: hidden;
16
+ text-overflow: ellipsis;
17
+ white-space: nowrap;
18
+ }
19
+
20
+ .np-select-input-placeholder {
21
+ color: var(--color-content-tertiary);
22
+ }
@@ -2,15 +2,15 @@ import { screen, waitFor, within } from '@testing-library/react';
2
2
  import { userEvent } from '@testing-library/user-event';
3
3
  import { mockAnimationsApi } from 'jsdom-testing-mocks';
4
4
 
5
- import { render, mockMatchMedia, mockResizeObserver } from '../test-utils';
5
+ import { render, mockMatchMedia, mockResizeObserver } from '../../test-utils';
6
6
 
7
7
  import {
8
8
  SelectInput,
9
9
  SelectInputOptionContent,
10
10
  type SelectInputOptionItem,
11
11
  type SelectInputProps,
12
- } from './SelectInput';
13
- import { Field } from '../field/Field';
12
+ } from '.';
13
+ import { Field } from '../../field/Field';
14
14
 
15
15
  mockMatchMedia();
16
16
  mockResizeObserver();
@@ -40,7 +40,7 @@ describe('SelectInput', () => {
40
40
  ]}
41
41
  renderFooter={({ queryNormalized: normalizedQuery }) =>
42
42
  normalizedQuery != null ? (
43
- <>Showing results for {normalizedQuery}’</>
43
+ <>Showing results for &apos;{normalizedQuery}&apos;</>
44
44
  ) : (
45
45
  <>All items shown</>
46
46
  )
@@ -56,16 +56,16 @@ describe('SelectInput', () => {
56
56
  expect(footer).toBeInTheDocument();
57
57
 
58
58
  await userEvent.keyboard('u');
59
- expect(footer).toHaveTextContent(/‘u’$/);
59
+ expect(footer).toHaveTextContent(/'u'$/);
60
60
 
61
61
  await userEvent.keyboard('r');
62
- expect(footer).toHaveTextContent(/‘ur’$/);
62
+ expect(footer).toHaveTextContent(/'ur'$/);
63
63
 
64
64
  await userEvent.keyboard('x');
65
- expect(footer).toHaveTextContent(/‘urx’$/);
65
+ expect(footer).toHaveTextContent(/'urx'$/);
66
66
 
67
67
  await userEvent.keyboard('{Backspace}');
68
- expect(footer).toHaveTextContent(/‘ur’$/);
68
+ expect(footer).toHaveTextContent(/'ur'$/);
69
69
  });
70
70
 
71
71
  it('allows navigating the listbox with cursors', async () => {
@@ -76,9 +76,7 @@ describe('SelectInput', () => {
76
76
  { type: 'option', value: 'EUR' },
77
77
  { type: 'option', value: 'USD' },
78
78
  ]}
79
- renderFooter={({ queryNormalized: normalizedQuery }) => (
80
- <button type="button">Footer button</button>
81
- )}
79
+ renderFooter={() => <button type="button">Footer button</button>}
82
80
  filterable
83
81
  />,
84
82
  );
@@ -0,0 +1,257 @@
1
+ import mergeProps from 'merge-props';
2
+ import { useEffect, useRef, useState, useDeferredValue } from 'react';
3
+ import { Listbox as ListboxBase } from '@headlessui/react';
4
+ import { useScreenSize } from '../../common/hooks/useScreenSize';
5
+ import { Breakpoint } from '../../common/propsValues/breakpoint';
6
+ import { useEffectEvent } from '../../common/hooks/useEffectEvent';
7
+ import { useInputAttributes } from '../contexts';
8
+
9
+ import { SelectInputBottomSheet } from './BottomSheet';
10
+ import { SelectInputPopover } from './Popover';
11
+ import { SelectInputOptions } from './Options';
12
+ import { DefaultRenderTrigger } from './DefaultRenderTrigger';
13
+
14
+ import {
15
+ SelectInputOptionContentWithinTriggerContext,
16
+ SelectInputTriggerButtonPropsContext,
17
+ } from './SelectInput.contexts';
18
+ import { searchableString, sortByRelevance } from './SelectInput.utils';
19
+ import { SelectInputProps } from './SelectInput.types';
20
+
21
+ const noop = () => {};
22
+
23
+ /**
24
+ * SelectInput component allows users to select an option from a dropdown list.
25
+ * Supports filtering, multiple selection, and customization.
26
+ */
27
+ export function SelectInput<T = string, M extends boolean = false>({
28
+ id: idProp,
29
+ parentId,
30
+ name,
31
+ multiple,
32
+ placeholder,
33
+ autocomplete,
34
+ items,
35
+ defaultValue,
36
+ value: controlledValue,
37
+ compareValues,
38
+ renderValue = String,
39
+ renderFooter,
40
+ renderTrigger = DefaultRenderTrigger,
41
+ filterable,
42
+ filterPlaceholder,
43
+ sortFilteredOptions,
44
+ disabled,
45
+ size = 'md',
46
+ className,
47
+ UNSAFE_triggerButtonProps,
48
+ triggerRef: externalTriggerRef,
49
+ onFilterChange = noop,
50
+ onChange,
51
+ onOpen,
52
+ onClose,
53
+ onClear,
54
+ }: SelectInputProps<T, M>) {
55
+ const inputAttributes = useInputAttributes({ nonLabelable: true });
56
+ const id = idProp ?? inputAttributes.id;
57
+
58
+ const [open, setOpen] = useState(false);
59
+
60
+ const initialized = useRef(false);
61
+ const handleClose = useEffectEvent(onClose ?? (() => {}));
62
+ const handleOpen = useEffectEvent(onOpen ?? (() => {}));
63
+ useEffect(() => {
64
+ if (initialized.current) {
65
+ if (open) {
66
+ handleOpen?.();
67
+ } else {
68
+ handleClose?.();
69
+ }
70
+ } else {
71
+ initialized.current = true;
72
+ }
73
+ }, [handleClose, handleOpen, open]);
74
+
75
+ const [filterQuery, _setFilterQuery] = useState('');
76
+ const deferredFilterQuery = useDeferredValue(filterQuery);
77
+ const setFilterQuery = useEffectEvent((query: string) => {
78
+ _setFilterQuery(query);
79
+ if (query !== filterQuery) {
80
+ onFilterChange({
81
+ query,
82
+ queryNormalized: query ? searchableString(query) : null,
83
+ });
84
+ }
85
+ });
86
+
87
+ const internalTriggerRef = useRef<HTMLButtonElement | null>(null);
88
+
89
+ const screenSm = useScreenSize(Breakpoint.SMALL);
90
+ const OptionsOverlay = screenSm ? SelectInputPopover : SelectInputBottomSheet;
91
+
92
+ const searchInputRef = useRef<HTMLInputElement>(null);
93
+ const listboxRef = useRef<HTMLDivElement>(null);
94
+ const controllerRef = filterable ? searchInputRef : listboxRef;
95
+
96
+ /**
97
+ * Attempts to resolve the `listbox` label
98
+ * @see https://storybook.wise.design/?path=/docs/forms-selectinput-accessibility--docs#labelling
99
+ */
100
+ const getListBoxLabelProps = (): {
101
+ listBoxLabel?: string;
102
+ listBoxLabelledBy?: string;
103
+ } => {
104
+ if (UNSAFE_triggerButtonProps?.['aria-label']) {
105
+ return {
106
+ listBoxLabel: UNSAFE_triggerButtonProps['aria-label'],
107
+ };
108
+ }
109
+
110
+ if (UNSAFE_triggerButtonProps?.['aria-labelledby']) {
111
+ return {
112
+ listBoxLabelledBy: UNSAFE_triggerButtonProps['aria-labelledby'],
113
+ };
114
+ }
115
+
116
+ if (inputAttributes['aria-labelledby']) {
117
+ return {
118
+ listBoxLabelledBy: inputAttributes['aria-labelledby'],
119
+ };
120
+ }
121
+
122
+ return {};
123
+ };
124
+
125
+ return (
126
+ <ListboxBase
127
+ name={name}
128
+ multiple={multiple}
129
+ defaultValue={defaultValue as M extends true ? T[] : T}
130
+ value={controlledValue as M extends true ? T[] : T}
131
+ by={compareValues}
132
+ disabled={disabled}
133
+ onChange={
134
+ ((value) => {
135
+ if (!multiple) {
136
+ setOpen(false);
137
+ }
138
+ onChange?.(value);
139
+ }) satisfies SelectInputProps<T, M>['onChange']
140
+ }
141
+ >
142
+ {({ disabled: uiDisabled, value }) => {
143
+ const placeholderShown =
144
+ multiple && Array.isArray(value) ? value.length === 0 : value == null;
145
+ return (
146
+ <OptionsOverlay
147
+ placement="bottom-start"
148
+ open={open}
149
+ renderTrigger={({ ref, getInteractionProps }) => (
150
+ <SelectInputTriggerButtonPropsContext.Provider
151
+ // eslint-disable-next-line react/jsx-no-constructed-context-values
152
+ value={{
153
+ ref: (node) => {
154
+ ref(node);
155
+ if (externalTriggerRef) {
156
+ // eslint-disable-next-line no-param-reassign
157
+ externalTriggerRef.current = node;
158
+ } else {
159
+ internalTriggerRef.current = node;
160
+ }
161
+ },
162
+ size,
163
+ ...inputAttributes,
164
+ ...UNSAFE_triggerButtonProps,
165
+ id,
166
+ ...mergeProps(
167
+ {
168
+ onClick: () => {
169
+ setOpen((prev) => !prev);
170
+ },
171
+ onKeyDown: (event: React.KeyboardEvent) => {
172
+ if (
173
+ event.key === ' ' ||
174
+ event.key === 'Enter' ||
175
+ event.key === 'ArrowDown' ||
176
+ event.key === 'ArrowUp'
177
+ ) {
178
+ setOpen((prev) => !prev);
179
+ }
180
+ },
181
+ },
182
+ getInteractionProps(),
183
+ ),
184
+ }}
185
+ >
186
+ {renderTrigger({
187
+ content: !placeholderShown ? (
188
+ <SelectInputOptionContentWithinTriggerContext.Provider value>
189
+ {multiple && Array.isArray(value)
190
+ ? (value as readonly NonNullable<T>[])
191
+ .map((option) => renderValue(option, true))
192
+ .filter((node) => node != null)
193
+ .join(', ')
194
+ : renderValue(value as NonNullable<T>, true)}
195
+ </SelectInputOptionContentWithinTriggerContext.Provider>
196
+ ) : (
197
+ placeholder
198
+ ),
199
+ placeholderShown,
200
+ clear:
201
+ onClear != null
202
+ ? () => {
203
+ onClear();
204
+ (externalTriggerRef?.current ?? internalTriggerRef.current)?.focus({
205
+ preventScroll: true,
206
+ });
207
+ }
208
+ : undefined,
209
+ disabled: uiDisabled,
210
+ size,
211
+ className,
212
+ })}
213
+ </SelectInputTriggerButtonPropsContext.Provider>
214
+ )}
215
+ initialFocusRef={controllerRef}
216
+ size={filterable ? 'lg' : 'md'}
217
+ padding="none"
218
+ onClose={() => {
219
+ setOpen(false);
220
+ }}
221
+ onCloseEnd={() => {
222
+ setFilterQuery('');
223
+ }}
224
+ >
225
+ <SelectInputOptions
226
+ id={id ? `${id}Search` : undefined}
227
+ parentId={parentId}
228
+ items={items}
229
+ compareValues={compareValues}
230
+ renderValue={renderValue}
231
+ renderFooter={renderFooter}
232
+ filterable={filterable}
233
+ filterPlaceholder={filterPlaceholder}
234
+ sortFilteredOptions={sortFilteredOptions}
235
+ searchInputRef={searchInputRef}
236
+ listboxRef={listboxRef}
237
+ filterQuery={deferredFilterQuery}
238
+ autocomplete={autocomplete}
239
+ name={name}
240
+ onFilterChange={setFilterQuery}
241
+ onAutocompleteSelect={(matchedValue) => {
242
+ onChange?.(matchedValue as M extends true ? T[] : T);
243
+ if (!multiple) {
244
+ setOpen(false);
245
+ }
246
+ }}
247
+ {...getListBoxLabelProps()}
248
+ />
249
+ </OptionsOverlay>
250
+ );
251
+ }}
252
+ </ListboxBase>
253
+ );
254
+ }
255
+
256
+ // Attach sortByRelevance to the component for convenience
257
+ SelectInput.sortByRelevance = sortByRelevance;
@@ -0,0 +1,113 @@
1
+ import React, { ReactNode } from 'react';
2
+ import { ButtonProps } from '../../button/Button.types';
3
+ import { WithInputAttributesProps } from './SelectInput.contexts';
4
+
5
+ // Item interfaces
6
+ export interface SelectInputOptionItem<T = string> {
7
+ type: 'option';
8
+ value: T;
9
+ filterMatchers?: readonly string[];
10
+ disabled?: boolean;
11
+ }
12
+
13
+ export interface SelectInputGroupItem<T = string> {
14
+ type: 'group';
15
+ label: ReactNode;
16
+ options: readonly SelectInputOptionItem<T>[];
17
+ action?: {
18
+ label: string;
19
+ onClick: ButtonProps['onClick'];
20
+ };
21
+ }
22
+
23
+ export interface SelectInputSeparatorItem {
24
+ type: 'separator';
25
+ }
26
+
27
+ export type SelectInputItem<T = string> =
28
+ | SelectInputOptionItem<T>
29
+ | SelectInputGroupItem<T>
30
+ | SelectInputSeparatorItem;
31
+
32
+ // Main component props
33
+ export interface SelectInputProps<T = string, M extends boolean = false> {
34
+ id?: string;
35
+ /**
36
+ * Sets the `data-wds-parent` attribute on the listbox container, which is needed for complex components like DateInput to correctly manage event handling.
37
+ * @internal
38
+ */
39
+ parentId?: string;
40
+ name?: string;
41
+ multiple?: M;
42
+ placeholder?: string;
43
+ items: readonly SelectInputItem<NonNullable<T>>[];
44
+ /**
45
+ * Enables browser autocomplete integration through the search input.
46
+ * Accepts standard HTML autocomplete values (e.g., "country-name", "address-level1").
47
+ *
48
+ * Requires `filterable={true}` to enable the search input.
49
+ *
50
+ * @example
51
+ * <SelectInput
52
+ * name="country"
53
+ * autocomplete="country-name"
54
+ * filterable={true}
55
+ * items={[{
56
+ * type: 'option',
57
+ * value: 'GB',
58
+ * filterMatchers: ['United Kingdom', 'UK']
59
+ * }]}
60
+ * />
61
+ */
62
+ autocomplete?: string;
63
+ defaultValue?: M extends true ? readonly T[] : T;
64
+ value?: M extends true ? readonly T[] : T;
65
+ compareValues?:
66
+ | (keyof NonNullable<T> & string)
67
+ | ((a: T | undefined, b: T | undefined) => boolean);
68
+ renderValue?: (value: NonNullable<T>, withinTrigger: boolean) => React.ReactNode;
69
+ renderFooter?: (args: {
70
+ resultsEmpty: boolean;
71
+ queryNormalized: string | null | undefined;
72
+ }) => React.ReactNode;
73
+ renderTrigger?: (args: {
74
+ content: React.ReactNode;
75
+ placeholderShown: boolean;
76
+ clear: (() => void) | undefined;
77
+ disabled: boolean;
78
+ size: 'sm' | 'md' | 'lg';
79
+ className: string | undefined;
80
+ }) => React.ReactNode;
81
+ filterable?: boolean;
82
+ filterPlaceholder?: string;
83
+ sortFilteredOptions?: (
84
+ a: SelectInputOptionItem<NonNullable<T>>,
85
+ b: SelectInputOptionItem<NonNullable<T>>,
86
+ searchQuery: string,
87
+ ) => number;
88
+ disabled?: boolean;
89
+ size?: 'sm' | 'md' | 'lg';
90
+ className?: string;
91
+ UNSAFE_triggerButtonProps?: WithInputAttributesProps['inputAttributes'] & {
92
+ 'aria-label'?: string;
93
+ };
94
+ /** Ref to the select trigger button element. */
95
+ triggerRef?: React.MutableRefObject<HTMLButtonElement | null>;
96
+ onFilterChange?: (args: { query: string; queryNormalized: string | null }) => void;
97
+ onChange?: (value: M extends true ? T[] : T) => void;
98
+ onOpen?: () => void;
99
+ onClose?: () => void;
100
+ onClear?: () => void;
101
+ }
102
+
103
+ export type {
104
+ SelectInputTriggerButtonElementType,
105
+ SelectInputTriggerButtonProps,
106
+ } from './TriggerButton';
107
+ export type { SelectInputClearButtonProps } from './ClearButton';
108
+ export type { SelectInputOptionContentProps } from './OptionContent';
109
+ export type { SelectInputOptionProps } from './Option';
110
+ export type { SelectInputItemViewProps } from './ItemView';
111
+ export type { SelectInputGroupItemViewProps } from './ItemView/GroupItemView';
112
+ export type { SelectInputOptionsProps } from './Options';
113
+ export type { SelectInputOptionsContainerProps } from './Options/OptionsContainer';
@@ -0,0 +1,205 @@
1
+ import { SelectInputItem, SelectInputOptionItem } from './SelectInput.types';
2
+
3
+ export const MAX_ITEMS_WITHOUT_VIRTUALIZATION = 50;
4
+
5
+ /**
6
+ * Converts a string to a normalized, searchable format by:
7
+ * - Trimming whitespace
8
+ * - Normalizing whitespace (convert multiple spaces to single space)
9
+ * - Converting to NFD normalization form to handle diacritics
10
+ * - Removing combining diacritical marks
11
+ * - Converting to lowercase
12
+ */
13
+ export function searchableString(value: string) {
14
+ return (
15
+ value
16
+ .trim()
17
+ .replace(/\s+/gu, ' ')
18
+ // NFD converts an Å to A + ̊ (and other special characters)
19
+ .normalize('NFD')
20
+ // and then this replaces the ̊ with nothing (and other special characters)
21
+ .replace(/[\u0300-\u036f]/g, '')
22
+ .toLowerCase()
23
+ );
24
+ }
25
+
26
+ /**
27
+ * Extracts searchable strings from a value.
28
+ * - If the value is a string, returns a normalized version.
29
+ * - If the value is an object, extracts all string values and normalizes them.
30
+ * - Otherwise returns an empty array.
31
+ */
32
+ export function inferSearchableStrings(value: unknown) {
33
+ if (typeof value === 'string') {
34
+ return [searchableString(value)];
35
+ }
36
+
37
+ if (typeof value === 'object' && value != null) {
38
+ return Object.values(value)
39
+ .filter((innerValue) => typeof innerValue === 'string')
40
+ .map((innerValue) => searchableString(innerValue));
41
+ }
42
+
43
+ return [];
44
+ }
45
+
46
+ /**
47
+ * Sets the value of a duplicate option item to undefined, effectively hiding it when rendered.
48
+ */
49
+ export function dedupeSelectInputOptionItem<T>(
50
+ item: SelectInputOptionItem<T>,
51
+ existingValues: Set<T>,
52
+ compareValues?: (a: T, b: T) => boolean,
53
+ ): SelectInputOptionItem<T | undefined> {
54
+ const isDuplicate = compareValues
55
+ ? Array.from(existingValues).some((existingValue) => compareValues(item.value, existingValue))
56
+ : existingValues.has(item.value);
57
+
58
+ if (!isDuplicate) {
59
+ existingValues.add(item.value);
60
+ return item;
61
+ }
62
+ return { ...item, value: undefined };
63
+ }
64
+
65
+ /**
66
+ * Sets the `value` of duplicate option items to `undefined`, hiding them when
67
+ * rendered. Indexes are kept intact within groups to preserve the active item
68
+ * between filter changes when possible.
69
+ */
70
+ export function dedupeSelectInputItems<T>(
71
+ items: readonly SelectInputItem<T>[],
72
+ compareValues?: (a: T, b: T) => boolean,
73
+ ): SelectInputItem<T | undefined>[] {
74
+ const existingValues = new Set<T>();
75
+
76
+ return items.map((item) => {
77
+ switch (item.type) {
78
+ case 'option': {
79
+ return dedupeSelectInputOptionItem(item, existingValues, compareValues);
80
+ }
81
+ case 'group': {
82
+ return {
83
+ ...item,
84
+ options: item.options.map((option) =>
85
+ dedupeSelectInputOptionItem(option, existingValues, compareValues),
86
+ ),
87
+ };
88
+ }
89
+ default:
90
+ }
91
+ return item;
92
+ });
93
+ }
94
+
95
+ /**
96
+ * Checks if a SelectInputOptionItem matches the search needle.
97
+ */
98
+ export function selectInputOptionItemIncludesNeedle<T>(
99
+ item: SelectInputOptionItem<T>,
100
+ needle: string,
101
+ ) {
102
+ return inferSearchableStrings(item.filterMatchers ?? item.value).some((haystack) =>
103
+ haystack.includes(needle),
104
+ );
105
+ }
106
+
107
+ /**
108
+ * Filters SelectInputItems based on the provided predicate function.
109
+ * For group items, it checks if any of their options match the predicate.
110
+ */
111
+ export function filterSelectInputItems<T>(
112
+ items: readonly SelectInputItem<T>[],
113
+ predicate: (item: SelectInputOptionItem<T>) => boolean,
114
+ ) {
115
+ return items.filter((item) => {
116
+ switch (item.type) {
117
+ case 'option': {
118
+ return predicate(item);
119
+ }
120
+ case 'group': {
121
+ return item.options.some((option) => predicate(option));
122
+ }
123
+ default:
124
+ }
125
+ return false;
126
+ });
127
+ }
128
+
129
+ /**
130
+ * Flattens and sorts filtered options using the provided comparator.
131
+ * Extracts all options from groups, filters out undefined values (deduplicated items),
132
+ * sorts them, and returns as a flat list of option items.
133
+ */
134
+ export function sortSelectInputItems<T>(
135
+ items: readonly SelectInputItem<T | undefined>[],
136
+ compareFn: (
137
+ a: SelectInputOptionItem<NonNullable<T>>,
138
+ b: SelectInputOptionItem<NonNullable<T>>,
139
+ searchQuery: string,
140
+ ) => number,
141
+ searchQuery: string,
142
+ ): SelectInputItem<NonNullable<T>>[] {
143
+ const flattenedOption = items.flatMap((item) => {
144
+ if (item.type === 'option') {
145
+ return item.value !== undefined ? [item as SelectInputOptionItem<NonNullable<T>>] : [];
146
+ }
147
+
148
+ if (item.type === 'group') {
149
+ return item.options.filter(
150
+ (option): option is SelectInputOptionItem<NonNullable<T>> => option.value !== undefined,
151
+ );
152
+ }
153
+
154
+ return [];
155
+ });
156
+
157
+ // eslint-disable-next-line functional/immutable-data
158
+ return flattenedOption.sort((a, b) => compareFn(a, b, searchQuery));
159
+ }
160
+
161
+ /**
162
+ * A prebuilt sort function for `sortFilteredOptions` that sorts options by relevance to the search query.
163
+ * Prioritizes: exact matches > starts with > contains > alphabetical.
164
+ *
165
+ * @param getLabel - Function to extract the label string from the option value. Defaults to using `title` property.
166
+ *
167
+ * @example
168
+ * ```tsx
169
+ * <SelectInput
170
+ * filterable
171
+ * sortFilteredOptions={sortByRelevance((value) => value.name)}
172
+ * // ...
173
+ * />
174
+ * ```
175
+ */
176
+ export function sortByRelevance<T>(
177
+ getLabel: (value: T) => string = (value) => (value as { title: string }).title,
178
+ ): (a: SelectInputOptionItem<T>, b: SelectInputOptionItem<T>, searchQuery: string) => number {
179
+ return (a, b, searchQuery) => {
180
+ const normalizedQuery = searchQuery.toLowerCase();
181
+ const labelA = getLabel(a.value).toLowerCase();
182
+ const labelB = getLabel(b.value).toLowerCase();
183
+
184
+ // Prioritize exact matches
185
+ const aExactMatch = labelA === normalizedQuery;
186
+ const bExactMatch = labelB === normalizedQuery;
187
+ if (aExactMatch && !bExactMatch) return -1;
188
+ if (!aExactMatch && bExactMatch) return 1;
189
+
190
+ // Then prioritize options where label starts with the search query
191
+ const aStartsWith = labelA.startsWith(normalizedQuery);
192
+ const bStartsWith = labelB.startsWith(normalizedQuery);
193
+ if (aStartsWith && !bStartsWith) return -1;
194
+ if (!aStartsWith && bStartsWith) return 1;
195
+
196
+ // Then prioritize options where label contains the search query
197
+ const aContains = labelA.includes(normalizedQuery);
198
+ const bContains = labelB.includes(normalizedQuery);
199
+ if (aContains && !bContains) return -1;
200
+ if (!aContains && bContains) return 1;
201
+
202
+ // Finally sort alphabetically
203
+ return labelA.localeCompare(labelB);
204
+ };
205
+ }