@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.
- package/build/actionOption/ActionOption.js.map +1 -1
- package/build/actionOption/ActionOption.mjs.map +1 -1
- package/build/alert/Alert.js +1 -1
- package/build/alert/Alert.js.map +1 -1
- package/build/alert/Alert.mjs +1 -1
- package/build/alert/Alert.mjs.map +1 -1
- package/build/checkboxOption/CheckboxOption.js.map +1 -1
- package/build/checkboxOption/CheckboxOption.mjs.map +1 -1
- package/build/common/Option/Option.js.map +1 -1
- package/build/common/Option/Option.mjs.map +1 -1
- package/build/common/liveRegion/LiveRegion.js +46 -7
- package/build/common/liveRegion/LiveRegion.js.map +1 -1
- package/build/common/liveRegion/LiveRegion.mjs +46 -7
- package/build/common/liveRegion/LiveRegion.mjs.map +1 -1
- package/build/flowNavigation/FlowNavigation.js +1 -0
- package/build/flowNavigation/FlowNavigation.js.map +1 -1
- package/build/flowNavigation/FlowNavigation.mjs +1 -0
- package/build/flowNavigation/FlowNavigation.mjs.map +1 -1
- package/build/legacylistItem/LegacyListItem.js.map +1 -1
- package/build/legacylistItem/LegacyListItem.mjs.map +1 -1
- package/build/main.css +52 -1
- package/build/navigationOption/NavigationOption.js.map +1 -1
- package/build/navigationOption/NavigationOption.mjs.map +1 -1
- package/build/overlayHeader/OverlayHeader.js +1 -0
- package/build/overlayHeader/OverlayHeader.js.map +1 -1
- package/build/overlayHeader/OverlayHeader.mjs +1 -0
- package/build/overlayHeader/OverlayHeader.mjs.map +1 -1
- package/build/prompt/InfoPrompt/InfoPrompt.js +2 -0
- package/build/prompt/InfoPrompt/InfoPrompt.js.map +1 -1
- package/build/prompt/InfoPrompt/InfoPrompt.mjs +2 -0
- package/build/prompt/InfoPrompt/InfoPrompt.mjs.map +1 -1
- package/build/radioOption/RadioOption.js.map +1 -1
- package/build/radioOption/RadioOption.mjs.map +1 -1
- package/build/styles/common/liveRegion/LiveRegion.css +3 -0
- package/build/styles/css/neptune.css +48 -1
- package/build/styles/main.css +52 -1
- package/build/styles/styles/less/neptune.css +48 -1
- package/build/summary/Summary.js +1 -1
- package/build/summary/Summary.js.map +1 -1
- package/build/summary/Summary.mjs +1 -1
- package/build/summary/Summary.mjs.map +1 -1
- package/build/switchOption/SwitchOption.js +1 -1
- package/build/switchOption/SwitchOption.js.map +1 -1
- package/build/switchOption/SwitchOption.mjs +1 -1
- package/build/switchOption/SwitchOption.mjs.map +1 -1
- package/build/types/actionOption/ActionOption.d.ts +1 -1
- package/build/types/alert/Alert.d.ts +1 -1
- package/build/types/checkboxOption/CheckboxOption.d.ts +1 -1
- package/build/types/common/Option/Option.d.ts +3 -0
- package/build/types/common/Option/Option.d.ts.map +1 -1
- package/build/types/common/liveRegion/LiveRegion.d.ts +5 -2
- package/build/types/common/liveRegion/LiveRegion.d.ts.map +1 -1
- package/build/types/legacylistItem/LegacyListItem.d.ts +1 -1
- package/build/types/navigationOption/NavigationOption.d.ts +1 -1
- package/build/types/prompt/InfoPrompt/InfoPrompt.d.ts +2 -2
- package/build/types/prompt/InfoPrompt/InfoPrompt.d.ts.map +1 -1
- package/build/types/radioOption/RadioOption.d.ts +1 -1
- package/build/types/summary/Summary.d.ts +1 -1
- package/build/types/switchOption/SwitchOption.d.ts +1 -1
- package/package.json +2 -2
- package/src/actionOption/ActionOption.story.tsx +2 -1
- package/src/actionOption/ActionOption.tsx +1 -1
- package/src/alert/Alert.story.tsx +1 -7
- package/src/alert/Alert.tsx +1 -1
- package/src/button/_stories/Button.story.tsx +0 -5
- package/src/checkboxButton/CheckboxButton.story.tsx +0 -1
- package/src/checkboxOption/CheckboxOption.story.tsx +2 -1
- package/src/checkboxOption/CheckboxOption.tsx +1 -1
- package/src/circularButton/CircularButton.story.tsx +0 -1
- package/src/common/Option/Option.tsx +3 -0
- package/src/common/liveRegion/LiveRegion.css +3 -0
- package/src/common/liveRegion/LiveRegion.less +3 -0
- package/src/common/liveRegion/LiveRegion.test.tsx +69 -2
- package/src/common/liveRegion/LiveRegion.tsx +77 -8
- package/src/display/Display.story.tsx +15 -1
- package/src/expressiveMoneyInput/ExpressiveMoneyInput.story.tsx +0 -1
- package/src/header/Header.story.tsx +0 -5
- package/src/inputWithDisplayFormat/InputWithDisplayFormat.story.tsx +0 -1
- package/src/inputs/SelectInput/_stories/SelectInput.docs.mdx +62 -0
- package/src/inputs/SelectInput/_stories/SelectInput.story.tsx +796 -220
- package/src/inputs/SelectInput/_stories/SelectInput.test.story.tsx +433 -4
- package/src/legacylistItem/LegacyListItem.story.tsx +2 -1
- package/src/legacylistItem/LegacyListItem.tsx +1 -1
- package/src/listItem/AdditionalInfo/ListItemAdditionalInfo.story.tsx +0 -5
- package/src/listItem/AvatarLayout/ListItemAvatarLayout.story.tsx +0 -5
- package/src/listItem/AvatarView/ListItemAvatarView.story.tsx +0 -5
- package/src/listItem/Button/ListItemButton.story.tsx +0 -5
- package/src/listItem/Checkbox/ListItemCheckbox.story.tsx +0 -5
- package/src/listItem/IconButton/ListItemIconButton.story.tsx +0 -5
- package/src/listItem/Image/ListItemImage.story.tsx +0 -5
- package/src/listItem/Navigation/ListItemNavigation.story.tsx +0 -5
- package/src/listItem/Prompt/ListItemPrompt.story.tsx +1 -5
- package/src/listItem/Radio/ListItemRadio.story.tsx +0 -5
- package/src/listItem/Switch/ListItemSwitch.story.tsx +0 -5
- package/src/listItem/_stories/ListItem.disabled.story.tsx +0 -1
- package/src/listItem/_stories/ListItem.scenarios.story.tsx +0 -1
- package/src/listItem/_stories/ListItem.story.tsx +1 -6
- package/src/main.css +52 -1
- package/src/main.less +1 -0
- package/src/modal/Modal.story.tsx +0 -1
- package/src/navigationOption/NavigationOption.story.tsx +2 -1
- package/src/navigationOption/NavigationOption.tsx +1 -1
- package/src/popover/Popover.story.tsx +0 -1
- package/src/prompt/ActionPrompt/ActionPrompt.story.tsx +0 -5
- package/src/prompt/InfoPrompt/InfoPrompt.story.tsx +0 -5
- package/src/prompt/InfoPrompt/InfoPrompt.test.story.tsx +142 -5
- package/src/prompt/InfoPrompt/InfoPrompt.test.tsx +11 -6
- package/src/prompt/InfoPrompt/InfoPrompt.tsx +4 -3
- package/src/prompt/InlinePrompt/InlinePrompt.story.tsx +0 -5
- package/src/provider/theme/ThemeProvider.story.tsx +8 -0
- package/src/radioOption/RadioOption.story.tsx +2 -1
- package/src/radioOption/RadioOption.tsx +1 -1
- package/src/sentimentSurface/SentimentSurface.story.tsx +0 -5
- package/src/sticky/Sticky.story.tsx +0 -1
- package/src/styles/less/core/_typography.less +15 -2
- package/src/styles/less/neptune.css +48 -1
- package/src/summary/Summary.story.tsx +1 -1
- package/src/summary/Summary.tsx +1 -1
- package/src/switchOption/SwitchOption.story.tsx +2 -1
- package/src/switchOption/SwitchOption.tsx +1 -1
- package/src/tokens/tokens.story.tsx +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
2
|
-
import {
|
|
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
|
-
|
|
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: '
|
|
144
|
-
name: '
|
|
145
|
-
countries: ['
|
|
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
|
|
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
|
|
160
|
+
Email me when it is available.
|
|
188
161
|
</a>
|
|
189
162
|
</>
|
|
190
163
|
) : (
|
|
191
164
|
<>
|
|
192
|
-
|
|
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
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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:
|
|
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:
|
|
681
|
+
filterPlaceholder: "Type a month's name",
|
|
446
682
|
},
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
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
|
|
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: '
|
|
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
|
-
<
|
|
585
|
-
<
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
|
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
|
|
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 "united" to see the sorting tiers in action:
|