diff --git a/site/src/components/Filter/filter.tsx b/site/src/components/Filter/filter.tsx index 8335408c11733..91d8d78ee1cf4 100644 --- a/site/src/components/Filter/filter.tsx +++ b/site/src/components/Filter/filter.tsx @@ -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 = ({ }, [filter.query]); const shouldDisplayError = hasError(error) && isApiValidationError(error); - const hasFilterQuery = filter.query !== ""; return (
= ({ learnMoreLabel2={learnMoreLabel2} learnMoreLink2={learnMoreLink2} /> - = ({ ? 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 = ({ "&:hover": { zIndex: 2, }, - "& input::placeholder": { - color: theme.palette.text.secondary, - }, - "& .MuiInputAdornment-root": { - marginLeft: 0, - }, "&.Mui-error": { zIndex: 3, }, }, - startAdornment: ( - - - - ), - endAdornment: hasFilterQuery && ( - - - { - filter.update(""); - }} - > - - - - - ), }} />
diff --git a/site/src/components/SearchField/SearchField.stories.tsx b/site/src/components/SearchField/SearchField.stories.tsx new file mode 100644 index 0000000000000..254de5fe637eb --- /dev/null +++ b/site/src/components/SearchField/SearchField.stories.tsx @@ -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 = { + title: "components/SearchField", + component: SearchField, + args: { + placeholder: "Search...", + }, + render: function StatefulWrapper(args) { + const [value, setValue] = useState(args.value); + return ; + }, +}; + +export default meta; +type Story = StoryObj; + +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" })); + }, +}; diff --git a/site/src/components/SearchField/SearchField.tsx b/site/src/components/SearchField/SearchField.tsx new file mode 100644 index 0000000000000..9e81b74e972ac --- /dev/null +++ b/site/src/components/SearchField/SearchField.tsx @@ -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 & { + onChange: (query: string) => void; +}; + +export const SearchField: FC = ({ + value = "", + onChange, + InputProps, + ...textFieldProps +}) => { + const theme = useTheme(); + return ( + onChange(e.target.value)} + InputProps={{ + startAdornment: ( + + + + ), + endAdornment: value !== "" && ( + + + { + onChange(""); + }} + > + + Clear field + + + + ), + ...InputProps, + }} + {...textFieldProps} + /> + ); +};