@coder/backstage-plugin-coder
Version:
Create and manage Coder workspaces from Backstage
171 lines (168 loc) • 5.74 kB
JavaScript
import { jsxs, jsx } from 'react/jsx-runtime';
import { useState, useRef, useEffect } from 'react';
import { useId } from '../../hooks/hookPolyfills.esm.js';
import { VisuallyHidden } from '../VisuallyHidden/VisuallyHidden.esm.js';
import { makeStyles } from '@material-ui/core';
import { useWorkspacesCardContext } from './Root.esm.js';
import SearchIcon from '@material-ui/icons/Search';
import CloseIcon from '@material-ui/icons/Close';
const LABEL_TEXT = "Search your Coder workspaces";
const SEARCH_DEBOUNCE_MS = 400;
const useStyles = makeStyles((theme) => ({
root: {
padding: 0,
margin: 0,
border: "none",
display: "flex",
flexFlow: "row nowrap",
alignItems: "center",
borderRadius: theme.shape.borderRadius,
boxShadow: "none",
// There's a weird styling issue where Spotify's default background colors
// don't have the same amount of contrast across their built-in light and
// dark themes. It's just right for the input in dark mode, but too faint in
// light mode. Have to make it darker to make sure input is more obvious
backgroundColor: () => {
const defaultBackgroundColor = theme.palette.background.default;
const isDefaultSpotifyLightTheme = defaultBackgroundColor.toUpperCase() === "#F8F8F8";
return isDefaultSpotifyLightTheme ? "hsl(0deg,0%,93%)" : defaultBackgroundColor;
},
"&:focus-within": {
boxShadow: "0 0 0 1px hsl(213deg, 94%, 68%)"
},
// Makes it so that the container doesn't have visible focus while you're
// focusing on the clear button
"&:has(button:focus)": {
boxShadow: "none"
}
},
labelWrapper: {
flexGrow: 1,
display: "flex",
flexFlow: "row nowrap",
alignItems: "center",
gap: theme.spacing(1.5),
padding: `${theme.spacing(1.5)}px ${theme.spacing(2)}px`
},
searchInput: {
color: "inherit",
display: "block",
height: "100%",
width: "100%",
backgroundColor: "inherit",
border: "none",
fontSize: theme.typography.body1.fontSize,
outline: "none"
},
clearButton: ({ isInputEmpty }) => ({
padding: `${theme.spacing(1.5)}px ${theme.spacing(2)}px`,
margin: 0,
lineHeight: 1,
backgroundColor: "inherit",
border: "none",
borderRadius: theme.shape.borderRadius,
color: theme.palette.text.primary,
opacity: isInputEmpty ? "40%" : "100%",
outline: "none",
cursor: "pointer",
"&:focus": {
boxShadow: "0 0 0 1px hsl(213deg, 94%, 68%)"
}
})
}));
const SearchBox = ({
className,
labelWrapperClassName,
searchInputClassName,
clearButtonClassName,
searchInputRef,
clearButtonRef,
...delegatedProps
}) => {
const hookId = useId();
const { queryFilter, onFilterChange } = useWorkspacesCardContext();
const [localInput, setLocalInput] = useState(queryFilter);
const isInputEmpty = localInput === "";
const styles = useStyles({ isInputEmpty });
const searchDebounceIdRef = useRef();
useEffect(() => {
const clearDebounceOnUnmount = () => {
window.clearTimeout(searchDebounceIdRef.current);
};
return clearDebounceOnUnmount;
}, []);
const onSearchClear = () => {
setLocalInput("");
onFilterChange("");
window.clearTimeout(searchDebounceIdRef.current);
};
const onChange = (event) => {
const newSearchText = event.currentTarget.value;
setLocalInput(newSearchText);
const textClearedViaInput = !isInputEmpty && newSearchText === "";
if (textClearedViaInput) {
onSearchClear();
return;
}
window.clearTimeout(searchDebounceIdRef.current);
searchDebounceIdRef.current = window.setTimeout(() => {
onFilterChange(newSearchText);
}, SEARCH_DEBOUNCE_MS);
};
const legendId = `${hookId}-legend`;
return (
// Have to use aria-labelledby even though <legend>s normally provide
// accessible names automatically - the hidden prop on the legend blocks the
// default behavior
/* @__PURE__ */ jsxs(
"fieldset",
{
"aria-labelledby": legendId,
className: `${styles.root} ${className ?? ""}`,
...delegatedProps,
children: [
/* @__PURE__ */ jsx("legend", { hidden: true, id: legendId, children: "Search controls" }),
/* @__PURE__ */ jsxs(
"label",
{
className: `${styles.labelWrapper} ${labelWrapperClassName ?? ""}`,
children: [
/* @__PURE__ */ jsx(SearchIcon, { "aria-hidden": true, fontSize: "small", htmlColor: "#7b7b7b" }),
/* @__PURE__ */ jsx(VisuallyHidden, { children: LABEL_TEXT }),
/* @__PURE__ */ jsx(
"input",
{
ref: searchInputRef,
type: "text",
role: "searchbox",
spellCheck: true,
placeholder: LABEL_TEXT,
value: localInput,
onChange,
className: `${styles.searchInput} ${searchInputClassName ?? ""}`
}
)
]
}
),
/* @__PURE__ */ jsxs(
"button",
{
type: "button",
ref: clearButtonRef,
onClick: onSearchClear,
disabled: isInputEmpty,
className: `${styles.clearButton} ${clearButtonClassName ?? ""}`,
children: [
/* @__PURE__ */ jsx(CloseIcon, { fontSize: "small" }),
/* @__PURE__ */ jsx(VisuallyHidden, { children: "Clear out search" })
]
}
)
]
}
)
);
};
export { SearchBox };
//# sourceMappingURL=SearchBox.esm.js.map