@rovula/ui 0.1.40 → 0.1.42
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/cjs/bundle.css +3 -0
- package/dist/cjs/bundle.js +4 -4
- package/dist/cjs/bundle.js.map +1 -1
- package/dist/cjs/types/components/AutoComplete/AutoComplete.d.ts +74 -0
- package/dist/cjs/types/components/AutoComplete/AutoComplete.stories.d.ts +361 -0
- package/dist/cjs/types/components/AutoComplete/index.d.ts +2 -0
- package/dist/cjs/types/index.d.ts +3 -0
- package/dist/components/AutoComplete/AutoComplete.js +103 -0
- package/dist/components/AutoComplete/AutoComplete.stories.js +212 -0
- package/dist/components/AutoComplete/index.js +1 -0
- package/dist/components/Dialog/Dialog.js +5 -1
- package/dist/components/TextInput/TextInput.js +2 -1
- package/dist/esm/bundle.css +3 -0
- package/dist/esm/bundle.js +4 -4
- package/dist/esm/bundle.js.map +1 -1
- package/dist/esm/types/components/AutoComplete/AutoComplete.d.ts +74 -0
- package/dist/esm/types/components/AutoComplete/AutoComplete.stories.d.ts +361 -0
- package/dist/esm/types/components/AutoComplete/index.d.ts +2 -0
- package/dist/esm/types/index.d.ts +3 -0
- package/dist/index.d.ts +75 -2
- package/dist/index.js +2 -0
- package/dist/src/theme/global.css +35 -31
- package/package.json +1 -1
- package/src/components/AutoComplete/AutoComplete.stories.tsx +525 -0
- package/src/components/AutoComplete/AutoComplete.tsx +374 -0
- package/src/components/AutoComplete/index.ts +2 -0
- package/src/components/Dialog/Dialog.tsx +4 -0
- package/src/components/TextInput/TextInput.tsx +13 -8
- package/src/index.ts +3 -0
- package/src/theme/themes/variable.css +31 -31
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import * as yup from "yup";
|
|
4
|
+
import AutoComplete, { type AutoCompleteOption } from "./AutoComplete";
|
|
5
|
+
import Avatar from "../Avatar/Avatar";
|
|
6
|
+
import Text from "../Text/Text";
|
|
7
|
+
import Button from "../Button/Button";
|
|
8
|
+
import {
|
|
9
|
+
Dialog,
|
|
10
|
+
DialogContent,
|
|
11
|
+
DialogHeader,
|
|
12
|
+
DialogTitle,
|
|
13
|
+
DialogDescription,
|
|
14
|
+
DialogBody,
|
|
15
|
+
DialogFooter,
|
|
16
|
+
DialogTrigger,
|
|
17
|
+
DialogClose,
|
|
18
|
+
} from "../Dialog/Dialog";
|
|
19
|
+
import { Form, useControlledForm } from "../Form/Form";
|
|
20
|
+
import { Field } from "../Form/Field";
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Meta
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
const meta = {
|
|
27
|
+
title: "Components/AutoComplete",
|
|
28
|
+
component: AutoComplete,
|
|
29
|
+
tags: ["autodocs"],
|
|
30
|
+
parameters: {
|
|
31
|
+
layout: "fullscreen",
|
|
32
|
+
},
|
|
33
|
+
decorators: [
|
|
34
|
+
(Story) => (
|
|
35
|
+
<div className="p-8 bg-bg-bg1">
|
|
36
|
+
<Story />
|
|
37
|
+
</div>
|
|
38
|
+
),
|
|
39
|
+
],
|
|
40
|
+
} satisfies Meta<typeof AutoComplete>;
|
|
41
|
+
|
|
42
|
+
export default meta;
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Shared fixtures
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
const fruits: AutoCompleteOption[] = [
|
|
49
|
+
{ value: "apple", label: "Apple" },
|
|
50
|
+
{ value: "banana", label: "Banana" },
|
|
51
|
+
{ value: "blueberry", label: "Blueberry" },
|
|
52
|
+
{ value: "cherry", label: "Cherry" },
|
|
53
|
+
{ value: "grape", label: "Grape" },
|
|
54
|
+
{ value: "mango", label: "Mango" },
|
|
55
|
+
{ value: "orange", label: "Orange" },
|
|
56
|
+
{ value: "peach", label: "Peach" },
|
|
57
|
+
{ value: "pineapple", label: "Pineapple" },
|
|
58
|
+
{ value: "strawberry", label: "Strawberry" },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Default — basic client-side filter
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
export const Default: StoryObj<typeof AutoComplete> = {
|
|
66
|
+
render: () => {
|
|
67
|
+
const [value, setValue] = useState("");
|
|
68
|
+
const filtered = fruits.filter((f) =>
|
|
69
|
+
f.label.toLowerCase().includes(value.toLowerCase()),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<AutoComplete
|
|
74
|
+
label="Fruit"
|
|
75
|
+
value={value}
|
|
76
|
+
options={filtered}
|
|
77
|
+
onChange={setValue}
|
|
78
|
+
onSearch={setValue}
|
|
79
|
+
noOptionsText="No fruit found"
|
|
80
|
+
fullwidth
|
|
81
|
+
data-testid="autocomplete-default"
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// showNoOptions — display "no results" message when empty (like MUI)
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
export const ShowNoOptionsMessage: StoryObj = {
|
|
92
|
+
name: "Show No Options Message",
|
|
93
|
+
render: () => {
|
|
94
|
+
const [value, setValue] = useState("");
|
|
95
|
+
const filtered = fruits.filter((f) =>
|
|
96
|
+
f.label.toLowerCase().includes(value.toLowerCase()),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div className="flex flex-col gap-6 w-full">
|
|
101
|
+
<div>
|
|
102
|
+
<Text variant="small4" className="text-text-g-contrast-medium mb-2">
|
|
103
|
+
showNoOptions=false (default) — popover hides when empty
|
|
104
|
+
</Text>
|
|
105
|
+
<AutoComplete
|
|
106
|
+
label="Fruit (no message)"
|
|
107
|
+
value={value}
|
|
108
|
+
options={filtered}
|
|
109
|
+
onChange={setValue}
|
|
110
|
+
onSearch={setValue}
|
|
111
|
+
showNoOptions={false}
|
|
112
|
+
noOptionsText="No fruit found"
|
|
113
|
+
fullwidth
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
<div>
|
|
117
|
+
<Text variant="small4" className="text-text-g-contrast-medium mb-2">
|
|
118
|
+
showNoOptions=true — shows "No fruit found" when no match
|
|
119
|
+
</Text>
|
|
120
|
+
<AutoComplete
|
|
121
|
+
label="Fruit (with message)"
|
|
122
|
+
value={value}
|
|
123
|
+
options={filtered}
|
|
124
|
+
onChange={setValue}
|
|
125
|
+
onSearch={setValue}
|
|
126
|
+
showNoOptions
|
|
127
|
+
noOptionsText="No fruit found"
|
|
128
|
+
fullwidth
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// FreeSolo — typed value is committed even without selecting from list
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
export const FreeSolo: StoryObj = {
|
|
141
|
+
name: "Free Solo (typed value always committed)",
|
|
142
|
+
render: () => {
|
|
143
|
+
const [value, setValue] = useState("");
|
|
144
|
+
const [committed, setCommitted] = useState("");
|
|
145
|
+
|
|
146
|
+
const filtered = fruits.filter((f) =>
|
|
147
|
+
f.label.toLowerCase().includes(value.toLowerCase()),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<div className="flex flex-col gap-4 w-full">
|
|
152
|
+
<AutoComplete
|
|
153
|
+
label="Email or name"
|
|
154
|
+
value={value}
|
|
155
|
+
options={filtered}
|
|
156
|
+
onChange={(v) => {
|
|
157
|
+
setValue(v);
|
|
158
|
+
setCommitted(v);
|
|
159
|
+
}}
|
|
160
|
+
onSearch={setValue}
|
|
161
|
+
showNoOptions
|
|
162
|
+
noOptionsText="No match — your typed value will be used"
|
|
163
|
+
fullwidth
|
|
164
|
+
/>
|
|
165
|
+
<Text variant="small2" className="text-text-g-contrast-medium">
|
|
166
|
+
Committed value: <strong>{committed || "—"}</strong>
|
|
167
|
+
</Text>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Async — simulates server search with debounce
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
const allUsers = [
|
|
178
|
+
{ value: "alice@co.com", label: "alice@co.com", name: "Alice" },
|
|
179
|
+
{ value: "bob@co.com", label: "bob@co.com", name: "Bob" },
|
|
180
|
+
{ value: "charlie@co.com", label: "charlie@co.com", name: "Charlie" },
|
|
181
|
+
{ value: "diana@co.com", label: "diana@co.com", name: "Diana" },
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
type UserOption = AutoCompleteOption & { name: string };
|
|
185
|
+
|
|
186
|
+
const AsyncAutoComplete = () => {
|
|
187
|
+
const [value, setValue] = useState("");
|
|
188
|
+
const [options, setOptions] = useState<UserOption[]>(allUsers);
|
|
189
|
+
const [loading, setLoading] = useState(false);
|
|
190
|
+
|
|
191
|
+
const handleSearch = (query: string) => {
|
|
192
|
+
setLoading(true);
|
|
193
|
+
setTimeout(() => {
|
|
194
|
+
setOptions(
|
|
195
|
+
query
|
|
196
|
+
? allUsers.filter(
|
|
197
|
+
(u) =>
|
|
198
|
+
u.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
199
|
+
u.value.toLowerCase().includes(query.toLowerCase()),
|
|
200
|
+
)
|
|
201
|
+
: allUsers,
|
|
202
|
+
);
|
|
203
|
+
setLoading(false);
|
|
204
|
+
}, 600);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<AutoComplete<UserOption>
|
|
209
|
+
label="Search user"
|
|
210
|
+
value={value}
|
|
211
|
+
options={options}
|
|
212
|
+
loading={loading}
|
|
213
|
+
onChange={setValue}
|
|
214
|
+
onSearch={handleSearch}
|
|
215
|
+
onSelect={(option) => setValue(option.value)}
|
|
216
|
+
filterOptions={(x) => x}
|
|
217
|
+
showNoOptions
|
|
218
|
+
noOptionsText="No users found"
|
|
219
|
+
renderOption={(option) => (
|
|
220
|
+
<div className="flex items-center gap-3">
|
|
221
|
+
<Avatar
|
|
222
|
+
type="text"
|
|
223
|
+
text={option.name}
|
|
224
|
+
className="size-7 shrink-0 text-xs"
|
|
225
|
+
/>
|
|
226
|
+
<div className="flex flex-col min-w-0">
|
|
227
|
+
<Text variant="subtitle4" className="truncate">
|
|
228
|
+
{option.name}
|
|
229
|
+
</Text>
|
|
230
|
+
<Text
|
|
231
|
+
variant="small2"
|
|
232
|
+
className="text-text-g-contrast-medium truncate"
|
|
233
|
+
>
|
|
234
|
+
{option.value}
|
|
235
|
+
</Text>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
fullwidth
|
|
240
|
+
data-testid="autocomplete-async"
|
|
241
|
+
/>
|
|
242
|
+
);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
export const Async: StoryObj = {
|
|
246
|
+
name: "Async Search (simulated API)",
|
|
247
|
+
render: () => <AsyncAutoComplete />,
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
// Form integration — react-hook-form + yup validation + Field
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
type InviteFormValues = {
|
|
255
|
+
email: string;
|
|
256
|
+
assignee: string;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const inviteSchema = yup.object({
|
|
260
|
+
email: yup
|
|
261
|
+
.string()
|
|
262
|
+
.email("Invalid email format")
|
|
263
|
+
.required("Email is required"),
|
|
264
|
+
assignee: yup.string().required("Please select an assignee"),
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const FormIntegrationDemo = () => {
|
|
268
|
+
const [submitted, setSubmitted] = useState<InviteFormValues | null>(null);
|
|
269
|
+
const [assigneeQuery, setAssigneeQuery] = useState("");
|
|
270
|
+
|
|
271
|
+
const assigneeOptions = allUsers
|
|
272
|
+
.filter(
|
|
273
|
+
(u) =>
|
|
274
|
+
!assigneeQuery ||
|
|
275
|
+
u.name.toLowerCase().includes(assigneeQuery.toLowerCase()) ||
|
|
276
|
+
u.value.toLowerCase().includes(assigneeQuery.toLowerCase()),
|
|
277
|
+
)
|
|
278
|
+
.map((u) => ({ value: u.value, label: u.name }));
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<div className="flex flex-col gap-4 w-full max-w-sm">
|
|
282
|
+
<Form<InviteFormValues>
|
|
283
|
+
className="flex flex-col gap-4"
|
|
284
|
+
defaultValues={{ email: "", assignee: "" }}
|
|
285
|
+
validationSchema={inviteSchema}
|
|
286
|
+
mode="onBlur"
|
|
287
|
+
onSubmit={(values) => setSubmitted(values)}
|
|
288
|
+
>
|
|
289
|
+
<Field<InviteFormValues, "email">
|
|
290
|
+
name="email"
|
|
291
|
+
component={AutoComplete as React.ComponentType<any>}
|
|
292
|
+
componentProps={{
|
|
293
|
+
label: "Email",
|
|
294
|
+
placeholder: "Search by email",
|
|
295
|
+
options: allUsers
|
|
296
|
+
.filter((u) => !assigneeQuery || u.value.includes(assigneeQuery))
|
|
297
|
+
.map((u) => ({ value: u.value, label: u.value })),
|
|
298
|
+
onSearch: setAssigneeQuery,
|
|
299
|
+
showNoOptions: true,
|
|
300
|
+
noOptionsText: "No matching email",
|
|
301
|
+
required: true,
|
|
302
|
+
fullwidth: true,
|
|
303
|
+
}}
|
|
304
|
+
/>
|
|
305
|
+
<Field<InviteFormValues, "assignee">
|
|
306
|
+
name="assignee"
|
|
307
|
+
component={AutoComplete as React.ComponentType<any>}
|
|
308
|
+
componentProps={{
|
|
309
|
+
label: "Assignee",
|
|
310
|
+
placeholder: "Search by name",
|
|
311
|
+
options: assigneeOptions,
|
|
312
|
+
onSearch: setAssigneeQuery,
|
|
313
|
+
showNoOptions: true,
|
|
314
|
+
noOptionsText: "No users found",
|
|
315
|
+
required: true,
|
|
316
|
+
fullwidth: true,
|
|
317
|
+
}}
|
|
318
|
+
/>
|
|
319
|
+
<Button type="submit" fullwidth>
|
|
320
|
+
Submit
|
|
321
|
+
</Button>
|
|
322
|
+
</Form>
|
|
323
|
+
{submitted && (
|
|
324
|
+
<div className="mt-2 p-3 rounded-md bg-bg-bg2 typography-small2 text-text-g-contrast-medium">
|
|
325
|
+
Submitted: <strong>{JSON.stringify(submitted)}</strong>
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
328
|
+
</div>
|
|
329
|
+
);
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
export const WithFormValidation: StoryObj = {
|
|
333
|
+
name: "Form — react-hook-form + yup + Field",
|
|
334
|
+
render: () => <FormIntegrationDemo />,
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
// Inside Dialog — portal + overflow escape
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
|
|
341
|
+
const dialogAssignees = allUsers.map((u) => ({
|
|
342
|
+
value: u.value,
|
|
343
|
+
label: u.name,
|
|
344
|
+
}));
|
|
345
|
+
|
|
346
|
+
const AutoCompleteInDialogDemo = () => {
|
|
347
|
+
const [query, setQuery] = useState("");
|
|
348
|
+
const [value, setValue] = useState("");
|
|
349
|
+
|
|
350
|
+
const options = dialogAssignees.filter(
|
|
351
|
+
(o) =>
|
|
352
|
+
!query ||
|
|
353
|
+
o.label.toLowerCase().includes(query.toLowerCase()) ||
|
|
354
|
+
o.value.toLowerCase().includes(query.toLowerCase()),
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
return (
|
|
358
|
+
<Dialog>
|
|
359
|
+
<DialogTrigger asChild>
|
|
360
|
+
<Button variant="outline">Open Dialog</Button>
|
|
361
|
+
</DialogTrigger>
|
|
362
|
+
<DialogContent showCloseButton>
|
|
363
|
+
<DialogHeader>
|
|
364
|
+
<DialogTitle>Assign Task</DialogTitle>
|
|
365
|
+
<DialogDescription>
|
|
366
|
+
AutoComplete inside a Dialog — popover escapes overflow correctly.
|
|
367
|
+
</DialogDescription>
|
|
368
|
+
</DialogHeader>
|
|
369
|
+
<DialogBody className="gap-4 py-2">
|
|
370
|
+
<AutoComplete
|
|
371
|
+
label="Assignee"
|
|
372
|
+
placeholder="Search by name or email"
|
|
373
|
+
value={value}
|
|
374
|
+
options={options}
|
|
375
|
+
onChange={setValue}
|
|
376
|
+
onSearch={setQuery}
|
|
377
|
+
onSelect={(opt) => setValue(opt.value)}
|
|
378
|
+
filterOptions={(x) => x}
|
|
379
|
+
showNoOptions
|
|
380
|
+
noOptionsText="No users found"
|
|
381
|
+
portal={false}
|
|
382
|
+
required
|
|
383
|
+
fullwidth
|
|
384
|
+
/>
|
|
385
|
+
</DialogBody>
|
|
386
|
+
<DialogFooter>
|
|
387
|
+
<DialogClose asChild>
|
|
388
|
+
<Button variant="outline">Cancel</Button>
|
|
389
|
+
</DialogClose>
|
|
390
|
+
<Button disabled={!value}>Confirm</Button>
|
|
391
|
+
</DialogFooter>
|
|
392
|
+
</DialogContent>
|
|
393
|
+
</Dialog>
|
|
394
|
+
);
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
export const InsideDialog: StoryObj = {
|
|
398
|
+
name: "Inside Dialog",
|
|
399
|
+
render: () => <AutoCompleteInDialogDemo />,
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// ---------------------------------------------------------------------------
|
|
403
|
+
// Sizes
|
|
404
|
+
// ---------------------------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
export const Sizes: StoryObj = {
|
|
407
|
+
render: () => {
|
|
408
|
+
const [sm, setSm] = useState("");
|
|
409
|
+
const [md, setMd] = useState("");
|
|
410
|
+
const [lg, setLg] = useState("");
|
|
411
|
+
|
|
412
|
+
const getOptions = (v: string) =>
|
|
413
|
+
fruits.filter((f) => f.label.toLowerCase().includes(v.toLowerCase()));
|
|
414
|
+
|
|
415
|
+
return (
|
|
416
|
+
<div className="flex flex-col gap-6 w-full">
|
|
417
|
+
{(
|
|
418
|
+
[
|
|
419
|
+
{ size: "sm", value: sm, onChange: setSm },
|
|
420
|
+
{ size: "md", value: md, onChange: setMd },
|
|
421
|
+
{ size: "lg", value: lg, onChange: setLg },
|
|
422
|
+
] as const
|
|
423
|
+
).map(({ size, value, onChange }) => (
|
|
424
|
+
<AutoComplete
|
|
425
|
+
key={size}
|
|
426
|
+
label={`Size: ${size}`}
|
|
427
|
+
size={size}
|
|
428
|
+
value={value}
|
|
429
|
+
options={getOptions(value)}
|
|
430
|
+
onChange={onChange}
|
|
431
|
+
onSearch={onChange}
|
|
432
|
+
noOptionsText="No fruit found"
|
|
433
|
+
fullwidth
|
|
434
|
+
/>
|
|
435
|
+
))}
|
|
436
|
+
</div>
|
|
437
|
+
);
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
// States — error, disabled
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
444
|
+
|
|
445
|
+
export const States: StoryObj = {
|
|
446
|
+
render: () => (
|
|
447
|
+
<div className="flex flex-col gap-6 w-full">
|
|
448
|
+
<AutoComplete
|
|
449
|
+
label="Error state"
|
|
450
|
+
value=""
|
|
451
|
+
options={fruits}
|
|
452
|
+
error
|
|
453
|
+
errorMessage="Please enter a valid email"
|
|
454
|
+
fullwidth
|
|
455
|
+
/>
|
|
456
|
+
<AutoComplete
|
|
457
|
+
label="Disabled state"
|
|
458
|
+
value="locked@example.com"
|
|
459
|
+
options={fruits}
|
|
460
|
+
disabled
|
|
461
|
+
fullwidth
|
|
462
|
+
/>
|
|
463
|
+
</div>
|
|
464
|
+
),
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
// ---------------------------------------------------------------------------
|
|
468
|
+
// Loading state
|
|
469
|
+
// ---------------------------------------------------------------------------
|
|
470
|
+
|
|
471
|
+
export const LoadingState: StoryObj = {
|
|
472
|
+
name: "Loading State",
|
|
473
|
+
render: () => {
|
|
474
|
+
const [value, setValue] = useState("j");
|
|
475
|
+
return (
|
|
476
|
+
<AutoComplete
|
|
477
|
+
label="Searching…"
|
|
478
|
+
value={value}
|
|
479
|
+
options={[]}
|
|
480
|
+
loading
|
|
481
|
+
onChange={setValue}
|
|
482
|
+
noOptionsText="No results"
|
|
483
|
+
fullwidth
|
|
484
|
+
/>
|
|
485
|
+
);
|
|
486
|
+
},
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
// ---------------------------------------------------------------------------
|
|
490
|
+
// Keyboard navigation
|
|
491
|
+
// ---------------------------------------------------------------------------
|
|
492
|
+
|
|
493
|
+
export const KeyboardNavigation: StoryObj = {
|
|
494
|
+
name: "Keyboard Navigation (ArrowUp/Down, Enter, Escape)",
|
|
495
|
+
render: () => {
|
|
496
|
+
const [value, setValue] = useState("");
|
|
497
|
+
const [selected, setSelected] = useState<AutoCompleteOption | null>(null);
|
|
498
|
+
const filtered = fruits.filter((f) =>
|
|
499
|
+
f.label.toLowerCase().includes(value.toLowerCase()),
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
return (
|
|
503
|
+
<div className="flex flex-col gap-4 w-full">
|
|
504
|
+
<AutoComplete
|
|
505
|
+
label="Pick a fruit (keyboard)"
|
|
506
|
+
value={value}
|
|
507
|
+
options={filtered}
|
|
508
|
+
onChange={setValue}
|
|
509
|
+
onSearch={setValue}
|
|
510
|
+
onSelect={(opt) => {
|
|
511
|
+
setValue(opt.label);
|
|
512
|
+
setSelected(opt);
|
|
513
|
+
}}
|
|
514
|
+
noOptionsText="No fruit found"
|
|
515
|
+
fullwidth
|
|
516
|
+
/>
|
|
517
|
+
{selected && (
|
|
518
|
+
<Text variant="small2" className="text-text-g-contrast-medium">
|
|
519
|
+
Selected: <strong>{selected.label}</strong>
|
|
520
|
+
</Text>
|
|
521
|
+
)}
|
|
522
|
+
</div>
|
|
523
|
+
);
|
|
524
|
+
},
|
|
525
|
+
};
|