UNPKG

@coder/backstage-plugin-coder

Version:

Create and manage Coder workspaces from Backstage

171 lines (168 loc) 5.74 kB
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