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

Commit9ee53e5

Browse files
chore(site): refactor filter component to be more extendable (#13688)
1 parent21a923a commit9ee53e5

File tree

20 files changed

+790
-661
lines changed

20 files changed

+790
-661
lines changed

‎site/src/components/Filter/OptionItem.stories.tsx

Lines changed: 0 additions & 39 deletions
This file was deleted.
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import{action}from"@storybook/addon-actions";
2+
importtype{Meta,StoryObj}from"@storybook/react";
3+
import{userEvent,within,expect}from"@storybook/test";
4+
import{useState}from"react";
5+
import{UserAvatar}from"components/UserAvatar/UserAvatar";
6+
import{withDesktopViewport}from"testHelpers/storybook";
7+
import{
8+
SelectFilter,
9+
SelectFilterSearch,
10+
typeSelectFilterOption,
11+
}from"./SelectFilter";
12+
13+
constoptions:SelectFilterOption[]=Array.from({length:50},(_,i)=>({
14+
startIcon:<UserAvatarusername={`username${i+1}`}size="xs"/>,
15+
label:`Option${i+1}`,
16+
value:`option-${i+1}`,
17+
}));
18+
19+
constmeta:Meta<typeofSelectFilter>={
20+
title:"components/SelectFilter",
21+
component:SelectFilter,
22+
args:{
23+
options,
24+
placeholder:"All options",
25+
},
26+
decorators:[withDesktopViewport],
27+
render:functionSelectFilterWithState(args){
28+
const[selectedOption,setSelectedOption]=useState<
29+
SelectFilterOption|undefined
30+
>(args.selectedOption);
31+
return(
32+
<SelectFilter
33+
{...args}
34+
selectedOption={selectedOption}
35+
onSelect={setSelectedOption}
36+
/>
37+
);
38+
},
39+
play:async({ canvasElement})=>{
40+
constcanvas=within(canvasElement);
41+
constbutton=canvas.getByRole("button");
42+
awaituserEvent.click(button);
43+
},
44+
};
45+
46+
exportdefaultmeta;
47+
typeStory=StoryObj<typeofSelectFilter>;
48+
49+
exportconstClosed:Story={
50+
play:()=>{},
51+
};
52+
53+
exportconstOpen:Story={};
54+
55+
exportconstSelected:Story={
56+
args:{
57+
selectedOption:options[25],
58+
},
59+
};
60+
61+
exportconstWithSearch:Story={
62+
args:{
63+
selectedOption:options[25],
64+
selectFilterSearch:(
65+
<SelectFilterSearch
66+
value=""
67+
onChange={action("onSearch")}
68+
placeholder="Search options..."
69+
/>
70+
),
71+
},
72+
};
73+
74+
exportconstLoadingOptions:Story={
75+
args:{
76+
options:undefined,
77+
},
78+
};
79+
80+
exportconstNoOptionsFound:Story={
81+
args:{
82+
options:[],
83+
},
84+
};
85+
86+
exportconstSelectingOption:Story={
87+
play:async({ canvasElement})=>{
88+
constcanvas=within(canvasElement);
89+
constbutton=canvas.getByRole("button");
90+
awaituserEvent.click(button);
91+
constoption=canvas.getByText("Option 25");
92+
awaituserEvent.click(option);
93+
awaitexpect(button).toHaveTextContent("Option 25");
94+
},
95+
};
96+
97+
exportconstUnselectingOption:Story={
98+
args:{
99+
selectedOption:options[25],
100+
},
101+
play:async({ canvasElement})=>{
102+
constcanvas=within(canvasElement);
103+
constbutton=canvas.getByRole("button");
104+
awaituserEvent.click(button);
105+
constmenu=canvasElement.querySelector<HTMLElement>("[role=menu]")!;
106+
constoption=within(menu).getByText("Option 26");
107+
awaituserEvent.click(option);
108+
awaitexpect(button).toHaveTextContent("All options");
109+
},
110+
};
111+
112+
exportconstSearchingOption:Story={
113+
render:functionSelectFilterWithSearch(args){
114+
const[selectedOption,setSelectedOption]=useState<
115+
SelectFilterOption|undefined
116+
>(args.selectedOption);
117+
const[search,setSearch]=useState("");
118+
constvisibleOptions=options.filter((option)=>
119+
option.value.includes(search),
120+
);
121+
122+
return(
123+
<SelectFilter
124+
{...args}
125+
selectedOption={selectedOption}
126+
onSelect={setSelectedOption}
127+
options={visibleOptions}
128+
selectFilterSearch={
129+
<SelectFilterSearch
130+
value={search}
131+
onChange={setSearch}
132+
placeholder="Search options..."
133+
inputProps={{"aria-label":"Search options"}}
134+
/>
135+
}
136+
/>
137+
);
138+
},
139+
play:async({ canvasElement})=>{
140+
constcanvas=within(canvasElement);
141+
constbutton=canvas.getByRole("button");
142+
awaituserEvent.click(button);
143+
constsearch=canvas.getByLabelText("Search options");
144+
awaituserEvent.type(search,"option-2");
145+
},
146+
};
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import{useState,typeFC,typeReactNode}from"react";
2+
import{Loader}from"components/Loader/Loader";
3+
import{
4+
SelectMenu,
5+
SelectMenuTrigger,
6+
SelectMenuButton,
7+
SelectMenuContent,
8+
SelectMenuSearch,
9+
SelectMenuList,
10+
SelectMenuItem,
11+
SelectMenuIcon,
12+
}from"components/SelectMenu/SelectMenu";
13+
14+
constBASE_WIDTH=200;
15+
constPOPOVER_WIDTH=320;
16+
17+
exporttypeSelectFilterOption={
18+
startIcon?:ReactNode;
19+
label:string;
20+
value:string;
21+
};
22+
23+
exporttypeSelectFilterProps={
24+
options:SelectFilterOption[]|undefined;
25+
selectedOption?:SelectFilterOption;
26+
// Used to add a accessibility label to the select
27+
label:string;
28+
// Used when there is no option selected
29+
placeholder:string;
30+
// Used to customize the empty state message
31+
emptyText?:string;
32+
onSelect:(option:SelectFilterOption|undefined)=>void;
33+
// SelectFilterSearch element
34+
selectFilterSearch?:ReactNode;
35+
};
36+
37+
exportconstSelectFilter:FC<SelectFilterProps>=({
38+
label,
39+
options,
40+
selectedOption,
41+
onSelect,
42+
placeholder,
43+
emptyText,
44+
selectFilterSearch,
45+
})=>{
46+
const[open,setOpen]=useState(false);
47+
48+
return(
49+
<SelectMenuopen={open}onOpenChange={setOpen}>
50+
<SelectMenuTrigger>
51+
<SelectMenuButton
52+
startIcon={selectedOption?.startIcon}
53+
css={{width:BASE_WIDTH}}
54+
aria-label={label}
55+
>
56+
{selectedOption?.label??placeholder}
57+
</SelectMenuButton>
58+
</SelectMenuTrigger>
59+
<SelectMenuContent
60+
horizontal="right"
61+
css={{
62+
"& .MuiPaper-root":{
63+
// When including selectFilterSearch, we aim for the width to be as
64+
// wide as possible.
65+
width:selectFilterSearch ?"100%" :undefined,
66+
maxWidth:POPOVER_WIDTH,
67+
minWidth:BASE_WIDTH,
68+
},
69+
}}
70+
>
71+
{selectFilterSearch}
72+
{options ?(
73+
options.length>0 ?(
74+
<SelectMenuList>
75+
{options.map((o)=>{
76+
constisSelected=o.value===selectedOption?.value;
77+
return(
78+
<SelectMenuItem
79+
key={o.value}
80+
selected={isSelected}
81+
onClick={()=>{
82+
setOpen(false);
83+
onSelect(isSelected ?undefined :o);
84+
}}
85+
>
86+
{o.startIcon&&(
87+
<SelectMenuIcon>{o.startIcon}</SelectMenuIcon>
88+
)}
89+
{o.label}
90+
</SelectMenuItem>
91+
);
92+
})}
93+
</SelectMenuList>
94+
) :(
95+
<div
96+
css={(theme)=>({
97+
display:"flex",
98+
alignItems:"center",
99+
justifyContent:"center",
100+
padding:32,
101+
color:theme.palette.text.secondary,
102+
lineHeight:1,
103+
})}
104+
>
105+
{emptyText||"No options found"}
106+
</div>
107+
)
108+
) :(
109+
<Loadersize={16}/>
110+
)}
111+
</SelectMenuContent>
112+
</SelectMenu>
113+
);
114+
};
115+
116+
exportconstSelectFilterSearch=SelectMenuSearch;

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp