Files
phosphor-icons/src/components/SearchInput/SearchInput.tsx
2024-01-04 22:08:20 -07:00

113 lines
3.0 KiB
TypeScript

import {
useState,
useEffect,
useRef,
MutableRefObject,
ReactNode,
} from "react";
import { useRecoilState } from "recoil";
import { useHotkeys } from "react-hotkeys-hook";
import {
Command,
MagnifyingGlass,
X,
HourglassHigh,
} from "@phosphor-icons/react";
import ReactGA from "react-ga4";
import { useDebounce } from "@/hooks";
import { searchQueryAtom } from "@/state";
import "./SearchInput.css";
const apple = /iPhone|iPod|iPad|Macintosh|MacIntel|MacPPC/i;
const isApple = apple.test(window.navigator.platform);
const mobile = /Android|iPhone|iPod|iPad|Opera Mini|IEMobile/i;
const isMobile = mobile.test(window.navigator.userAgent);
type SearchInputProps = {};
const SearchInput = (_: SearchInputProps) => {
const [value, setValue] = useState<string>("");
const [query, setQuery] = useRecoilState(searchQueryAtom);
const inputRef =
useRef<HTMLInputElement>() as MutableRefObject<HTMLInputElement>;
useHotkeys("ctrl+k,meta+k", (e) => {
e.preventDefault();
if (!e.repeat) {
inputRef.current?.focus();
inputRef.current.select();
}
});
/* eslint-disable react-hooks/exhaustive-deps */
useEffect(() => {
let isMounted = true;
if (value !== query) {
isMounted && setValue(query);
ReactGA.event({ category: "Search", action: "Tag", label: query });
}
return () => void (isMounted = false);
}, [query]);
/* eslint-enable react-hooks/exhaustive-deps */
const [_isReady, _cancel] = useDebounce(
() => {
if (value !== query) {
setQuery(value);
!!value &&
ReactGA.event({ category: "Search", action: "Query", label: value });
}
!!value &&
void document
.getElementById("beacon")
?.scrollIntoView({ block: "start", behavior: "smooth" });
},
500,
[value]
);
const handleCancelSearch = () => {
setValue("");
// Should cancel pending debounce timeouts and immediately clear query
// without causing lag!
// setQuery("");
};
return (
<div className="search-bar">
<MagnifyingGlass id="search-icon" size={24} />
<input
ref={inputRef}
id="search-input"
title="Search for icon names, categories, or keywords"
aria-label="Search for an icon"
type="text"
autoCapitalize="off"
autoComplete="off"
value={value}
placeholder="Search"
onChange={({ currentTarget }) => setValue(currentTarget.value)}
onKeyDown={({ currentTarget, key }) =>
(key === "Enter" || key === "Escape") && currentTarget.blur()
}
/>
{!value && !isMobile && <Keys>{isApple ? <Command /> : "Ctrl + "}K</Keys>}
{value ? (
value === query ? (
<X className="clear-icon" size={18} onClick={handleCancelSearch} />
) : (
<HourglassHigh className="wait-icon" weight="fill" size={18} />
)
) : null}
</div>
);
};
const Keys = ({ children }: { children?: ReactNode }) => (
<div className="keys">{children}</div>
);
export default SearchInput;