SearchInput: debounce input and add wait indicator

This commit is contained in:
rektdeckard
2020-08-18 14:16:36 -04:00
parent e1c4a72026
commit 07c708e9fd
3 changed files with 30 additions and 10 deletions

View File

@@ -43,3 +43,8 @@
.clear-icon { .clear-icon {
cursor: pointer; cursor: pointer;
} }
.wait-icon {
cursor: wait;
/* opacity: 0.5; */
}

View File

@@ -1,6 +1,7 @@
import React from "react"; import React, { useState } from "react";
import { useRecoilState } from "recoil"; import { useRecoilState } from "recoil";
import { MagnifyingGlass, X } from "phosphor-react"; import { useDebounce } from "react-use";
import { MagnifyingGlass, X, HourglassHigh } from "phosphor-react";
import { searchQueryAtom } from "../../state/atoms"; import { searchQueryAtom } from "../../state/atoms";
import "./SearchInput.css"; import "./SearchInput.css";
@@ -8,11 +9,19 @@ import "./SearchInput.css";
type SearchInputProps = {}; type SearchInputProps = {};
const SearchInput: React.FC<SearchInputProps> = () => { const SearchInput: React.FC<SearchInputProps> = () => {
const [value, setValue] = useState<string>("");
const [query, setQuery] = useRecoilState(searchQueryAtom); const [query, setQuery] = useRecoilState(searchQueryAtom);
void(query);
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => { const [isReady] = useDebounce(() => setQuery(value), 250, [value]);
setQuery(event.target.value);
const handleCancelSearch = () => {
setValue("");
// Should cancel pending debounce timeouts and immediately clear query
// without causing lag!
// setQuery("");
}; };
return ( return (
<div className="search-bar"> <div className="search-bar">
<MagnifyingGlass size={24} /> <MagnifyingGlass size={24} />
@@ -21,11 +30,17 @@ const SearchInput: React.FC<SearchInputProps> = () => {
aria-label="Search for an icon" aria-label="Search for an icon"
type="text" type="text"
autoComplete="off" autoComplete="off"
value={query} value={value}
placeholder="Search for an icon" placeholder="Search for an icon"
onChange={handleSearchChange} onChange={({ currentTarget }) => setValue(currentTarget.value)}
/> />
{query && <X className="clear-icon" size={18} onClick={() => setQuery("")} />} {value ? (
isReady() ? (
<X className="clear-icon" size={18} onClick={handleCancelSearch} />
) : (
<HourglassHigh className="wait-icon" weight="fill" size={18} />
)
) : null}
</div> </div>
); );
}; };

View File

@@ -20,7 +20,7 @@ export const filteredQueryResultsSelector = selector<Readonly<IconEntry[]>>({
if (!query && !style) return icons; if (!query && !style) return icons;
return await new Promise((resolve) => return new Promise((resolve) =>
resolve(icons.filter((icon) => isQueryMatch(icon, query))) resolve(icons.filter((icon) => isQueryMatch(icon, query)))
); );
}, },
@@ -34,7 +34,7 @@ export const categorizedQueryResultsSelector = selector<
key: "categorizedQueryResultsSelector", key: "categorizedQueryResultsSelector",
get: async ({ get }) => { get: async ({ get }) => {
const filteredResults = get(filteredQueryResultsSelector); const filteredResults = get(filteredQueryResultsSelector);
return await new Promise((resolve) => return new Promise((resolve) =>
resolve( resolve(
filteredResults.reduce<CategorizedIcons>((acc, curr) => { filteredResults.reduce<CategorizedIcons>((acc, curr) => {
curr.categories.forEach((category) => { curr.categories.forEach((category) => {
@@ -55,7 +55,7 @@ export const singleCategoryQueryResultsSelector = selectorFamily<
key: "singleCategoryQueryResultsSelector", key: "singleCategoryQueryResultsSelector",
get: (category: IconCategory) => async ({ get }) => { get: (category: IconCategory) => async ({ get }) => {
const filteredResults = get(filteredQueryResultsSelector); const filteredResults = get(filteredQueryResultsSelector);
return await new Promise((resolve) => return new Promise((resolve) =>
resolve( resolve(
filteredResults.filter((icon) => icon.categories.includes(category)) filteredResults.filter((icon) => icon.categories.includes(category))
) )