Skip to content

Commit 0c627a4

Browse files
refactor(site): refactor filter search field (#13545)
1 parent a11f8b0 commit 0c627a4

File tree

3 files changed

+112
-47
lines changed

3 files changed

+112
-47
lines changed

site/src/components/Filter/filter.tsx

+9-47
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
11
import { useTheme } from "@emotion/react";
22
import CheckOutlined from "@mui/icons-material/CheckOutlined";
3-
import CloseOutlined from "@mui/icons-material/CloseOutlined";
43
import KeyboardArrowDown from "@mui/icons-material/KeyboardArrowDown";
54
import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined";
6-
import SearchOutlined from "@mui/icons-material/SearchOutlined";
75
import Button, { type ButtonProps } from "@mui/material/Button";
86
import Divider from "@mui/material/Divider";
9-
import IconButton from "@mui/material/IconButton";
10-
import InputAdornment from "@mui/material/InputAdornment";
117
import Menu, { type MenuProps } from "@mui/material/Menu";
128
import MenuItem from "@mui/material/MenuItem";
139
import MenuList from "@mui/material/MenuList";
1410
import Skeleton, { type SkeletonProps } from "@mui/material/Skeleton";
15-
import TextField from "@mui/material/TextField";
16-
import Tooltip from "@mui/material/Tooltip";
1711
import {
1812
type FC,
1913
type ReactNode,
@@ -35,6 +29,7 @@ import {
3529
SearchInput,
3630
searchStyles,
3731
} from "components/Search/Search";
32+
import { SearchField } from "components/SearchField/SearchField";
3833
import { useDebouncedFunction } from "hooks/debounce";
3934
import type { useFilterMenu } from "./menu";
4035
import type { BaseOption } from "./options";
@@ -199,7 +194,6 @@ export const Filter: FC<FilterProps> = ({
199194
}, [filter.query]);
200195

201196
const shouldDisplayError = hasError(error) && isApiValidationError(error);
202-
const hasFilterQuery = filter.query !== "";
203197

204198
return (
205199
<div
@@ -226,25 +220,23 @@ export const Filter: FC<FilterProps> = ({
226220
learnMoreLabel2={learnMoreLabel2}
227221
learnMoreLink2={learnMoreLink2}
228222
/>
229-
<TextField
223+
<SearchField
230224
fullWidth
231225
error={shouldDisplayError}
232226
helperText={
233227
shouldDisplayError
234228
? getValidationErrorMessage(error)
235229
: undefined
236230
}
237-
size="small"
231+
placeholder="Search..."
232+
value={queryCopy}
233+
onChange={(query) => {
234+
setQueryCopy(query);
235+
filter.debounceUpdate(query);
236+
}}
238237
InputProps={{
239-
"aria-label": "Filter",
240-
name: "query",
241-
placeholder: "Search...",
242-
value: queryCopy,
243238
ref: textboxInputRef,
244-
onChange: (e) => {
245-
setQueryCopy(e.target.value);
246-
filter.debounceUpdate(e.target.value);
247-
},
239+
"aria-label": "Filter",
248240
onBlur: () => {
249241
if (queryCopy !== filter.query) {
250242
setQueryCopy(filter.query);
@@ -258,40 +250,10 @@ export const Filter: FC<FilterProps> = ({
258250
"&:hover": {
259251
zIndex: 2,
260252
},
261-
"& input::placeholder": {
262-
color: theme.palette.text.secondary,
263-
},
264-
"& .MuiInputAdornment-root": {
265-
marginLeft: 0,
266-
},
267253
"&.Mui-error": {
268254
zIndex: 3,
269255
},
270256
},
271-
startAdornment: (
272-
<InputAdornment position="start">
273-
<SearchOutlined
274-
css={{
275-
fontSize: 14,
276-
color: theme.palette.text.secondary,
277-
}}
278-
/>
279-
</InputAdornment>
280-
),
281-
endAdornment: hasFilterQuery && (
282-
<InputAdornment position="end">
283-
<Tooltip title="Clear filter">
284-
<IconButton
285-
size="small"
286-
onClick={() => {
287-
filter.update("");
288-
}}
289-
>
290-
<CloseOutlined css={{ fontSize: 14 }} />
291-
</IconButton>
292-
</Tooltip>
293-
</InputAdornment>
294-
),
295257
}}
296258
/>
297259
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { userEvent, within } from "@storybook/test";
3+
import { useState } from "react";
4+
import { SearchField } from "./SearchField";
5+
6+
const meta: Meta<typeof SearchField> = {
7+
title: "components/SearchField",
8+
component: SearchField,
9+
args: {
10+
placeholder: "Search...",
11+
},
12+
render: function StatefulWrapper(args) {
13+
const [value, setValue] = useState(args.value);
14+
return <SearchField {...args} value={value} onChange={setValue} />;
15+
},
16+
};
17+
18+
export default meta;
19+
type Story = StoryObj<typeof SearchField>;
20+
21+
export const Empty: Story = {};
22+
23+
export const DefaultValue: Story = {
24+
args: {
25+
value: "owner:me",
26+
},
27+
};
28+
29+
export const TypeValue: Story = {
30+
play: async ({ canvasElement }) => {
31+
const canvas = within(canvasElement);
32+
const input = canvas.getByRole("textbox");
33+
await userEvent.type(input, "owner:me");
34+
},
35+
};
36+
37+
export const ClearValue: Story = {
38+
args: {
39+
value: "owner:me",
40+
},
41+
play: async ({ canvasElement }) => {
42+
const canvas = within(canvasElement);
43+
await userEvent.click(canvas.getByRole("button", { name: "Clear field" }));
44+
},
45+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { useTheme } from "@emotion/react";
2+
import CloseIcon from "@mui/icons-material/CloseOutlined";
3+
import SearchIcon from "@mui/icons-material/SearchOutlined";
4+
import IconButton from "@mui/material/IconButton";
5+
import InputAdornment from "@mui/material/InputAdornment";
6+
import TextField, { type TextFieldProps } from "@mui/material/TextField";
7+
import Tooltip from "@mui/material/Tooltip";
8+
import visuallyHidden from "@mui/utils/visuallyHidden";
9+
import type { FC } from "react";
10+
11+
export type SearchFieldProps = Omit<TextFieldProps, "onChange"> & {
12+
onChange: (query: string) => void;
13+
};
14+
15+
export const SearchField: FC<SearchFieldProps> = ({
16+
value = "",
17+
onChange,
18+
InputProps,
19+
...textFieldProps
20+
}) => {
21+
const theme = useTheme();
22+
return (
23+
<TextField
24+
size="small"
25+
value={value}
26+
onChange={(e) => onChange(e.target.value)}
27+
InputProps={{
28+
startAdornment: (
29+
<InputAdornment position="start">
30+
<SearchIcon
31+
css={{
32+
fontSize: 14,
33+
color: theme.palette.text.secondary,
34+
}}
35+
/>
36+
</InputAdornment>
37+
),
38+
endAdornment: value !== "" && (
39+
<InputAdornment position="end">
40+
<Tooltip title="Clear field">
41+
<IconButton
42+
size="small"
43+
onClick={() => {
44+
onChange("");
45+
}}
46+
>
47+
<CloseIcon css={{ fontSize: 14 }} />
48+
<span css={{ ...visuallyHidden }}>Clear field</span>
49+
</IconButton>
50+
</Tooltip>
51+
</InputAdornment>
52+
),
53+
...InputProps,
54+
}}
55+
{...textFieldProps}
56+
/>
57+
);
58+
};

0 commit comments

Comments
 (0)