@transferwise/components 46.131.2 → 46.132.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/build/actionOption/ActionOption.js.map +1 -1
  2. package/build/actionOption/ActionOption.mjs.map +1 -1
  3. package/build/alert/Alert.js +1 -1
  4. package/build/alert/Alert.js.map +1 -1
  5. package/build/alert/Alert.mjs +1 -1
  6. package/build/alert/Alert.mjs.map +1 -1
  7. package/build/checkboxOption/CheckboxOption.js.map +1 -1
  8. package/build/checkboxOption/CheckboxOption.mjs.map +1 -1
  9. package/build/common/Option/Option.js.map +1 -1
  10. package/build/common/Option/Option.mjs.map +1 -1
  11. package/build/common/liveRegion/LiveRegion.js +46 -7
  12. package/build/common/liveRegion/LiveRegion.js.map +1 -1
  13. package/build/common/liveRegion/LiveRegion.mjs +46 -7
  14. package/build/common/liveRegion/LiveRegion.mjs.map +1 -1
  15. package/build/flowNavigation/FlowNavigation.js +1 -0
  16. package/build/flowNavigation/FlowNavigation.js.map +1 -1
  17. package/build/flowNavigation/FlowNavigation.mjs +1 -0
  18. package/build/flowNavigation/FlowNavigation.mjs.map +1 -1
  19. package/build/legacylistItem/LegacyListItem.js.map +1 -1
  20. package/build/legacylistItem/LegacyListItem.mjs.map +1 -1
  21. package/build/main.css +52 -1
  22. package/build/navigationOption/NavigationOption.js.map +1 -1
  23. package/build/navigationOption/NavigationOption.mjs.map +1 -1
  24. package/build/overlayHeader/OverlayHeader.js +1 -0
  25. package/build/overlayHeader/OverlayHeader.js.map +1 -1
  26. package/build/overlayHeader/OverlayHeader.mjs +1 -0
  27. package/build/overlayHeader/OverlayHeader.mjs.map +1 -1
  28. package/build/prompt/InfoPrompt/InfoPrompt.js +2 -0
  29. package/build/prompt/InfoPrompt/InfoPrompt.js.map +1 -1
  30. package/build/prompt/InfoPrompt/InfoPrompt.mjs +2 -0
  31. package/build/prompt/InfoPrompt/InfoPrompt.mjs.map +1 -1
  32. package/build/radioOption/RadioOption.js.map +1 -1
  33. package/build/radioOption/RadioOption.mjs.map +1 -1
  34. package/build/styles/common/liveRegion/LiveRegion.css +3 -0
  35. package/build/styles/css/neptune.css +48 -1
  36. package/build/styles/main.css +52 -1
  37. package/build/styles/styles/less/neptune.css +48 -1
  38. package/build/summary/Summary.js +1 -1
  39. package/build/summary/Summary.js.map +1 -1
  40. package/build/summary/Summary.mjs +1 -1
  41. package/build/summary/Summary.mjs.map +1 -1
  42. package/build/switchOption/SwitchOption.js +1 -1
  43. package/build/switchOption/SwitchOption.js.map +1 -1
  44. package/build/switchOption/SwitchOption.mjs +1 -1
  45. package/build/switchOption/SwitchOption.mjs.map +1 -1
  46. package/build/types/actionOption/ActionOption.d.ts +1 -1
  47. package/build/types/alert/Alert.d.ts +1 -1
  48. package/build/types/checkboxOption/CheckboxOption.d.ts +1 -1
  49. package/build/types/common/Option/Option.d.ts +3 -0
  50. package/build/types/common/Option/Option.d.ts.map +1 -1
  51. package/build/types/common/liveRegion/LiveRegion.d.ts +5 -2
  52. package/build/types/common/liveRegion/LiveRegion.d.ts.map +1 -1
  53. package/build/types/legacylistItem/LegacyListItem.d.ts +1 -1
  54. package/build/types/navigationOption/NavigationOption.d.ts +1 -1
  55. package/build/types/prompt/InfoPrompt/InfoPrompt.d.ts +2 -2
  56. package/build/types/prompt/InfoPrompt/InfoPrompt.d.ts.map +1 -1
  57. package/build/types/radioOption/RadioOption.d.ts +1 -1
  58. package/build/types/summary/Summary.d.ts +1 -1
  59. package/build/types/switchOption/SwitchOption.d.ts +1 -1
  60. package/package.json +2 -2
  61. package/src/actionOption/ActionOption.story.tsx +2 -1
  62. package/src/actionOption/ActionOption.tsx +1 -1
  63. package/src/alert/Alert.story.tsx +1 -7
  64. package/src/alert/Alert.tsx +1 -1
  65. package/src/button/_stories/Button.story.tsx +0 -5
  66. package/src/checkboxButton/CheckboxButton.story.tsx +0 -1
  67. package/src/checkboxOption/CheckboxOption.story.tsx +2 -1
  68. package/src/checkboxOption/CheckboxOption.tsx +1 -1
  69. package/src/circularButton/CircularButton.story.tsx +0 -1
  70. package/src/common/Option/Option.tsx +3 -0
  71. package/src/common/liveRegion/LiveRegion.css +3 -0
  72. package/src/common/liveRegion/LiveRegion.less +3 -0
  73. package/src/common/liveRegion/LiveRegion.test.tsx +69 -2
  74. package/src/common/liveRegion/LiveRegion.tsx +77 -8
  75. package/src/display/Display.story.tsx +15 -1
  76. package/src/expressiveMoneyInput/ExpressiveMoneyInput.story.tsx +0 -1
  77. package/src/header/Header.story.tsx +0 -5
  78. package/src/inputWithDisplayFormat/InputWithDisplayFormat.story.tsx +0 -1
  79. package/src/inputs/SelectInput/_stories/SelectInput.docs.mdx +62 -0
  80. package/src/inputs/SelectInput/_stories/SelectInput.story.tsx +796 -220
  81. package/src/inputs/SelectInput/_stories/SelectInput.test.story.tsx +433 -4
  82. package/src/legacylistItem/LegacyListItem.story.tsx +2 -1
  83. package/src/legacylistItem/LegacyListItem.tsx +1 -1
  84. package/src/listItem/AdditionalInfo/ListItemAdditionalInfo.story.tsx +0 -5
  85. package/src/listItem/AvatarLayout/ListItemAvatarLayout.story.tsx +0 -5
  86. package/src/listItem/AvatarView/ListItemAvatarView.story.tsx +0 -5
  87. package/src/listItem/Button/ListItemButton.story.tsx +0 -5
  88. package/src/listItem/Checkbox/ListItemCheckbox.story.tsx +0 -5
  89. package/src/listItem/IconButton/ListItemIconButton.story.tsx +0 -5
  90. package/src/listItem/Image/ListItemImage.story.tsx +0 -5
  91. package/src/listItem/Navigation/ListItemNavigation.story.tsx +0 -5
  92. package/src/listItem/Prompt/ListItemPrompt.story.tsx +1 -5
  93. package/src/listItem/Radio/ListItemRadio.story.tsx +0 -5
  94. package/src/listItem/Switch/ListItemSwitch.story.tsx +0 -5
  95. package/src/listItem/_stories/ListItem.disabled.story.tsx +0 -1
  96. package/src/listItem/_stories/ListItem.scenarios.story.tsx +0 -1
  97. package/src/listItem/_stories/ListItem.story.tsx +1 -6
  98. package/src/main.css +52 -1
  99. package/src/main.less +1 -0
  100. package/src/modal/Modal.story.tsx +0 -1
  101. package/src/navigationOption/NavigationOption.story.tsx +2 -1
  102. package/src/navigationOption/NavigationOption.tsx +1 -1
  103. package/src/popover/Popover.story.tsx +0 -1
  104. package/src/prompt/ActionPrompt/ActionPrompt.story.tsx +0 -5
  105. package/src/prompt/InfoPrompt/InfoPrompt.story.tsx +0 -5
  106. package/src/prompt/InfoPrompt/InfoPrompt.test.story.tsx +142 -5
  107. package/src/prompt/InfoPrompt/InfoPrompt.test.tsx +11 -6
  108. package/src/prompt/InfoPrompt/InfoPrompt.tsx +4 -3
  109. package/src/prompt/InlinePrompt/InlinePrompt.story.tsx +0 -5
  110. package/src/provider/theme/ThemeProvider.story.tsx +8 -0
  111. package/src/radioOption/RadioOption.story.tsx +2 -1
  112. package/src/radioOption/RadioOption.tsx +1 -1
  113. package/src/sentimentSurface/SentimentSurface.story.tsx +0 -5
  114. package/src/sticky/Sticky.story.tsx +0 -1
  115. package/src/styles/less/core/_typography.less +15 -2
  116. package/src/styles/less/neptune.css +48 -1
  117. package/src/summary/Summary.story.tsx +1 -1
  118. package/src/summary/Summary.tsx +1 -1
  119. package/src/switchOption/SwitchOption.story.tsx +2 -1
  120. package/src/switchOption/SwitchOption.tsx +1 -1
  121. package/src/tokens/tokens.story.tsx +1 -1
