Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit5092645

Browse files
authored
feat: add radix autocomplete component (#21262)
<img width="339" height="330" alt="Screenshot 2025-12-13 at 18 39 30"src="https://github.com/user-attachments/assets/41bade09-1e2e-4ff4-9b27-a3bdc9cb07f2"/>
1 parent55f4efd commit5092645

File tree

11 files changed

+714
-193
lines changed

11 files changed

+714
-193
lines changed
Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
importtype{Meta,StoryObj}from"@storybook/react-vite";
2+
import{Avatar}from"components/Avatar/Avatar";
3+
import{AvatarData}from"components/Avatar/AvatarData";
4+
import{Check}from"lucide-react";
5+
import{useState}from"react";
6+
import{expect,screen,userEvent,waitFor,within}from"storybook/test";
7+
import{Autocomplete}from"./Autocomplete";
8+
9+
constmeta:Meta<typeofAutocomplete>={
10+
title:"components/Autocomplete",
11+
component:Autocomplete,
12+
args:{
13+
placeholder:"Select an option",
14+
},
15+
};
16+
17+
exportdefaultmeta;
18+
19+
typeStory=StoryObj<typeofAutocomplete>;
20+
21+
interfaceSimpleOption{
22+
id:string;
23+
name:string;
24+
}
25+
26+
constsimpleOptions:SimpleOption[]=[
27+
{id:"1",name:"Mango"},
28+
{id:"2",name:"Banana"},
29+
{id:"3",name:"Pineapple"},
30+
{id:"4",name:"Kiwi"},
31+
{id:"5",name:"Coconut"},
32+
];
33+
34+
exportconstDefault:Story={
35+
render:functionDefaultStory(){
36+
const[value,setValue]=useState<SimpleOption|null>(null);
37+
return(
38+
<divclassName="w-80">
39+
<Autocomplete
40+
value={value}
41+
onChange={setValue}
42+
options={simpleOptions}
43+
getOptionValue={(opt)=>opt.id}
44+
getOptionLabel={(opt)=>opt.name}
45+
placeholder="Select a fruit"
46+
/>
47+
</div>
48+
);
49+
},
50+
play:async({ canvasElement})=>{
51+
constcanvas=within(canvasElement);
52+
consttrigger=canvas.getByRole("button");
53+
54+
expect(trigger).toHaveTextContent("Select a fruit");
55+
awaituserEvent.click(trigger);
56+
57+
awaitwaitFor(()=>
58+
expect(screen.getByRole("option",{name:"Mango"})).toBeInTheDocument(),
59+
);
60+
},
61+
};
62+
63+
exportconstWithSelectedValue:Story={
64+
render:functionWithSelectedValueStory(){
65+
const[value,setValue]=useState<SimpleOption|null>(simpleOptions[2]);
66+
return(
67+
<divclassName="w-80">
68+
<Autocomplete
69+
value={value}
70+
onChange={setValue}
71+
options={simpleOptions}
72+
getOptionValue={(opt)=>opt.id}
73+
getOptionLabel={(opt)=>opt.name}
74+
placeholder="Select a fruit"
75+
/>
76+
</div>
77+
);
78+
},
79+
play:async({ canvasElement})=>{
80+
constcanvas=within(canvasElement);
81+
consttrigger=canvas.getByRole("button",{name:/pineapple/i});
82+
expect(trigger).toHaveTextContent("Pineapple");
83+
84+
awaituserEvent.click(trigger);
85+
86+
awaitwaitFor(()=>
87+
expect(
88+
screen.getByRole("option",{name:"Pineapple"}),
89+
).toBeInTheDocument(),
90+
);
91+
92+
awaituserEvent.click(screen.getByRole("option",{name:"Mango"}));
93+
awaitwaitFor(()=>expect(trigger).toHaveTextContent("Mango"));
94+
},
95+
};
96+
97+
exportconstNotClearable:Story={
98+
render:functionNotClearableStory(){
99+
const[value,setValue]=useState<SimpleOption|null>(simpleOptions[0]);
100+
return(
101+
<divclassName="w-80">
102+
<Autocomplete
103+
value={value}
104+
onChange={setValue}
105+
options={simpleOptions}
106+
getOptionValue={(opt)=>opt.id}
107+
getOptionLabel={(opt)=>opt.name}
108+
placeholder="Select a fruit"
109+
clearable={false}
110+
/>
111+
</div>
112+
);
113+
},
114+
};
115+
116+
exportconstLoading:Story={
117+
render:functionLoadingStory(){
118+
const[value,setValue]=useState<SimpleOption|null>(null);
119+
return(
120+
<divclassName="w-80">
121+
<Autocomplete
122+
value={value}
123+
onChange={setValue}
124+
options={[]}
125+
getOptionValue={(opt)=>opt.id}
126+
getOptionLabel={(opt)=>opt.name}
127+
placeholder="Loading options..."
128+
loading
129+
/>
130+
</div>
131+
);
132+
},
133+
play:async({ canvasElement})=>{
134+
constcanvas=within(canvasElement);
135+
awaituserEvent.click(canvas.getByRole("button"));
136+
awaitwaitFor(()=>{
137+
constspinners=screen.getAllByTitle("Loading spinner");
138+
expect(spinners.length).toBeGreaterThanOrEqual(1);
139+
});
140+
},
141+
};
142+
143+
exportconstDisabled:Story={
144+
render:functionDisabledStory(){
145+
const[value,setValue]=useState<SimpleOption|null>(simpleOptions[1]);
146+
return(
147+
<divclassName="w-80">
148+
<Autocomplete
149+
value={value}
150+
onChange={setValue}
151+
options={simpleOptions}
152+
getOptionValue={(opt)=>opt.id}
153+
getOptionLabel={(opt)=>opt.name}
154+
placeholder="Select a fruit"
155+
disabled
156+
/>
157+
</div>
158+
);
159+
},
160+
};
161+
162+
exportconstEmptyOptions:Story={
163+
render:functionEmptyOptionsStory(){
164+
const[value,setValue]=useState<SimpleOption|null>(null);
165+
return(
166+
<divclassName="w-80">
167+
<Autocomplete
168+
value={value}
169+
onChange={setValue}
170+
options={[]}
171+
getOptionValue={(opt)=>opt.id}
172+
getOptionLabel={(opt)=>opt.name}
173+
placeholder="Select a fruit"
174+
noOptionsText="No fruits available"
175+
/>
176+
</div>
177+
);
178+
},
179+
play:async({ canvasElement})=>{
180+
constcanvas=within(canvasElement);
181+
awaituserEvent.click(canvas.getByRole("button"));
182+
awaitwaitFor(()=>
183+
expect(screen.getByText("No fruits available")).toBeInTheDocument(),
184+
);
185+
},
186+
};
187+
188+
exportconstSearchAndFilter:Story={
189+
render:functionSearchAndFilterStory(){
190+
const[value,setValue]=useState<SimpleOption|null>(null);
191+
return(
192+
<divclassName="w-80">
193+
<Autocomplete
194+
value={value}
195+
onChange={setValue}
196+
options={simpleOptions}
197+
getOptionValue={(opt)=>opt.id}
198+
getOptionLabel={(opt)=>opt.name}
199+
placeholder="Select a fruit"
200+
/>
201+
</div>
202+
);
203+
},
204+
play:async({ canvasElement})=>{
205+
constcanvas=within(canvasElement);
206+
awaituserEvent.click(
207+
canvas.getByRole("button",{name:/selectafruit/i}),
208+
);
209+
constsearchInput=screen.getByRole("combobox");
210+
awaituserEvent.type(searchInput,"an");
211+
212+
awaitwaitFor(()=>{
213+
expect(screen.getByRole("option",{name:"Mango"})).toBeInTheDocument();
214+
expect(
215+
screen.getByRole("option",{name:"Banana"}),
216+
).toBeInTheDocument();
217+
expect(
218+
screen.queryByRole("option",{name:"Pineapple"}),
219+
).not.toBeInTheDocument();
220+
});
221+
},
222+
};
223+
224+
exportconstClearSelection:Story={
225+
render:functionClearSelectionStory(){
226+
const[value,setValue]=useState<SimpleOption|null>(simpleOptions[0]);
227+
return(
228+
<divclassName="w-80">
229+
<Autocomplete
230+
value={value}
231+
onChange={setValue}
232+
options={simpleOptions}
233+
getOptionValue={(opt)=>opt.id}
234+
getOptionLabel={(opt)=>opt.name}
235+
placeholder="Select a fruit"
236+
/>
237+
</div>
238+
);
239+
},
240+
play:async({ canvasElement})=>{
241+
constcanvas=within(canvasElement);
242+
consttrigger=canvas.getByRole("button",{name:/mango/i});
243+
expect(trigger).toHaveTextContent("Mango");
244+
245+
constclearButton=canvas.getByRole("button",{name:"Clear selection"});
246+
awaituserEvent.click(clearButton);
247+
248+
awaitwaitFor(()=>
249+
expect(
250+
canvas.getByRole("button",{name:/selectafruit/i}),
251+
).toBeInTheDocument(),
252+
);
253+
},
254+
};
255+
256+
interfaceUser{
257+
id:string;
258+
username:string;
259+
email:string;
260+
avatar_url?:string;
261+
}
262+
263+
constusers:User[]=[
264+
{
265+
id:"1",
266+
username:"alice",
267+
email:"alice@example.com",
268+
avatar_url:"",
269+
},
270+
{
271+
id:"2",
272+
username:"bob",
273+
email:"bob@example.com",
274+
avatar_url:"",
275+
},
276+
{
277+
id:"3",
278+
username:"charlie",
279+
email:"charlie@example.com",
280+
avatar_url:"",
281+
},
282+
];
283+
284+
exportconstWithCustomRenderOption:Story={
285+
render:functionWithCustomRenderOptionStory(){
286+
const[value,setValue]=useState<User|null>(null);
287+
return(
288+
<divclassName="w-[350px]">
289+
<Autocomplete
290+
value={value}
291+
onChange={setValue}
292+
options={users}
293+
getOptionValue={(user)=>user.id}
294+
getOptionLabel={(user)=>user.email}
295+
placeholder="Search for a user"
296+
renderOption={(user,isSelected)=>(
297+
<divclassName="flex items-center justify-between w-full">
298+
<AvatarData
299+
title={user.username}
300+
subtitle={user.email}
301+
src={user.avatar_url}
302+
/>
303+
{isSelected&&<CheckclassName="size-4 shrink-0"/>}
304+
</div>
305+
)}
306+
/>
307+
</div>
308+
);
309+
},
310+
play:async({ canvasElement})=>{
311+
constcanvas=within(canvasElement);
312+
consttrigger=canvas.getByRole("button");
313+
314+
expect(trigger).toHaveTextContent("Search for a user");
315+
awaituserEvent.click(trigger);
316+
},
317+
};
318+
319+
exportconstWithStartAdornment:Story={
320+
render:functionWithStartAdornmentStory(){
321+
const[value,setValue]=useState<User|null>(users[0]);
322+
return(
323+
<divclassName="w-[350px]">
324+
<Autocomplete
325+
value={value}
326+
onChange={setValue}
327+
options={users}
328+
getOptionValue={(user)=>user.id}
329+
getOptionLabel={(user)=>user.email}
330+
placeholder="Search for a user"
331+
startAdornment={
332+
value&&(
333+
<Avatar
334+
size="sm"
335+
src={value.avatar_url}
336+
fallback={value.username}
337+
/>
338+
)
339+
}
340+
renderOption={(user,isSelected)=>(
341+
<divclassName="flex items-center justify-between w-full">
342+
<AvatarData
343+
title={user.username}
344+
subtitle={user.email}
345+
src={user.avatar_url}
346+
/>
347+
{isSelected&&<CheckclassName="size-4 shrink-0"/>}
348+
</div>
349+
)}
350+
/>
351+
</div>
352+
);
353+
},
354+
};

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp