@agregio-solutions/design-system 1.91.0 → 1.92.0
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/dist/packages/components/Accordion/doc.md +342 -0
- package/dist/packages/components/Badge/doc.md +192 -0
- package/dist/packages/components/Breadcrumbs/doc.md +332 -0
- package/dist/packages/components/Button/doc.md +425 -0
- package/dist/packages/components/Calendar/doc.md +465 -0
- package/dist/packages/components/ChartLegend/doc.md +151 -0
- package/dist/packages/components/ChartTooltip/doc.md +124 -0
- package/dist/packages/components/Checkbox/doc.md +329 -0
- package/dist/packages/components/CheckboxGroup/doc.md +242 -0
- package/dist/packages/components/Chip/doc.md +99 -0
- package/dist/packages/components/Combobox/doc.md +680 -0
- package/dist/packages/components/DataTable/doc.md +1124 -0
- package/dist/packages/components/DatePicker/doc.md +579 -0
- package/dist/packages/components/DateRangePicker/doc.md +638 -0
- package/dist/packages/components/Drawer/doc.md +338 -0
- package/dist/packages/components/Dropdown/doc.md +205 -0
- package/dist/packages/components/EmptyState/doc.md +101 -0
- package/dist/packages/components/FileUpload/doc.md +449 -0
- package/dist/packages/components/Filter/doc.md +196 -0
- package/dist/packages/components/Header/doc.md +373 -0
- package/dist/packages/components/I18nProvider/doc.md +187 -0
- package/dist/packages/components/Icon/doc.md +63 -0
- package/dist/packages/components/Label/doc.md +60 -0
- package/dist/packages/components/LinearProgressBar/doc.md +148 -0
- package/dist/packages/components/Link/doc.md +206 -0
- package/dist/packages/components/List/doc.md +481 -0
- package/dist/packages/components/Loader/doc.md +53 -0
- package/dist/packages/components/Menu/doc.md +231 -0
- package/dist/packages/components/Message/doc.md +166 -0
- package/dist/packages/components/Modal/doc.md +289 -0
- package/dist/packages/components/Navigation/doc.md +992 -0
- package/dist/packages/components/NavigationItem/doc.md +167 -0
- package/dist/packages/components/NotificationCard/doc.md +206 -0
- package/dist/packages/components/Notifications/doc.md +240 -0
- package/dist/packages/components/NumberField/doc.md +582 -0
- package/dist/packages/components/PageLayout/doc.md +651 -0
- package/dist/packages/components/Pagination/doc.md +227 -0
- package/dist/packages/components/Popover/doc.md +245 -0
- package/dist/packages/components/Radio/doc.md +370 -0
- package/dist/packages/components/RouterProvider/doc.md +64 -0
- package/dist/packages/components/SearchBar/doc.md +504 -0
- package/dist/packages/components/SegmentedControl/doc.md +398 -0
- package/dist/packages/components/Select/doc.md +1133 -0
- package/dist/packages/components/Skeleton/doc.md +129 -0
- package/dist/packages/components/Slider/doc.md +362 -0
- package/dist/packages/components/Stepper/doc.md +104 -0
- package/dist/packages/components/Switch/doc.md +296 -0
- package/dist/packages/components/Tabs/doc.md +295 -0
- package/dist/packages/components/Tag/doc.md +81 -0
- package/dist/packages/components/TextInput/doc.md +490 -0
- package/dist/packages/components/TimeField/doc.md +353 -0
- package/dist/packages/components/Timeline/doc.md +1046 -0
- package/dist/packages/components/Toaster/doc.md +263 -0
- package/dist/packages/components/ToggleButton/doc.md +108 -0
- package/dist/packages/components/ToggleButtonGroup/doc.md +307 -0
- package/dist/packages/components/Tooltip/doc.md +206 -0
- package/dist/packages/components/YearMonthPicker/doc.md +638 -0
- package/dist/public_docs/components.md +68 -0
- package/dist/public_docs/index.md +30 -0
- package/dist/public_docs/tokens.md +121 -0
- package/package.json +3 -2
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
# Combobox
|
|
2
|
+
|
|
3
|
+
## Props
|
|
4
|
+
|
|
5
|
+
The complete Props documentation with JS doc for this component is available at this path:
|
|
6
|
+
|
|
7
|
+
node_modules/@agregio-solutions/design-system/dist/packages/components/Combobox/Combobox.d.ts
|
|
8
|
+
|
|
9
|
+
## Example usage
|
|
10
|
+
|
|
11
|
+
Here are the Storybook Stories.
|
|
12
|
+
|
|
13
|
+
Base stories:
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { Meta, StoryObj } from "@storybook/react-vite";
|
|
17
|
+
import { expect, fn, screen, userEvent, within } from "storybook/test";
|
|
18
|
+
|
|
19
|
+
import Combobox from "./Combobox";
|
|
20
|
+
|
|
21
|
+
const meta: Meta<typeof Combobox> = {
|
|
22
|
+
component: Combobox,
|
|
23
|
+
argTypes: {
|
|
24
|
+
helperText: { control: { type: "text" } },
|
|
25
|
+
label: { control: { type: "text" } },
|
|
26
|
+
description: { control: { type: "text" } },
|
|
27
|
+
errorHelperText: { control: { type: "text" } },
|
|
28
|
+
successHelperText: { control: { type: "text" } },
|
|
29
|
+
items: { control: false },
|
|
30
|
+
defaultInputValue: { control: false },
|
|
31
|
+
},
|
|
32
|
+
parameters: {
|
|
33
|
+
layout: "centered",
|
|
34
|
+
},
|
|
35
|
+
globals: {
|
|
36
|
+
backgrounds: { value: "light" },
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
export default meta;
|
|
40
|
+
|
|
41
|
+
export const Playground: StoryObj<typeof Combobox> = {
|
|
42
|
+
args: {
|
|
43
|
+
id: "my-select",
|
|
44
|
+
onChange: fn(),
|
|
45
|
+
label: "[Insert label]",
|
|
46
|
+
defaultItems: [
|
|
47
|
+
{ text: "Option 1", id: "1" },
|
|
48
|
+
{ text: "Option 2", id: "2" },
|
|
49
|
+
{ text: "Option 3", id: "3" },
|
|
50
|
+
{ text: "Option 4", id: "4" },
|
|
51
|
+
{ text: "Option 5", id: "5" },
|
|
52
|
+
{ text: "Option 6", id: "6" },
|
|
53
|
+
{ text: "Option 7", id: "7" },
|
|
54
|
+
{ text: "Option 8", id: "8" },
|
|
55
|
+
{ text: "Option 9", id: "9" },
|
|
56
|
+
{ text: "Option disabled", id: "disabled" },
|
|
57
|
+
],
|
|
58
|
+
disabledIds: ["disabled"],
|
|
59
|
+
placeholder: "Please select an option",
|
|
60
|
+
labelIconRight: "help_outline",
|
|
61
|
+
labelIconRightTooltip: "Additional information",
|
|
62
|
+
required: true,
|
|
63
|
+
description: "[Insert description]",
|
|
64
|
+
helperText: "[Insert helper]",
|
|
65
|
+
helperTextIcon: "help_outline",
|
|
66
|
+
fullWidth: true,
|
|
67
|
+
wrapperProps: {
|
|
68
|
+
style: { width: 220 },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
play: async ({ canvasElement }) => {
|
|
72
|
+
const canvas = within(canvasElement);
|
|
73
|
+
await canvas.findByTitle("Required");
|
|
74
|
+
await canvas.findByText("[Insert label]");
|
|
75
|
+
await canvas.findByText("[Insert description]");
|
|
76
|
+
await canvas.findByText("[Insert helper]");
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const WithLongOptions: StoryObj<typeof Combobox> = {
|
|
81
|
+
args: {
|
|
82
|
+
...Playground.args,
|
|
83
|
+
items: [
|
|
84
|
+
{ text: "Option 1", id: "1" },
|
|
85
|
+
{
|
|
86
|
+
text: "Very long option that should be truncated",
|
|
87
|
+
id: "very-long",
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
value: "very-long",
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const Disabled: StoryObj<typeof Combobox> = {
|
|
95
|
+
args: {
|
|
96
|
+
...Playground.args,
|
|
97
|
+
disabled: true,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const Focus: StoryObj<typeof Combobox> = {
|
|
102
|
+
args: {
|
|
103
|
+
...Playground.args,
|
|
104
|
+
value: "2",
|
|
105
|
+
inputProps: {
|
|
106
|
+
...Playground.args?.inputProps,
|
|
107
|
+
// @ts-ignore
|
|
108
|
+
"data-force-focus": true,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const WithError: StoryObj<typeof Combobox> = {
|
|
114
|
+
args: {
|
|
115
|
+
...Playground.args,
|
|
116
|
+
value: "2",
|
|
117
|
+
helperText: undefined,
|
|
118
|
+
errorHelperText: "Error message",
|
|
119
|
+
errorHelperTextIcon: "error_outline",
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const WithSuccess: StoryObj<typeof Combobox> = {
|
|
124
|
+
args: {
|
|
125
|
+
...Playground.args,
|
|
126
|
+
value: "2",
|
|
127
|
+
helperText: undefined,
|
|
128
|
+
successHelperText: "Success message",
|
|
129
|
+
successHelperTextIcon: "check",
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const WithWarning: StoryObj<typeof Combobox> = {
|
|
134
|
+
args: {
|
|
135
|
+
...Playground.args,
|
|
136
|
+
helperText: undefined,
|
|
137
|
+
warningHelperText: "Warning message",
|
|
138
|
+
warningHelperTextIcon: "warning_amber",
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const Horizontal: StoryObj<typeof Combobox> = {
|
|
143
|
+
args: {
|
|
144
|
+
...Playground.args,
|
|
145
|
+
description: undefined,
|
|
146
|
+
orientation: "horizontal",
|
|
147
|
+
wrapperProps: undefined,
|
|
148
|
+
fullWidth: false,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export const WithTallDropdown: StoryObj<typeof Combobox> = {
|
|
153
|
+
args: {
|
|
154
|
+
...Playground.args,
|
|
155
|
+
tallDropdown: true,
|
|
156
|
+
},
|
|
157
|
+
play: async ({ canvasElement }) => {
|
|
158
|
+
const canvas = within(canvasElement);
|
|
159
|
+
const user = userEvent.setup();
|
|
160
|
+
await user.type(canvas.getByLabelText("[Insert label]"), "Option");
|
|
161
|
+
const listbox = await screen.findByRole("listbox");
|
|
162
|
+
await expect(listbox).toHaveAttribute("data-tall", "true");
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export const FullWidth: StoryObj<typeof Combobox> = {
|
|
167
|
+
parameters: {
|
|
168
|
+
...meta.parameters,
|
|
169
|
+
layout: "padded",
|
|
170
|
+
},
|
|
171
|
+
args: {
|
|
172
|
+
...Playground.args,
|
|
173
|
+
wrapperProps: undefined,
|
|
174
|
+
inputProps: undefined,
|
|
175
|
+
fullWidth: true,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## How to test this component
|
|
181
|
+
|
|
182
|
+
Here are some more advanced stories with more testing coverage and examples that you can read to understand how to test this component.
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
import { Meta, StoryObj } from "@storybook/react-vite";
|
|
186
|
+
|
|
187
|
+
import Combobox from "../Combobox";
|
|
188
|
+
import { Playground } from "../Combobox.stories";
|
|
189
|
+
import { userEvent, within, screen, expect } from "storybook/test";
|
|
190
|
+
import { expectNotPresent } from "@internal/test-utils-storybook/test-utils-storybook";
|
|
191
|
+
import * as ComboboxStories from "../Combobox.stories";
|
|
192
|
+
import { useEffect, useState } from "react";
|
|
193
|
+
import { useController, useForm } from "react-hook-form";
|
|
194
|
+
|
|
195
|
+
const meta: Meta<typeof Combobox> = {
|
|
196
|
+
component: Combobox,
|
|
197
|
+
...ComboboxStories.default,
|
|
198
|
+
parameters: {
|
|
199
|
+
...ComboboxStories.default.parameters,
|
|
200
|
+
chromatic: { disableSnapshot: true },
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
export default meta;
|
|
204
|
+
|
|
205
|
+
export const ShouldSelectAValue: StoryObj<typeof Combobox> = {
|
|
206
|
+
args: {
|
|
207
|
+
...Playground.args,
|
|
208
|
+
},
|
|
209
|
+
play: async ({ canvasElement, args }) => {
|
|
210
|
+
const canvas = within(canvasElement);
|
|
211
|
+
const user = userEvent.setup({ delay: 50 });
|
|
212
|
+
await user.type(canvas.getByLabelText("[Insert label]"), "9");
|
|
213
|
+
await screen.findByText("Option 9", { selector: "span" });
|
|
214
|
+
await user.click(screen.getByText("Option 9", { selector: "span" }));
|
|
215
|
+
await expectNotPresent(() =>
|
|
216
|
+
screen.queryByText("Option 1", { selector: "span" }),
|
|
217
|
+
);
|
|
218
|
+
await expect(canvas.getByLabelText("[Insert label]")).toHaveValue(
|
|
219
|
+
"Option 9",
|
|
220
|
+
);
|
|
221
|
+
await expect((args as any).onChange).toHaveBeenCalledWith("9");
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export const TestWithCustomAriaLabel: StoryObj<typeof Combobox> = {
|
|
226
|
+
args: {
|
|
227
|
+
...Playground.args,
|
|
228
|
+
label: (
|
|
229
|
+
<div>
|
|
230
|
+
Something <i>not</i> serializable
|
|
231
|
+
</div>
|
|
232
|
+
),
|
|
233
|
+
"aria-label": "Select me",
|
|
234
|
+
},
|
|
235
|
+
play: async ({ canvasElement, args }) => {
|
|
236
|
+
const canvas = within(canvasElement);
|
|
237
|
+
const user = userEvent.setup({ delay: 50 });
|
|
238
|
+
await user.type(canvas.getByLabelText("Select me"), "3");
|
|
239
|
+
await screen.findByText("Option 3", { selector: "span" });
|
|
240
|
+
await user.click(screen.getByText("Option 3", { selector: "span" }));
|
|
241
|
+
await expectNotPresent(() =>
|
|
242
|
+
screen.queryByText("Option 1", { selector: "span" }),
|
|
243
|
+
);
|
|
244
|
+
await expect(canvas.getByLabelText("Select me")).toHaveValue("Option 3");
|
|
245
|
+
await expect((args as any).onChange).toHaveBeenCalledWith("3");
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
export const ShouldDeselectAValue: StoryObj<typeof Combobox> = {
|
|
250
|
+
args: {
|
|
251
|
+
...Playground.args,
|
|
252
|
+
},
|
|
253
|
+
play: async ({ canvasElement, args }) => {
|
|
254
|
+
const canvas = within(canvasElement);
|
|
255
|
+
const user = userEvent.setup({ delay: 50 });
|
|
256
|
+
await user.type(canvas.getByLabelText("[Insert label]"), "Option 3");
|
|
257
|
+
await screen.findByText("Option 3", { selector: "span" });
|
|
258
|
+
await user.click(screen.getByText("Option 3", { selector: "span" }));
|
|
259
|
+
await expect(canvas.getByLabelText("[Insert label]")).toHaveValue(
|
|
260
|
+
"Option 3",
|
|
261
|
+
);
|
|
262
|
+
await expect((args as any).onChange).toHaveBeenCalledWith("3");
|
|
263
|
+
await user.clear(canvas.getByLabelText("[Insert label]"));
|
|
264
|
+
await expect(canvas.getByLabelText("[Insert label]")).toHaveValue("");
|
|
265
|
+
await expect((args as any).onChange).toHaveBeenCalledWith("");
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
export const ShouldNotSelectADisabledOption: StoryObj<typeof Combobox> = {
|
|
270
|
+
args: {
|
|
271
|
+
...Playground.args,
|
|
272
|
+
},
|
|
273
|
+
play: async ({ canvasElement, args }) => {
|
|
274
|
+
const canvas = within(canvasElement);
|
|
275
|
+
const user = userEvent.setup({ delay: 50 });
|
|
276
|
+
await user.type(canvas.getByLabelText("[Insert label]"), "disabled");
|
|
277
|
+
await screen.findByText("Option disabled", { selector: "span" });
|
|
278
|
+
await user.click(screen.getByText("Option disabled", { selector: "span" }));
|
|
279
|
+
await expect(canvas.getByLabelText("[Insert label]")).not.toHaveValue(
|
|
280
|
+
"Option disabled",
|
|
281
|
+
);
|
|
282
|
+
await expect((args as any).onChange).not.toHaveBeenCalled();
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
export const TestDisabledCombobox: StoryObj<typeof Combobox> = {
|
|
287
|
+
args: {
|
|
288
|
+
...Playground.args,
|
|
289
|
+
disabled: true,
|
|
290
|
+
},
|
|
291
|
+
play: async ({ canvasElement }) => {
|
|
292
|
+
const canvas = within(canvasElement);
|
|
293
|
+
const user = userEvent.setup({ delay: 50 });
|
|
294
|
+
await expect(canvas.getByLabelText("[Insert label]")).toBeDisabled();
|
|
295
|
+
await user.type(canvas.getByLabelText("[Insert label]"), "1");
|
|
296
|
+
await expectNotPresent(() =>
|
|
297
|
+
screen.queryByText("Option 1", { selector: "span" }),
|
|
298
|
+
);
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
export const ExampleControlledValue: StoryObj<typeof Combobox> = {
|
|
303
|
+
render: () => {
|
|
304
|
+
const Form = () => {
|
|
305
|
+
const [value, setValue] = useState<string>("");
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<>
|
|
309
|
+
<div>The selected value is : {value}</div>
|
|
310
|
+
<Combobox
|
|
311
|
+
id="my-combobox"
|
|
312
|
+
label="[Insert label]"
|
|
313
|
+
value={value}
|
|
314
|
+
onChange={setValue}
|
|
315
|
+
defaultItems={Array.from({ length: 30 }, (_, index) => ({
|
|
316
|
+
text: `Option ${index + 1}`,
|
|
317
|
+
id: `${index + 1}`,
|
|
318
|
+
}))}
|
|
319
|
+
/>
|
|
320
|
+
</>
|
|
321
|
+
);
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
return <Form />;
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
export const TestControlleValuedMode: StoryObj<typeof Combobox> = {
|
|
329
|
+
render: ExampleControlledValue.render,
|
|
330
|
+
play: async ({ canvasElement }) => {
|
|
331
|
+
const canvas = within(canvasElement);
|
|
332
|
+
const user = userEvent.setup({ delay: 50 });
|
|
333
|
+
await user.type(canvas.getByLabelText("[Insert label]"), "2");
|
|
334
|
+
await screen.findByText("Option 2", { selector: "span" });
|
|
335
|
+
await user.click(screen.getByText("Option 2", { selector: "span" }));
|
|
336
|
+
await expectNotPresent(() =>
|
|
337
|
+
screen.queryByText("Option 1", { selector: "span" }),
|
|
338
|
+
);
|
|
339
|
+
await expect(canvas.getByLabelText("[Insert label]")).toHaveValue(
|
|
340
|
+
"Option 2",
|
|
341
|
+
);
|
|
342
|
+
await canvas.findByText("The selected value is : 2");
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
export const ExampleWithReactHookForm: StoryObj<typeof Combobox> = {
|
|
347
|
+
render: (args) => {
|
|
348
|
+
const ReactHookFormExample = () => {
|
|
349
|
+
const { control } = useForm<any>();
|
|
350
|
+
const comboboxField = useController({ control, name: "combobox" });
|
|
351
|
+
|
|
352
|
+
return (
|
|
353
|
+
<>
|
|
354
|
+
<div>The selected value is : {comboboxField.field.value}</div>
|
|
355
|
+
<Combobox
|
|
356
|
+
id="my-combobox"
|
|
357
|
+
label="[Insert label]"
|
|
358
|
+
value={comboboxField.field.value}
|
|
359
|
+
onChange={comboboxField.field.onChange}
|
|
360
|
+
defaultItems={[
|
|
361
|
+
{ text: "Option 1", id: "1" },
|
|
362
|
+
{ text: "Option 2", id: "2" },
|
|
363
|
+
{ text: "Option 3", id: "3" },
|
|
364
|
+
]}
|
|
365
|
+
/>
|
|
366
|
+
</>
|
|
367
|
+
);
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
return <ReactHookFormExample {...args} />;
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
export const TestReactHookForm: StoryObj<typeof Combobox> = {
|
|
375
|
+
render: ExampleWithReactHookForm.render as any,
|
|
376
|
+
play: async ({ canvasElement }) => {
|
|
377
|
+
const canvas = within(canvasElement);
|
|
378
|
+
const user = userEvent.setup({ delay: 50 });
|
|
379
|
+
await user.type(canvas.getByLabelText("[Insert label]"), "2");
|
|
380
|
+
await screen.findByText("Option 2", { selector: "span" });
|
|
381
|
+
await user.click(screen.getByText("Option 2", { selector: "span" }));
|
|
382
|
+
await expectNotPresent(() =>
|
|
383
|
+
screen.queryByText("Option 1", { selector: "span" }),
|
|
384
|
+
);
|
|
385
|
+
await expect(canvas.getByLabelText("[Insert label]")).toHaveValue(
|
|
386
|
+
"Option 2",
|
|
387
|
+
);
|
|
388
|
+
await canvas.findByText("The selected value is : 2");
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
export const ExampleBackendManaged: StoryObj<typeof Combobox> = {
|
|
393
|
+
render: () => {
|
|
394
|
+
type Item = {
|
|
395
|
+
id: string;
|
|
396
|
+
text: string;
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const ALL_ITEMS: Item[] = Array.from({ length: 100 }, (_, index) => {
|
|
400
|
+
return {
|
|
401
|
+
id: `${index + 1}`,
|
|
402
|
+
text: `Option ${index + 1}`,
|
|
403
|
+
};
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const fakeApi = (search: string): Promise<Item[]> => {
|
|
407
|
+
return new Promise((resolve) => {
|
|
408
|
+
setTimeout(() => {
|
|
409
|
+
resolve(
|
|
410
|
+
ALL_ITEMS.filter((item) => item.text.includes(search)).slice(0, 5),
|
|
411
|
+
);
|
|
412
|
+
}, 300);
|
|
413
|
+
});
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const Form = () => {
|
|
417
|
+
const [value, setValue] = useState<string>("");
|
|
418
|
+
const [inputValue, setInputValue] = useState<string>("");
|
|
419
|
+
const [options, setOptions] = useState<Item[]>([]);
|
|
420
|
+
|
|
421
|
+
useEffect(() => {
|
|
422
|
+
fakeApi(inputValue).then((options) => setOptions(options));
|
|
423
|
+
}, [inputValue]);
|
|
424
|
+
|
|
425
|
+
return (
|
|
426
|
+
<>
|
|
427
|
+
<div>The selected value is : {value}</div>
|
|
428
|
+
<Combobox
|
|
429
|
+
id="my-combobox"
|
|
430
|
+
label="[Insert label]"
|
|
431
|
+
value={value}
|
|
432
|
+
onChange={setValue}
|
|
433
|
+
inputValue={inputValue}
|
|
434
|
+
onInputChange={setInputValue}
|
|
435
|
+
items={options} // Please note we now use `items` instead of `defaultItems`. This tells Combobox to not filter the items internally.
|
|
436
|
+
/>
|
|
437
|
+
</>
|
|
438
|
+
);
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
return <Form />;
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
export const TestExampleBackendManaged: StoryObj<typeof Combobox> = {
|
|
446
|
+
render: ExampleBackendManaged.render as any,
|
|
447
|
+
play: async ({ canvasElement }) => {
|
|
448
|
+
const canvas = within(canvasElement);
|
|
449
|
+
const user = userEvent.setup({ delay: 50 });
|
|
450
|
+
await user.type(canvas.getByLabelText("[Insert label]"), "Option 99");
|
|
451
|
+
await screen.findByText("Option 99", { selector: "span" }); // Wait for the option to show up (can be async data fetching)
|
|
452
|
+
await user.click(screen.getByText("Option 99", { selector: "span" })); // Click on the option
|
|
453
|
+
await canvas.findByText("The selected value is : 99"); // Check that the value is selected (will probably be different depending on the implementation)
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
export const ExampleWithLinks: StoryObj<typeof Combobox> = {
|
|
458
|
+
render: () => {
|
|
459
|
+
const Form = () => {
|
|
460
|
+
const [value, setValue] = useState<string>("");
|
|
461
|
+
|
|
462
|
+
return (
|
|
463
|
+
<>
|
|
464
|
+
<div>The selected value is : {value}</div>
|
|
465
|
+
<Combobox
|
|
466
|
+
id="my-combobox"
|
|
467
|
+
label="[Insert label]"
|
|
468
|
+
value={value}
|
|
469
|
+
onChange={setValue}
|
|
470
|
+
defaultItems={[
|
|
471
|
+
{ text: "Option 1", id: "1" },
|
|
472
|
+
{ text: "Google", id: "google", href: "https://google.com" },
|
|
473
|
+
]}
|
|
474
|
+
/>
|
|
475
|
+
</>
|
|
476
|
+
);
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
return <Form />;
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
export const ExampleWithCustomFiltering: StoryObj<typeof Combobox> = {
|
|
484
|
+
render: () => {
|
|
485
|
+
const options = [
|
|
486
|
+
{ id: "1", text: "fake@email.com" },
|
|
487
|
+
{ id: "2", text: "anotherfake@email.com" },
|
|
488
|
+
{ id: "3", text: "bob@email.com" },
|
|
489
|
+
{ id: "4", text: "joe@email.com" },
|
|
490
|
+
{ id: "5", text: "yourEmail@email.com" },
|
|
491
|
+
{ id: "6", text: "valid@email.com" },
|
|
492
|
+
];
|
|
493
|
+
|
|
494
|
+
const Form = () => {
|
|
495
|
+
const [value, setValue] = useState<string>("");
|
|
496
|
+
|
|
497
|
+
return (
|
|
498
|
+
<>
|
|
499
|
+
<div>The selected value is : {value}</div>
|
|
500
|
+
<Combobox
|
|
501
|
+
id="my-combobox"
|
|
502
|
+
label="[Insert label]"
|
|
503
|
+
value={value}
|
|
504
|
+
onChange={setValue}
|
|
505
|
+
defaultItems={options}
|
|
506
|
+
defaultFilter={(itemText, filterValue) =>
|
|
507
|
+
// Prefer a `startsWith` filter instead of the default `includes`
|
|
508
|
+
itemText.toLowerCase().startsWith(filterValue.toLowerCase())
|
|
509
|
+
}
|
|
510
|
+
/>
|
|
511
|
+
</>
|
|
512
|
+
);
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
return <Form />;
|
|
516
|
+
},
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
export const MultipleSelectionExample: StoryObj<typeof Combobox> = {
|
|
520
|
+
parameters: {
|
|
521
|
+
...meta.parameters,
|
|
522
|
+
layout: "padded",
|
|
523
|
+
},
|
|
524
|
+
render: () => {
|
|
525
|
+
const data = new Array(10).fill(0).map((_, index) => ({
|
|
526
|
+
id: `${index + 1}`,
|
|
527
|
+
name: `Perimeter ${index + 1}`,
|
|
528
|
+
}));
|
|
529
|
+
|
|
530
|
+
const ParentComponent = () => {
|
|
531
|
+
// List of the selected perimeters ids
|
|
532
|
+
const [selectedPerimetersIds, setSelectedPerimetersIds] = useState<
|
|
533
|
+
Array<string>
|
|
534
|
+
>([]);
|
|
535
|
+
|
|
536
|
+
// List of the selected perimeters (derived state)
|
|
537
|
+
const selectedPerimeters = data.filter((perimeter) =>
|
|
538
|
+
selectedPerimetersIds.includes(perimeter.id),
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
return (
|
|
542
|
+
<div style={{ display: "flex", gap: "var(--spacing-xs)" }}>
|
|
543
|
+
<Combobox
|
|
544
|
+
id="perimeters-combobox"
|
|
545
|
+
label="Select perimeters"
|
|
546
|
+
value="" // Need to force the value as we "fake" the normal combobox behavior
|
|
547
|
+
selectionMode="multiple" // Enable the checkboxes on the select items
|
|
548
|
+
onChange={(id) => {
|
|
549
|
+
// if element is already selected, remove it
|
|
550
|
+
if (selectedPerimetersIds.includes(id)) {
|
|
551
|
+
setSelectedPerimetersIds(
|
|
552
|
+
selectedPerimetersIds.filter(
|
|
553
|
+
(perimeterId) => perimeterId !== id,
|
|
554
|
+
),
|
|
555
|
+
);
|
|
556
|
+
} else {
|
|
557
|
+
// If the element is not selected, add it to the list
|
|
558
|
+
setSelectedPerimetersIds([...selectedPerimetersIds, id]);
|
|
559
|
+
}
|
|
560
|
+
}}
|
|
561
|
+
defaultItems={data.map((perimeter) => ({
|
|
562
|
+
text: perimeter.name,
|
|
563
|
+
id: perimeter.id,
|
|
564
|
+
isSelected: selectedPerimetersIds.includes(perimeter.id),
|
|
565
|
+
}))}
|
|
566
|
+
/>
|
|
567
|
+
|
|
568
|
+
<div>
|
|
569
|
+
<div>Selected perimeters:</div>
|
|
570
|
+
<ul>
|
|
571
|
+
{selectedPerimeters.map((perimeter) => (
|
|
572
|
+
<li key={perimeter.id}>{perimeter.name}</li>
|
|
573
|
+
))}
|
|
574
|
+
</ul>
|
|
575
|
+
</div>
|
|
576
|
+
</div>
|
|
577
|
+
);
|
|
578
|
+
};
|
|
579
|
+
return <ParentComponent />;
|
|
580
|
+
},
|
|
581
|
+
};
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
## Developer notes
|
|
585
|
+
|
|
586
|
+
Here are the notes available for the developer on the built Storybook, you can read them to understand the component and how to use it.
|
|
587
|
+
|
|
588
|
+
```mdx
|
|
589
|
+
import {
|
|
590
|
+
Meta,
|
|
591
|
+
Canvas,
|
|
592
|
+
Controls,
|
|
593
|
+
Source,
|
|
594
|
+
ArgTypes,
|
|
595
|
+
Stories,
|
|
596
|
+
} from "@storybook/addon-docs/blocks";
|
|
597
|
+
|
|
598
|
+
import * as Combobox from "./Combobox.stories";
|
|
599
|
+
import * as ComboboxTests from "./tests/Combobox.stories";
|
|
600
|
+
|
|
601
|
+
<Meta of={Combobox} />
|
|
602
|
+
|
|
603
|
+
# Combobox
|
|
604
|
+
|
|
605
|
+
A combo box combines a text input with a listbox, allowing users to filter a list of options to items matching a query.
|
|
606
|
+
|
|
607
|
+
A combo box can be built using the `<datalist>` HTML element, but this is very limited in functionality and difficult to style.
|
|
608
|
+
ComboBox helps achieve accessible combo box and autocomplete components that can be styled as needed.
|
|
609
|
+
|
|
610
|
+
- **Flexible** – Support for selecting pre-defined values, controlled and uncontrolled state, custom filter functions, async loading, disabled items.
|
|
611
|
+
- **Keyboard navigation** – ComboBox can be opened and navigated using the arrow keys, along with page up/down, home/end, etc. The list of options is filtered while typing into the input, and items can be selected with the enter key.
|
|
612
|
+
- **Accessible** – Follows the ARIA combobox pattern. Custom localized announcements are included for option focusing, filtering, and selection using an ARIA live region to ensure announcements are clear and consistent.
|
|
613
|
+
|
|
614
|
+
## Basic usage (with state), filtering managed internally
|
|
615
|
+
|
|
616
|
+
Here is the most simple usage of the combobox, with a list of predefined items and a controlled state.
|
|
617
|
+
|
|
618
|
+
**Please note that we are using the `defaultItems` prop to pass the items to the combobox.**
|
|
619
|
+
Doing so will let the combobox manage the filtered list of items internally.
|
|
620
|
+
|
|
621
|
+
<Source of={ComboboxTests.ExampleControlledValue} type="code" dark />
|
|
622
|
+
|
|
623
|
+
## Custom filtering
|
|
624
|
+
|
|
625
|
+
By default, ComboBox uses a "includes" function to filter the list of options.
|
|
626
|
+
This can be overridden using the `defaultFilter` prop (or by using the `items` prop to control the filtered list).
|
|
627
|
+
**When `items` is provided rather than `defaultItems`, ComboBox does no filtering of its own.**
|
|
628
|
+
|
|
629
|
+
The following example is a slightly modified version of the previous example, with a custom filter function that uses the `defaultFilter` prop.
|
|
630
|
+
|
|
631
|
+
<Source of={ComboboxTests.ExampleWithCustomFiltering} type="code" dark />
|
|
632
|
+
|
|
633
|
+
## With React hook form
|
|
634
|
+
|
|
635
|
+
Here is an example of how to use the combobox with React hook form.
|
|
636
|
+
|
|
637
|
+
<Source of={ComboboxTests.ExampleWithReactHookForm} type="code" dark />
|
|
638
|
+
|
|
639
|
+
## Async filtering on backend
|
|
640
|
+
|
|
641
|
+
Here is an example of how to use the combobox with async filtering on backend (i.e. API call).
|
|
642
|
+
|
|
643
|
+
Use this if you have a large list of items and you have an API endpoint that is responsible for filtering the items.
|
|
644
|
+
|
|
645
|
+
**Please note that we are using the `items` prop to pass the items to the combobox (instead of `defaultItems`).**
|
|
646
|
+
Doing so will disable the internal filtering of the combobox, and you will be responsible for managing the filtered list of items.
|
|
647
|
+
|
|
648
|
+
<Source of={ComboboxTests.ExampleBackendManaged} type="code" dark />
|
|
649
|
+
|
|
650
|
+
## Links
|
|
651
|
+
|
|
652
|
+
By default, interacting with an item in a ComboBox selects it and updates the input value.
|
|
653
|
+
Alternatively, items may be links to another page or website.
|
|
654
|
+
This can be achieved by passing the `href` attribute to the item (`items` prop).
|
|
655
|
+
Interacting with link items navigates to the provided URL and does not update the selection or input value.
|
|
656
|
+
|
|
657
|
+
<Source of={ComboboxTests.ExampleWithLinks} type="code" dark />
|
|
658
|
+
|
|
659
|
+
## Multiple selection
|
|
660
|
+
|
|
661
|
+
Here is an example of how to use the combobox with multiple selection.
|
|
662
|
+
Please note that this is a temporary solution, the React Aria combobox does not support multiple selection yet.
|
|
663
|
+
The implementation details may change in the future.
|
|
664
|
+
|
|
665
|
+
<Source of={ComboboxTests.MultipleSelectionExample} type="code" dark />
|
|
666
|
+
|
|
667
|
+
## How to test this component?
|
|
668
|
+
|
|
669
|
+
Here is a code snippet that you can use as an example to know how to test this component with react testing library:
|
|
670
|
+
|
|
671
|
+
<Source of={ComboboxTests.TestExampleBackendManaged} type="code" dark />
|
|
672
|
+
|
|
673
|
+
## Playground & Props
|
|
674
|
+
|
|
675
|
+
<Canvas of={Combobox.Playground} />
|
|
676
|
+
|
|
677
|
+
<Controls of={Combobox.Playground} />
|
|
678
|
+
|
|
679
|
+
<Stories />
|
|
680
|
+
```
|