@@ -1,5 +1,5 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-webpack5';
2
- import { expect, fn, type Mock, screen, userEvent, within, waitFor } from 'storybook/test';
2
+ import { fn, type Mock } from 'storybook/test';
3
3
  import { Calendar, ChevronDown } from '@transferwise/icons';
4
4
  import { Flag } from '@wise/art';
5
5
  import { clsx } from 'clsx';
@@ -10,7 +10,6 @@ import { getMonthNames } from '../../../common/dateUtils';
10
10
  import Drawer from '../../../drawer';
11
11
  import { Field } from '../../../field/Field';
12
12
  import Modal from '../../../modal';
13
- import { wait } from '../../../test-utils/wait';
14
13
  import {
15
14
  SelectInput,
16
15
  type SelectInputItem,
@@ -19,6 +18,12 @@ import {
19
18
  SelectInputTriggerButton,
20
19
  } from '..';
21
20
 
21
+ /**
22
+ * A searchable, accessible combobox for selecting a single or multiple values.
23
+ * Supports flat and grouped items, optional filtering, custom triggers, a clear button,
24
+ * and a custom footer. Renders a popover on desktop and a bottom sheet on mobile.
25
+ * Lists with 50+ items are automatically virtualised.
26
+ */
22
27
  const meta = {
23
28
  title: 'Forms/SelectInput',
24
29
  component: SelectInput,
@@ -30,14 +35,33 @@ const meta = {
30
35
  },
31
36
  argTypes: {
32
37
  parentId: {
38
+ table: { category: 'WDS internal' },
39
+ },
40
+ renderTrigger: {
33
41
  table: {
34
- category: 'WDS internal',
42
+ defaultValue: { summary: 'Default trigger (InputGroup + ButtonInput)' },
35
43
  },
36
44
  },
45
+ // Events
46
+ onClear: {
47
+ table: { category: 'Events' },
48
+ },
49
+ onFilterChange: {
50
+ table: { category: 'Events' },
51
+ },
52
+ onChange: {
53
+ table: { category: 'Events' },
54
+ },
55
+ onClose: {
56
+ table: { category: 'Events' },
57
+ },
58
+ onOpen: {
59
+ table: { category: 'Events' },
60
+ },
37
61
  },
38
62
  parameters: {
39
- docs: { toc: true },
40
63
  actions: { argTypesRegex: '' },
64
+ docs: { toc: true },
41
65
  },
42
66
  } satisfies Meta<typeof SelectInput>;
43
67
  export default meta;
@@ -54,57 +78,6 @@ const months: Month[] = getMonthNames('en-US').map((name, index) => ({
54
78
  name,
55
79
  }));
56
80
 
57
- export const Months: Story<Month | null> = {
58
- args: {
59
- placeholder: 'Month',
60
- items: months.map((month) => ({
61
- type: 'option',
62
- value: month,
63
- })),
64
- renderValue: (month) => <SelectInputOptionContent title={month.name} />,
65
- },
66
- render: function Render({ onChange, onClear, ...args }) {
67
- const [selectedMonth, setSelectedMonth] = useState<Month | null>(null);
68
-
69
- return (
70
- <SelectInput
71
- {...args}
72
- value={selectedMonth}
73
- onChange={(month) => {
74
- setSelectedMonth(month);
75
- onChange?.(month);
76
- }}
77
- onClear={() => {
78
- setSelectedMonth(null);
79
- onClear?.();
80
- }}
81
- />
82
- );
83
- },
84
- play: async ({ canvasElement, step }) => {
85
- const canvas = within(canvasElement);
86
-
87
- await step('renders placeholder', async () => {
88
- const triggerButton = canvas.getByRole('combobox');
89
- await waitFor(async () => expect(triggerButton).toHaveTextContent('Month'));
90
- });
91
-
92
- await step('selects option via mouse', async () => {
93
- const triggerButton = canvas.getByRole('combobox');
94
-
95
- await userEvent.click(triggerButton);
96
- await userEvent.unhover(triggerButton);
97
-
98
- const option = within(screen.getByRole('listbox')).getByRole('option', {
99
- name: 'May',
100
- });
101
- await userEvent.click(option);
102
-
103
- await waitFor(async () => expect(triggerButton).toHaveTextContent('May'));
104
- });
105
- },
106
- };
107
-
108
81
  interface Currency {
109
82
  code: string;
110
83
  name: string;
@@ -140,9 +113,9 @@ const otherCurrencies: Currency[] = [
140
113
  name: 'Australian dollar',
141
114
  },
142
115
  {
143
- code: 'ÅLD',
144
- name: 'Ålandian peso',
145
- countries: ['Ålandia'],
116
+ code: 'JPY',
117
+ name: 'Japanese yen',
118
+ countries: ['Japan'],
146
119
  },
147
120
  ];
148
121
 
@@ -182,18 +155,18 @@ const CurrenciesArgs = {
182
155
  renderFooter: ({ resultsEmpty, queryNormalized }) =>
183
156
  resultsEmpty && queryNormalized != null && /^[a-z]{3}$/u.test(queryNormalized) ? (
184
157
  <>
185
- It’s not possible use {queryNormalized.toUpperCase()} yet.{' '}
158
+ It is not possible to use {queryNormalized.toUpperCase()} yet.{' '}
186
159
  <a href="#_" className="np-text-link-default" onClick={(event) => event.preventDefault()}>
187
- Email me when it’s available.
160
+ Email me when it is available.
188
161
  </a>
189
162
  </>
190
163
  ) : (
191
164
  <>
192
- Can’t find it?{' '}
165
+ Cannot find it?{' '}
193
166
  <a href="#_" className="np-text-link-default" onClick={(event) => event.preventDefault()}>
194
167
  Request the currency you need,
195
168
  </a>{' '}
196
- and we’ll notify you once it’s available.
169
+ and we will notify you once it is available.
197
170
  </>
198
171
  ),
199
172
  filterable: true,
@@ -201,69 +174,244 @@ const CurrenciesArgs = {
201
174
  size: 'lg',
202
175
  } satisfies Story<Currency>['args'];
203
176
 
204
- export const Currencies: Story<Currency> = {
205
- args: CurrenciesArgs,
206
- play: async ({ step }) => {
207
- await step('Tab to the combobox', async () => {
208
- await userEvent.tab();
209
- });
210
-
211
- await step('Open the combobox', async () => {
212
- await userEvent.keyboard(' ');
213
- });
214
-
215
- await step('Check if options are displayed', async () => {
216
- await waitFor(async () => expect(await screen.findAllByRole('option')).toHaveLength(9));
217
- await waitFor(async () => expect(screen.getByText(/^Can’t find it?/u)).toBeInTheDocument());
218
- });
219
-
220
- await step('Type "huf" in the combobox', async () => {
221
- // this is workaround for Chromatic: Firefox and Edge tests failing.
222
- // Unclear on a proper solution
223
- await wait(750);
224
- const input = await screen.findByRole('combobox');
225
- await userEvent.type(input, 'huf');
226
- });
227
-
228
- await step('Check if no options are displayed', async () => {
229
- // this is workaround for Chromatic: Firefox and Edge tests failing.
230
- // Unclear on a proper solution
231
- await wait(750);
232
- await expect(await screen.findByText(/^No results found/u)).toBeInTheDocument();
233
- await expect(await screen.findByText(/^It’s not possible use HUF yet./u)).toBeInTheDocument();
234
- await waitFor(
235
- async () => {
236
- await expect(screen.queryAllByRole('option')).toHaveLength(0);
237
- },
238
- { timeout: 1500 },
177
+ const previewArgGroup = {
178
+ table: { category: 'Storybook Preview', type: { summary: undefined } },
179
+ };
180
+
181
+ type PlaygroundArgs = {
182
+ size?: 'sm' | 'md' | 'lg';
183
+ filterable?: boolean;
184
+ filterPlaceholder?: string;
185
+ disabled?: boolean;
186
+ placeholder?: string;
187
+ multiple?: boolean;
188
+ previewWithGroups: boolean;
189
+ previewClearable: boolean;
190
+ previewWithFooter: boolean;
191
+ };
192
+
193
+ export const Playground: StoryObj<PlaygroundArgs> = {
194
+ argTypes: {
195
+ previewWithGroups: {
196
+ name: 'Preview with groups',
197
+ control: 'boolean',
198
+ ...previewArgGroup,
199
+ },
200
+ previewClearable: {
201
+ name: 'Preview with clear button',
202
+ control: 'boolean',
203
+ ...previewArgGroup,
204
+ },
205
+ previewWithFooter: {
206
+ name: 'Preview with footer',
207
+ control: 'boolean',
208
+ ...previewArgGroup,
209
+ },
210
+ },
211
+ args: {
212
+ disabled: false,
213
+ multiple: false,
214
+ filterable: true,
215
+ filterPlaceholder: 'Type a currency / country',
216
+ placeholder: 'Select a currency',
217
+ size: 'lg',
218
+ previewWithGroups: true,
219
+ previewClearable: true,
220
+ previewWithFooter: true,
221
+ },
222
+ render: function Render({
223
+ previewWithGroups,
224
+ previewClearable,
225
+ previewWithFooter,
226
+ multiple,
227
+ filterPlaceholder,
228
+ ...args
229
+ }) {
230
+ const [singleValue, setSingleValue] = useState<Currency | undefined>(undefined);
231
+ const [multiValue, setMultiValue] = useState<Currency[]>([]);
232
+
233
+ const items: SelectInputItem<Currency>[] = previewWithGroups
234
+ ? [
235
+ {
236
+ type: 'group',
237
+ label: 'Popular currencies',
238
+ options: popularCurrencies.map(currencyOption),
239
+ },
240
+ { type: 'group', label: 'All currencies', options: allCurrencies.map(currencyOption) },
241
+ ]
242
+ : allCurrencies.map(currencyOption);
243
+
244
+ const renderValue = (currency: Currency, withinTrigger: boolean) => {
245
+ if (multiple && withinTrigger) return currency.code;
246
+ return (
247
+ <SelectInputOptionContent
248
+ title={currency.code}
249
+ note={withinTrigger ? undefined : currency.name}
250
+ icon={<Flag code={currency.code} intrinsicSize={24} />}
251
+ />
239
252
  );
240
- });
253
+ };
241
254
 
242
- await step('Remove last two characters', async () => {
243
- await wait(500);
244
- const input = await screen.findByRole('combobox');
245
- await userEvent.type(input, '{Backspace}{Backspace}');
246
- });
255
+ const renderFooter = previewWithFooter
256
+ ? ({
257
+ resultsEmpty,
258
+ queryNormalized,
259
+ }: {
260
+ resultsEmpty: boolean;
261
+ queryNormalized: string | null | undefined;
262
+ }) =>
263
+ resultsEmpty && queryNormalized != null && /^[a-z]{3}$/u.test(queryNormalized) ? (
264
+ <>It is not possible to use {queryNormalized.toUpperCase()} yet.</>
265
+ ) : (
266
+ <>
267
+ Cannot find it?{' '}
268
+ <a href="#_" className="np-text-link-default" onClick={(e) => e.preventDefault()}>
269
+ Request the currency
270
+ </a>
271
+ </>
272
+ )
273
+ : undefined;
247
274
 
248
- await step('Check if two options are displayed', async () => {
249
- await waitFor(async () =>
250
- expect(within(screen.getByRole('listbox')).queryAllByRole('option')).toHaveLength(2),
275
+ if (multiple) {
276
+ return (
277
+ <SelectInput<Currency, true>
278
+ {...(args as SelectInputProps<Currency, true>)}
279
+ multiple
280
+ items={items}
281
+ value={multiValue}
282
+ renderValue={renderValue}
283
+ renderFooter={renderFooter}
284
+ filterPlaceholder={filterPlaceholder}
285
+ onClear={previewClearable ? () => setMultiValue([]) : undefined}
286
+ onChange={setMultiValue}
287
+ />
251
288
  );
252
- });
289
+ }
290
+
291
+ return (
292
+ <SelectInput<Currency>
293
+ {...(args as SelectInputProps<Currency>)}
294
+ items={items}
295
+ value={singleValue}
296
+ renderValue={renderValue}
297
+ renderFooter={renderFooter}
298
+ filterPlaceholder={filterPlaceholder}
299
+ onClear={previewClearable ? () => setSingleValue(undefined) : undefined}
300
+ onChange={setSingleValue}
301
+ />
302
+ );
303
+ },
304
+ parameters: {
305
+ docs: {
306
+ canvas: { sourceState: 'hidden' },
307
+ },
308
+ },
309
+ };
310
+
311
+ /**
312
+ * The simplest usage: a flat list of options with no icons or grouping.
313
+ * `renderValue` controls what is shown in both the trigger and each dropdown row.
314
+ */
315
+ export const Basic: Story<Month | null> = {
316
+ args: {
317
+ placeholder: 'Month',
318
+ items: months.map((month) => ({
319
+ type: 'option',
320
+ value: month,
321
+ })),
322
+ renderValue: (month) => <SelectInputOptionContent title={month.name} />,
323
+ },
324
+ render: function Render({ onChange, onClear, ...args }) {
325
+ const [selectedMonth, setSelectedMonth] = useState<Month | null>(null);
253
326
 
254
- await step('Type "e" in the combobox', async () => {
255
- const input = await screen.findByRole('combobox');
256
- await userEvent.type(input, '{Backspace}e');
257
- });
327
+ return (
328
+ <SelectInput
329
+ {...args}
330
+ value={selectedMonth}
331
+ onChange={(month) => {
332
+ setSelectedMonth(month);
333
+ onChange?.(month);
334
+ }}
335
+ onClear={() => {
336
+ setSelectedMonth(null);
337
+ onClear?.();
338
+ }}
339
+ />
340
+ );
341
+ },
342
+ parameters: {
343
+ docs: {
344
+ source: {
345
+ code: `<SelectInput
346
+ placeholder="Month"
347
+ items={[
348
+ { type: 'option', value: { id: 1, name: 'January' } },
349
+ { type: 'option', value: { id: 2, name: 'February' } },
350
+ // ... 10 more months
351
+ ]}
352
+ renderValue={(month) => <SelectInputOptionContent title={month.name} />}
353
+ value={selectedMonth}
354
+ onChange={setSelectedMonth}
355
+ />`,
356
+ },
357
+ },
358
+ },
359
+ };
258
360
 
259
- await step('Check if aria-activedescendant is not set', async () => {
260
- const input = screen.getByRole('combobox');
261
- await waitFor(async () => expect(input).toHaveAttribute('aria-activedescendant'));
262
- });
361
+ /**
362
+ * A real-world currency selector with grouped items, flag icons, filterable search,
363
+ * and a custom footer. `renderValue` receives a `withinTrigger` boolean to render
364
+ * a compact version inside the trigger/selected state (code only, no full currency name).
365
+ */
366
+ export const Grouping: Story<Currency> = {
367
+ args: CurrenciesArgs,
368
+ parameters: {
369
+ docs: {
370
+ source: {
371
+ code: `<SelectInput
372
+ filterable
373
+ filterPlaceholder="Type a currency / country"
374
+ size="lg"
375
+ items={[
376
+ {
377
+ type: 'group',
378
+ label: 'Popular currencies',
379
+ options: popularCurrencies.map((c) => ({
380
+ type: 'option',
381
+ value: c,
382
+ filterMatchers: [c.code, c.name, ...(c.countries ?? [])],
383
+ })),
384
+ },
385
+ // ... more groups
386
+ ]}
387
+ defaultValue={popularCurrencies[0]}
388
+ renderValue={(currency, withinTrigger) => (
389
+ <SelectInputOptionContent
390
+ title={currency.code}
391
+ note={withinTrigger ? undefined : currency.name}
392
+ icon={<Flag code={currency.code} intrinsicSize={24} />}
393
+ />
394
+ )}
395
+ renderFooter={({ resultsEmpty }) =>
396
+ resultsEmpty ? (
397
+ <>Cannot find it? <a>Request the currency you need.</a></>
398
+ ) : null
399
+ }
400
+ />`,
401
+ },
402
+ },
263
403
  },
264
404
  };
265
405
 
266
- export const MultipleCurrencies: Story<Currency, true> = {
406
+ /**
407
+ * When `multiple` is `true`, `value` and `onChange` operate on arrays. Use the
408
+ * `withinTrigger` argument in `renderValue` to show a compact summary inside the
409
+ * trigger (e.g. currency codes joined by commas).
410
+ */
411
+ export const MultiSelect: Story<Currency, true> = {
412
+ argTypes: {
413
+ multiple: { table: { disable: true } },
414
+ },
267
415
  args: {
268
416
  ...CurrenciesArgs,
269
417
  multiple: true,
@@ -280,35 +428,46 @@ export const MultipleCurrencies: Story<Currency, true> = {
280
428
  />
281
429
  ),
282
430
  },
283
- play: async ({ canvasElement, step, args }) => {
284
- const canvas = within(canvasElement);
285
-
286
- await step('Open the combobox', async () => {
287
- const triggerButton = canvas.getByRole('combobox');
288
- await userEvent.click(triggerButton);
289
- await wait(500);
290
- await userEvent.unhover(triggerButton);
291
- await expect(args.onOpen).toHaveBeenCalledOnce();
292
- });
293
-
294
- await step('Select EUR option', async () => {
295
- const option = within(screen.getByRole('listbox')).getAllByRole('option', {
296
- name: /^EUR/u,
297
- })[0];
298
- await wait(500);
299
- await userEvent.click(option);
300
- });
301
-
302
- await step('Check if selected options are displayed', async () => {
303
- const triggerButton = document.querySelector('button[role="combobox"]');
304
- await waitFor(async () => expect(triggerButton).toHaveTextContent('USD, EUR'));
305
- });
431
+ parameters: {
432
+ docs: {
433
+ source: {
434
+ code: `<SelectInput
435
+ multiple
436
+ filterable
437
+ placeholder="Choose currencies…"
438
+ items={[
439
+ { type: 'option', value: { code: 'USD', name: 'United States Dollar' }, filterMatchers: ['USD', 'United States Dollar'] },
440
+ // ... more options
441
+ ]}
442
+ defaultValue={[currencies[0]]}
443
+ renderValue={(currency, withinTrigger) =>
444
+ withinTrigger
445
+ ? currency.code
446
+ : (
447
+ <SelectInputOptionContent
448
+ title={currency.code}
449
+ note={currency.name}
450
+ icon={<Flag code={currency.code} intrinsicSize={24} />}
451
+ />
452
+ )
453
+ }
454
+ />`,
455
+ },
456
+ },
306
457
  },
307
458
  };
308
459
 
309
- export const WithSelectAll: Story<Currency, true> = {
460
+ /**
461
+ * Groups can have an `action` button (e.g. "Select all" / "Deselect all") rendered
462
+ * next to the group heading. The label should update dynamically based on whether
463
+ * all items in that group are selected.
464
+ */
465
+ export const WithGroupActions: Story<Currency, true> = {
466
+ argTypes: {
467
+ multiple: { table: { disable: true } },
468
+ },
310
469
  args: {
311
- ...MultipleCurrencies.args,
470
+ ...MultiSelect.args,
312
471
  },
313
472
  render: function Render(args) {
314
473
  const [selectedItems, setSelectedItems] = useState<Currency[]>([]);
@@ -363,21 +522,87 @@ export const WithSelectAll: Story<Currency, true> = {
363
522
  />
364
523
  );
365
524
  },
525
+ parameters: {
526
+ docs: {
527
+ source: {
528
+ code: `<SelectInput
529
+ multiple
530
+ items={[
531
+ {
532
+ type: 'group',
533
+ label: 'Popular currencies',
534
+ options: popularCurrencies.map(currencyOption),
535
+ action: {
536
+ label: allSelected(popularCurrencies) ? 'Deselect all' : 'Select all',
537
+ onClick: () => toggleItems(popularCurrencies),
538
+ },
539
+ },
540
+ // ... more groups
541
+ ]}
542
+ value={selectedItems}
543
+ onChange={setSelectedItems}
544
+ onClear={() => setSelectedItems([])}
545
+ />`,
546
+ },
547
+ },
548
+ },
366
549
  };
367
550
 
551
+ /** Pass `onClear` to show a clear button when a value is selected. */
368
552
  export const WithClear: Story<Currency> = {
553
+ argTypes: {
554
+ onClear: { table: { disable: true } },
555
+ },
369
556
  args: {
370
557
  ...CurrenciesArgs,
371
558
  onClear: fn() satisfies Mock,
372
559
  },
373
- play: async ({ step }) => {
374
- await step('Has clear button', async () => {
375
- const clearBtn = await screen.findByRole('button', { name: 'Clear' });
376
- await expect(clearBtn).toBeInTheDocument();
377
- });
560
+ render: function Render({ onChange, onClear, defaultValue: _dv, ...args }) {
561
+ const [value, setValue] = useState<Currency | undefined>(popularCurrencies[0]);
562
+ return (
563
+ <SelectInput
564
+ {...args}
565
+ value={value}
566
+ onChange={(currency) => {
567
+ setValue(currency);
568
+ onChange?.(currency);
569
+ }}
570
+ onClear={() => {
571
+ setValue(undefined);
572
+ onClear?.();
573
+ }}
574
+ />
575
+ );
576
+ },
577
+ parameters: {
578
+ docs: {
579
+ source: {
580
+ code: `<SelectInput
581
+ filterable
582
+ size="lg"
583
+ items={[
584
+ { type: 'option', value: { code: 'USD', name: 'United States Dollar' }, filterMatchers: ['USD', 'United States Dollar'] },
585
+ // ... more options
586
+ ]}
587
+ defaultValue={currencies[0]}
588
+ renderValue={(currency) => (
589
+ <SelectInputOptionContent
590
+ title={currency.code}
591
+ icon={<Flag code={currency.code} intrinsicSize={24} />}
592
+ />
593
+ )}
594
+ onClear={() => setValue(undefined)}
595
+ />`,
596
+ },
597
+ },
378
598
  },
379
599
  };
380
600
 
601
+ /**
602
+ * Use `renderTrigger` with `SelectInputTriggerButton` to fully customise the trigger
603
+ * appearance. The interactive element **must** be `SelectInputTriggerButton` to receive
604
+ * the correct ARIA attributes (`aria-expanded`, `aria-haspopup`, `aria-controls`).
605
+ */
381
606
  export const CustomTrigger: Story<Month> = {
382
607
  args: {
383
608
  placeholder: 'Month',
@@ -397,28 +622,39 @@ export const CustomTrigger: Story<Month> = {
397
622
  </SelectInputTriggerButton>
398
623
  ),
399
624
  },
400
- play: async ({ canvasElement, step, args }) => {
401
- const canvas = within(canvasElement);
402
-
403
- await step('Open the combobox', async () => {
404
- const triggerButton = canvas.getByRole('combobox');
405
- await userEvent.click(triggerButton);
406
- await expect(args.onOpen).toHaveBeenCalledOnce();
407
- });
625
+ parameters: {
626
+ docs: {
627
+ source: {
628
+ code: `<SelectInput
629
+ placeholder="Month"
630
+ items={monthItems}
631
+ renderValue={(month, withinTrigger) =>
632
+ withinTrigger ? month.name : <SelectInputOptionContent title={month.name} />
633
+ }
634
+ renderTrigger={({ content, className }) => (
635
+ <SelectInputTriggerButton
636
+ className={clsx(className, 'btn-unstyled np-text-link-large align-items-center')}
637
+ style={{ display: 'inline-flex', columnGap: '0.25rem' }}
638
+ >
639
+ {content}
640
+ <ChevronDown size={16} />
641
+ </SelectInputTriggerButton>
642
+ )}
643
+ />`,
644
+ },
645
+ },
408
646
  },
409
647
  };
410
648
 
411
- const quarters = [
412
- months.slice(0, 3),
413
- months.slice(3, 6),
414
- months.slice(6, 9),
415
- months.slice(9, 12),
416
- ] as const;
417
-
649
+ /**
650
+ * Combines grouped items, `separator` items, disabled options, and filterable search.
651
+ * `SelectInputOptionContent` supports `title`, `note` (inline), and
652
+ * `description` (below-title) for rich two-line option layouts.
653
+ */
418
654
  export const Advanced: Story<Month> = {
419
655
  args: {
420
656
  placeholder: 'Month',
421
- items: quarters
657
+ items: [months.slice(0, 3), months.slice(3, 6), months.slice(6, 9), months.slice(9, 12)]
422
658
  .flatMap<SelectInputItem<Month>>((quarterMonths, quarterIndex) => [
423
659
  {
424
660
  type: 'group',
@@ -442,19 +678,46 @@ export const Advanced: Story<Month> = {
442
678
  />
443
679
  ),
444
680
  filterable: true,
445
- filterPlaceholder: 'Type a months name',
681
+ filterPlaceholder: "Type a month's name",
446
682
  },
447
- play: async ({ canvasElement, step }) => {
448
- const canvas = within(canvasElement);
449
-
450
- await step('Open the combobox', async () => {
451
- const triggerButton = canvas.getByRole('combobox');
452
- await userEvent.click(triggerButton);
453
- });
683
+ parameters: {
684
+ docs: {
685
+ source: {
686
+ code: `<SelectInput
687
+ filterable
688
+ filterPlaceholder="Type a month name"
689
+ items={[
690
+ {
691
+ type: 'group',
692
+ label: 'Quarter #1',
693
+ options: [
694
+ { type: 'option', value: jan, filterMatchers: ['January'] },
695
+ { type: 'option', value: feb, filterMatchers: ['February'] },
696
+ { type: 'option', value: mar, filterMatchers: ['March'], disabled: true },
697
+ ],
698
+ },
699
+ { type: 'separator' },
700
+ // ... more groups
701
+ ]}
702
+ renderValue={(month) => (
703
+ <SelectInputOptionContent
704
+ title={month.name}
705
+ note="Note"
706
+ description={\`Month #\${month.id}\`}
707
+ icon={<Calendar size={24} />}
708
+ />
709
+ )}
710
+ />`,
711
+ },
712
+ },
454
713
  },
455
714
  };
456
715
 
457
- export const ManyItems: Story<string, true> = {
716
+ /**
717
+ * Lists with 50+ items are automatically virtualised using a windowed renderer.
718
+ * This example renders 1,000 numbered options with multi-select enabled.
719
+ */
720
+ export const Virtualization: Story<string, true> = {
458
721
  args: {
459
722
  multiple: true,
460
723
  items: Array.from({ length: 1000 }, (_, index) => ({
@@ -472,18 +735,257 @@ export const ManyItems: Story<string, true> = {
472
735
  ),
473
736
  filterable: true,
474
737
  },
738
+ parameters: {
739
+ docs: {
740
+ source: {
741
+ code: `<SelectInput
742
+ multiple
743
+ filterable
744
+ items={Array.from({ length: 1000 }, (_, i) => ({
745
+ type: 'option',
746
+ value: String(i + 1),
747
+ }))}
748
+ renderValue={(value, withinTrigger) =>
749
+ withinTrigger ? value : <SelectInputOptionContent title={value} />
750
+ }
751
+ />`,
752
+ },
753
+ },
754
+ },
475
755
  };
476
756
 
477
- export const WithinField = {
478
- args: Months.args,
479
- decorators: [
480
- (Story) => (
481
- <Field message="Something went wrong" sentiment="negative" label="Month">
482
- <Story />
757
+ /** The `disabled` prop prevents all interaction. */
758
+ export const Disabled: Story<Currency> = {
759
+ argTypes: {
760
+ disabled: { table: { disable: true } },
761
+ id: { table: { disable: true } },
762
+ name: { table: { disable: true } },
763
+ multiple: { table: { disable: true } },
764
+ placeholder: { table: { disable: true } },
765
+ items: { table: { disable: true } },
766
+ autocomplete: { table: { disable: true } },
767
+ defaultValue: { table: { disable: true } },
768
+ value: { table: { disable: true } },
769
+ compareValues: { table: { disable: true } },
770
+ renderValue: { table: { disable: true } },
771
+ renderFooter: { table: { disable: true } },
772
+ renderTrigger: { table: { disable: true } },
773
+ filterable: { table: { disable: true } },
774
+ filterPlaceholder: { table: { disable: true } },
775
+ sortFilteredOptions: { table: { disable: true } },
776
+ className: { table: { disable: true } },
777
+ UNSAFE_triggerButtonProps: { table: { disable: true } },
778
+ triggerRef: { table: { disable: true } },
779
+ parentId: { table: { disable: true } },
780
+ onFilterChange: { table: { disable: true } },
781
+ onChange: { table: { disable: true } },
782
+ onClose: { table: { disable: true } },
783
+ onOpen: { table: { disable: true } },
784
+ onClear: { table: { disable: true } },
785
+ },
786
+ args: {
787
+ ...CurrenciesArgs,
788
+ disabled: true,
789
+ },
790
+ parameters: {
791
+ docs: {
792
+ source: {
793
+ code: `<SelectInput
794
+ disabled
795
+ filterable
796
+ size="lg"
797
+ items={currencyItems}
798
+ defaultValue={currencies[0]}
799
+ renderValue={(currency) => (
800
+ <SelectInputOptionContent
801
+ title={currency.code}
802
+ icon={<Flag code={currency.code} intrinsicSize={24} />}
803
+ />
804
+ )}
805
+ />`,
806
+ },
807
+ },
808
+ },
809
+ };
810
+
811
+ /** Three size variants: `sm`, `md`, and `lg`. */
812
+ export const Sizes: Story<Currency> = {
813
+ argTypes: {
814
+ size: { table: { disable: true } },
815
+ },
816
+ render: (args) => (
817
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', maxWidth: '400px' }}>
818
+ <SelectInput<Currency>
819
+ {...args}
820
+ size="sm"
821
+ placeholder="Small"
822
+ items={popularCurrencies.map(currencyOption)}
823
+ renderValue={(c) => <SelectInputOptionContent title={c.code} note={c.name} />}
824
+ />
825
+ <SelectInput<Currency>
826
+ {...args}
827
+ size="md"
828
+ placeholder="Medium"
829
+ items={popularCurrencies.map(currencyOption)}
830
+ renderValue={(c) => <SelectInputOptionContent title={c.code} note={c.name} />}
831
+ />
832
+ <SelectInput<Currency>
833
+ {...args}
834
+ size="lg"
835
+ placeholder="Large (default)"
836
+ items={popularCurrencies.map(currencyOption)}
837
+ renderValue={(c) => <SelectInputOptionContent title={c.code} note={c.name} />}
838
+ />
839
+ </div>
840
+ ),
841
+ parameters: {
842
+ docs: {
843
+ source: {
844
+ code: `<SelectInput size="sm" placeholder="Small" items={currencyItems} renderValue={renderCurrency} />
845
+ <SelectInput size="md" placeholder="Medium" items={currencyItems} renderValue={renderCurrency} />
846
+ <SelectInput size="lg" placeholder="Large" items={currencyItems} renderValue={renderCurrency} />`,
847
+ },
848
+ },
849
+ },
850
+ };
851
+
852
+ /**
853
+ * "Commit on close" pattern: stage selections locally and apply them only when the
854
+ * dropdown closes. Useful when each selection change is expensive (e.g. an API call)
855
+ * or when the UX requires confirmation before updating external state.
856
+ */
857
+ export const WithCommitOnClose: Story<Currency, true> = {
858
+ argTypes: {
859
+ onClose: { table: { disable: true } },
860
+ },
861
+ render: function Render(args) {
862
+ const [committedValues, setCommittedValues] = useState<Currency[]>([popularCurrencies[0]]);
863
+ const [stagedValues, setStagedValues] = useState<Currency[]>([popularCurrencies[0]]);
864
+
865
+ return (
866
+ <div>
867
+ <p className="m-t-2 np-text-body-small">
868
+ Applied: {committedValues.map((c) => c.code).join(', ') || 'none'}
869
+ </p>
870
+ <SelectInput<Currency, true>
871
+ {...args}
872
+ multiple
873
+ filterable
874
+ filterPlaceholder="Type a currency / country"
875
+ size="lg"
876
+ placeholder="Choose currencies…"
877
+ items={[
878
+ {
879
+ type: 'group',
880
+ label: 'Popular currencies',
881
+ options: popularCurrencies.map(currencyOption),
882
+ },
883
+ {
884
+ type: 'group',
885
+ label: 'All currencies',
886
+ options: allCurrencies.map(currencyOption),
887
+ },
888
+ ]}
889
+ renderValue={(currency, withinTrigger) =>
890
+ withinTrigger ? (
891
+ currency.code
892
+ ) : (
893
+ <SelectInputOptionContent
894
+ title={currency.code}
895
+ note={currency.name}
896
+ icon={<Flag code={currency.code} intrinsicSize={24} />}
897
+ />
898
+ )
899
+ }
900
+ value={stagedValues}
901
+ onChange={setStagedValues}
902
+ onClose={() => setCommittedValues(stagedValues)}
903
+ onClear={() => {
904
+ setStagedValues([]);
905
+ setCommittedValues([]);
906
+ }}
907
+ />
908
+ </div>
909
+ );
910
+ },
911
+ parameters: {
912
+ docs: {
913
+ source: {
914
+ code: `const [committedValues, setCommittedValues] = useState([]);
915
+ const [stagedValues, setStagedValues] = useState([]);
916
+
917
+ <SelectInput
918
+ multiple
919
+ value={stagedValues}
920
+ onChange={setStagedValues}
921
+ onClose={() => setCommittedValues(stagedValues)}
922
+ onClear={() => { setStagedValues([]); setCommittedValues([]); }}
923
+ // ...items, renderValue
924
+ />`,
925
+ },
926
+ },
927
+ },
928
+ };
929
+
930
+ /**
931
+ * Wrap with `Field` to inherit error state and label association automatically.
932
+ * `Field` injects `aria-describedby` on the trigger pointing to the validation
933
+ * message — no manual wiring required.
934
+ */
935
+ export const WithinField: Story<Month> = {
936
+ args: {
937
+ placeholder: "Today's Month",
938
+ items: months.map((month) => ({
939
+ type: 'option',
940
+ value: month,
941
+ })),
942
+ renderValue: (month) => <SelectInputOptionContent title={month.name} />,
943
+ },
944
+ render: function Render({ onChange, ...args }) {
945
+ const currentMonthIndex = new Date().getMonth();
946
+ const defaultMonth = months[currentMonthIndex === 0 ? 2 : 0];
947
+ const [selectedMonth, setSelectedMonth] = useState<Month>(defaultMonth);
948
+ const isCorrect = selectedMonth.id === currentMonthIndex + 1;
949
+
950
+ return (
951
+ <Field
952
+ label="Today's Month"
953
+ message={isCorrect ? undefined : 'Please select the current month'}
954
+ sentiment={isCorrect ? undefined : 'negative'}
955
+ >
956
+ <SelectInput
957
+ {...args}
958
+ value={selectedMonth}
959
+ onChange={(month) => {
960
+ setSelectedMonth(month);
961
+ onChange?.(month);
962
+ }}
963
+ />
483
964
  </Field>
484
- ),
485
- ],
486
- } satisfies Story<Month | null>;
965
+ );
966
+ },
967
+ parameters: {
968
+ docs: {
969
+ source: {
970
+ code: `const isCorrect = selectedMonth.id === currentMonthIndex + 1;
971
+
972
+ <Field
973
+ label="Today's Month"
974
+ message={isCorrect ? undefined : 'Please select the current month'}
975
+ sentiment={isCorrect ? undefined : 'negative'}
976
+ >
977
+ <SelectInput
978
+ placeholder="Today's Month"
979
+ items={monthItems}
980
+ renderValue={(month) => <SelectInputOptionContent title={month.name} />}
981
+ value={selectedMonth}
982
+ onChange={setSelectedMonth}
983
+ />
984
+ </Field>`,
985
+ },
986
+ },
987
+ },
988
+ };
487
989
 
488
990
  export const WithinDrawer: Story<Currency> = {
489
991
  args: CurrenciesArgs,
@@ -506,6 +1008,28 @@ export const WithinDrawer: Story<Currency> = {
506
1008
  );
507
1009
  },
508
1010
  ],
1011
+ parameters: {
1012
+ docs: {
1013
+ source: {
1014
+ code: `<Drawer open={open} onClose={() => setOpen(false)}>
1015
+ <SelectInput
1016
+ filterable
1017
+ filterPlaceholder="Type a currency / country"
1018
+ size="lg"
1019
+ items={currencyItems}
1020
+ renderValue={(currency, withinTrigger) => (
1021
+ <SelectInputOptionContent
1022
+ title={currency.code}
1023
+ note={withinTrigger ? undefined : currency.name}
1024
+ icon={<Flag code={currency.code} intrinsicSize={24} />}
1025
+ />
1026
+ )}
1027
+ onChange={setSelectedCurrency}
1028
+ />
1029
+ </Drawer>`,
1030
+ },
1031
+ },
1032
+ },
509
1033
  };
510
1034
 
511
1035
  export const WithinModal: Story<Currency> = {
@@ -518,11 +1042,42 @@ export const WithinModal: Story<Currency> = {
518
1042
  <Button v2 onClick={() => setOpen(true)}>
519
1043
  Open modal
520
1044
  </Button>
521
- <Modal open={open} body={<Story />} onClose={() => setOpen(false)} />
1045
+ <Modal
1046
+ title="Select a currency"
1047
+ open={open}
1048
+ body={<Story />}
1049
+ onClose={() => setOpen(false)}
1050
+ />
522
1051
  </>
523
1052
  );
524
1053
  },
525
1054
  ],
1055
+ parameters: {
1056
+ docs: {
1057
+ source: {
1058
+ code: `<Modal
1059
+ open={open}
1060
+ onClose={() => setOpen(false)}
1061
+ body={
1062
+ <SelectInput
1063
+ filterable
1064
+ filterPlaceholder="Type a currency / country"
1065
+ size="lg"
1066
+ items={currencyItems}
1067
+ renderValue={(currency, withinTrigger) => (
1068
+ <SelectInputOptionContent
1069
+ title={currency.code}
1070
+ note={withinTrigger ? undefined : currency.name}
1071
+ icon={<Flag code={currency.code} intrinsicSize={24} />}
1072
+ />
1073
+ )}
1074
+ onChange={setSelectedCurrency}
1075
+ />
1076
+ }
1077
+ />`,
1078
+ },
1079
+ },
1080
+ },
526
1081
  };
527
1082
 
528
1083
  interface Country {
@@ -546,7 +1101,7 @@ const countries: Country[] = [
546
1101
  { code: 'NL', name: 'Netherlands' },
547
1102
  { code: 'CH', name: 'Switzerland' },
548
1103
  { code: 'SE', name: 'Sweden' },
549
- { code: 'AL', name: 'Ålandia' },
1104
+ { code: 'MX', name: 'Mexico' },
550
1105
  ];
551
1106
 
552
1107
  function countryOption(country: Country) {
@@ -557,6 +1112,11 @@ function countryOption(country: Country) {
557
1112
  } satisfies SelectInputItem;
558
1113
  }
559
1114
 
1115
+ /**
1116
+ * Use `name` and `autocomplete` to integrate with the browser's native autofill.
1117
+ * The component renders a hidden `<input>` that carries the selected value,
1118
+ * making it submittable inside a `<form>` and detectable by password managers.
1119
+ */
560
1120
  export const WithAutocomplete: Story<string> = {
561
1121
  args: {
562
1122
  name: 'country',
@@ -581,43 +1141,59 @@ export const WithAutocomplete: Story<string> = {
581
1141
  const [selectedCountry, setSelectedCountry] = useState<string | undefined>(undefined);
582
1142
 
583
1143
  return (
584
- <div>
585
- <form
586
- method="post"
587
- onSubmit={(e) => {
588
- e.preventDefault();
589
- console.log(
590
- `Form submitted with country: ${selectedCountry}. This saves data for browser autocomplete!`,
591
- );
592
- }}
593
- >
594
- <div>
595
- <label htmlFor="country-select" className="block text-sm font-medium mb-2">
596
- Country Selection with Autocomplete:
597
- </label>
598
- <SelectInput
599
- {...args}
600
- id="country-select"
601
- value={selectedCountry}
602
- onChange={(country) => {
603
- setSelectedCountry(country);
604
- onChange?.(country);
605
- console.log('Country selected via SelectInput:', country);
606
- }}
607
- onClear={() => {
608
- setSelectedCountry(undefined);
609
- onClear?.();
610
- }}
611
- />
612
- </div>
613
-
614
- <Button type="submit" v2 className="m-t-2" data-testid="submit-btn">
615
- Submit Form
616
- </Button>
617
- </form>
618
- </div>
1144
+ <form method="post" onSubmit={(e) => e.preventDefault()}>
1145
+ <Field label="Country">
1146
+ <SelectInput
1147
+ {...args}
1148
+ id="country-select"
1149
+ value={selectedCountry}
1150
+ onChange={(country) => {
1151
+ setSelectedCountry(country);
1152
+ onChange?.(country);
1153
+ }}
1154
+ onClear={() => {
1155
+ setSelectedCountry(undefined);
1156
+ onClear?.();
1157
+ }}
1158
+ />
1159
+ </Field>
1160
+ <Button type="submit" v2 className="m-t-2" data-testid="submit-btn">
1161
+ Submit Form
1162
+ </Button>
1163
+ </form>
619
1164
  );
620
1165
  },
1166
+ parameters: {
1167
+ docs: {
1168
+ source: {
1169
+ code: `<form method="post">
1170
+ <Field label="Country">
1171
+ <SelectInput
1172
+ name="country"
1173
+ autocomplete="country-name"
1174
+ filterable
1175
+ filterPlaceholder="Type a country name"
1176
+ size="lg"
1177
+ items={[
1178
+ { type: 'option', value: 'US', filterMatchers: ['US', 'United States'] },
1179
+ { type: 'option', value: 'GB', filterMatchers: ['GB', 'United Kingdom'] },
1180
+ // ... more countries
1181
+ ]}
1182
+ renderValue={(code, withinTrigger) => (
1183
+ <SelectInputOptionContent
1184
+ title={withinTrigger ? code : countryName(code)}
1185
+ note={withinTrigger ? undefined : code}
1186
+ icon={<Flag code={code} intrinsicSize={24} />}
1187
+ />
1188
+ )}
1189
+ value={selectedCountry}
1190
+ onChange={setSelectedCountry}
1191
+ />
1192
+ </Field>
1193
+ </form>`,
1194
+ },
1195
+ },
1196
+ },
621
1197
  };
622
1198
 
623
1199
  interface CountryWithCurrency extends Country {
@@ -640,7 +1216,7 @@ function countryWithCurrencyOption(country: CountryWithCurrency) {
640
1216
  } satisfies SelectInputItem<CountryWithCurrency>;
641
1217
  }
642
1218
 
643
- export const WithCustomSearchResultSorting: Story<CountryWithCurrency> = {
1219
+ export const WithBuiltInSearchResultSorting: Story<CountryWithCurrency> = {
644
1220
  args: {
645
1221
  items: countriesWithCurrency.map(countryWithCurrencyOption),
646
1222
  compareValues: 'code',
@@ -661,8 +1237,8 @@ export const WithCustomSearchResultSorting: Story<CountryWithCurrency> = {
661
1237
  <div>
662
1238
  <p className="m-b-3 np-text-body-default" style={{ maxWidth: '600px' }}>
663
1239
  This example uses the built-in <code>SelectInput.sortByRelevance</code> helper to sort
664
- filtered results by relevance. It prioritises: exact matches starts with contains →
665
- alphabetical.
1240
+ filtered results by relevance (You can implement your own). This one prioritises: exact
1241
+ matches → starts with → contains → alphabetical.
666
1242
  <br />
667
1243
  <br />
668
1244
  Try searching for &quot;united&quot; to see the sorting tiers in action: