mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor(site): refactor filter search field (#13545)
This commit is contained in:
@@ -1,19 +1,13 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import CheckOutlined from "@mui/icons-material/CheckOutlined";
|
||||
import CloseOutlined from "@mui/icons-material/CloseOutlined";
|
||||
import KeyboardArrowDown from "@mui/icons-material/KeyboardArrowDown";
|
||||
import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined";
|
||||
import SearchOutlined from "@mui/icons-material/SearchOutlined";
|
||||
import Button, { type ButtonProps } from "@mui/material/Button";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import InputAdornment from "@mui/material/InputAdornment";
|
||||
import Menu, { type MenuProps } from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import MenuList from "@mui/material/MenuList";
|
||||
import Skeleton, { type SkeletonProps } from "@mui/material/Skeleton";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import {
|
||||
type FC,
|
||||
type ReactNode,
|
||||
@@ -35,6 +29,7 @@ import {
|
||||
SearchInput,
|
||||
searchStyles,
|
||||
} from "components/Search/Search";
|
||||
import { SearchField } from "components/SearchField/SearchField";
|
||||
import { useDebouncedFunction } from "hooks/debounce";
|
||||
import type { useFilterMenu } from "./menu";
|
||||
import type { BaseOption } from "./options";
|
||||
@@ -199,7 +194,6 @@ export const Filter: FC<FilterProps> = ({
|
||||
}, [filter.query]);
|
||||
|
||||
const shouldDisplayError = hasError(error) && isApiValidationError(error);
|
||||
const hasFilterQuery = filter.query !== "";
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -226,7 +220,7 @@ export const Filter: FC<FilterProps> = ({
|
||||
learnMoreLabel2={learnMoreLabel2}
|
||||
learnMoreLink2={learnMoreLink2}
|
||||
/>
|
||||
<TextField
|
||||
<SearchField
|
||||
fullWidth
|
||||
error={shouldDisplayError}
|
||||
helperText={
|
||||
@@ -234,17 +228,15 @@ export const Filter: FC<FilterProps> = ({
|
||||
? getValidationErrorMessage(error)
|
||||
: undefined
|
||||
}
|
||||
size="small"
|
||||
placeholder="Search..."
|
||||
value={queryCopy}
|
||||
onChange={(query) => {
|
||||
setQueryCopy(query);
|
||||
filter.debounceUpdate(query);
|
||||
}}
|
||||
InputProps={{
|
||||
"aria-label": "Filter",
|
||||
name: "query",
|
||||
placeholder: "Search...",
|
||||
value: queryCopy,
|
||||
ref: textboxInputRef,
|
||||
onChange: (e) => {
|
||||
setQueryCopy(e.target.value);
|
||||
filter.debounceUpdate(e.target.value);
|
||||
},
|
||||
"aria-label": "Filter",
|
||||
onBlur: () => {
|
||||
if (queryCopy !== filter.query) {
|
||||
setQueryCopy(filter.query);
|
||||
@@ -258,40 +250,10 @@ export const Filter: FC<FilterProps> = ({
|
||||
"&:hover": {
|
||||
zIndex: 2,
|
||||
},
|
||||
"& input::placeholder": {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
"& .MuiInputAdornment-root": {
|
||||
marginLeft: 0,
|
||||
},
|
||||
"&.Mui-error": {
|
||||
zIndex: 3,
|
||||
},
|
||||
},
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchOutlined
|
||||
css={{
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
/>
|
||||
</InputAdornment>
|
||||
),
|
||||
endAdornment: hasFilterQuery && (
|
||||
<InputAdornment position="end">
|
||||
<Tooltip title="Clear filter">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
filter.update("");
|
||||
}}
|
||||
>
|
||||
<CloseOutlined css={{ fontSize: 14 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { userEvent, within } from "@storybook/test";
|
||||
import { useState } from "react";
|
||||
import { SearchField } from "./SearchField";
|
||||
|
||||
const meta: Meta<typeof SearchField> = {
|
||||
title: "components/SearchField",
|
||||
component: SearchField,
|
||||
args: {
|
||||
placeholder: "Search...",
|
||||
},
|
||||
render: function StatefulWrapper(args) {
|
||||
const [value, setValue] = useState(args.value);
|
||||
return <SearchField {...args} value={value} onChange={setValue} />;
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof SearchField>;
|
||||
|
||||
export const Empty: Story = {};
|
||||
|
||||
export const DefaultValue: Story = {
|
||||
args: {
|
||||
value: "owner:me",
|
||||
},
|
||||
};
|
||||
|
||||
export const TypeValue: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const input = canvas.getByRole("textbox");
|
||||
await userEvent.type(input, "owner:me");
|
||||
},
|
||||
};
|
||||
|
||||
export const ClearValue: Story = {
|
||||
args: {
|
||||
value: "owner:me",
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(canvas.getByRole("button", { name: "Clear field" }));
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import CloseIcon from "@mui/icons-material/CloseOutlined";
|
||||
import SearchIcon from "@mui/icons-material/SearchOutlined";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import InputAdornment from "@mui/material/InputAdornment";
|
||||
import TextField, { type TextFieldProps } from "@mui/material/TextField";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import visuallyHidden from "@mui/utils/visuallyHidden";
|
||||
import type { FC } from "react";
|
||||
|
||||
export type SearchFieldProps = Omit<TextFieldProps, "onChange"> & {
|
||||
onChange: (query: string) => void;
|
||||
};
|
||||
|
||||
export const SearchField: FC<SearchFieldProps> = ({
|
||||
value = "",
|
||||
onChange,
|
||||
InputProps,
|
||||
...textFieldProps
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<TextField
|
||||
size="small"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon
|
||||
css={{
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
/>
|
||||
</InputAdornment>
|
||||
),
|
||||
endAdornment: value !== "" && (
|
||||
<InputAdornment position="end">
|
||||
<Tooltip title="Clear field">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
onChange("");
|
||||
}}
|
||||
>
|
||||
<CloseIcon css={{ fontSize: 14 }} />
|
||||
<span css={{ ...visuallyHidden }}>Clear field</span>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</InputAdornment>
|
||||
),
|
||||
...InputProps,
|
||||
}}
|
||||
{...textFieldProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user