Use downshift-js for better search bar experience

This commit is contained in:
Tyler Hallada 2022-02-06 20:45:18 -05:00
parent 86a7516c3b
commit 6005028439
4 changed files with 106 additions and 88 deletions

View File

@ -1,3 +1,4 @@
import { useCombobox } from "downshift";
import React, { useEffect, useState, useRef } from "react"; import React, { useEffect, useState, useRef } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import type mapboxgl from "mapbox-gl"; import type mapboxgl from "mapbox-gl";
@ -5,8 +6,6 @@ import MiniSearch, { SearchResult } from "minisearch";
import useSWRImmutable from "swr/immutable"; import useSWRImmutable from "swr/immutable";
import styles from "../styles/SearchBar.module.css"; import styles from "../styles/SearchBar.module.css";
import { join } from "path/posix";
import { countReset } from "console";
type Props = { type Props = {
clearSelectedCell: () => void; clearSelectedCell: () => void;
@ -59,72 +58,63 @@ const SearchBar: React.FC<Props> = ({ clearSelectedCell, counts, map }) => {
}, [data]); }, [data]);
const searchInput = useRef<HTMLInputElement | null>(null); const searchInput = useRef<HTMLInputElement | null>(null);
const [search, setSearch] = useState<string>("");
const [searchFocused, setSearchFocused] = useState<boolean>(false); const [searchFocused, setSearchFocused] = useState<boolean>(false);
const [clickingResult, setClickingResult] = useState<boolean>(false);
const [results, setResults] = useState<SearchResult[]>([]); const [results, setResults] = useState<SearchResult[]>([]);
useEffect(() => { const {
if (searchInput.current) { isOpen,
if ( getMenuProps,
searchFocused && getInputProps,
global.document.activeElement !== searchInput.current getComboboxProps,
) { highlightedIndex,
searchInput.current.focus(); getItemProps,
} else if ( reset,
!searchFocused && } = useCombobox({
global.document.activeElement === searchInput.current items: results,
) { itemToString: (item) => (item ? item.name : ""),
searchInput.current.blur(); onInputValueChange: ({ inputValue }) => {
} if (searchEngine.current && inputValue) {
} const results: SearchResult[] = searchEngine.current.search(inputValue);
}, [searchFocused]);
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
if (searchEngine.current) {
const results: SearchResult[] = searchEngine.current.search(
e.target.value
);
setResults(results); setResults(results);
} }
}; },
onSelectedItemChange: ({ selectedItem }) => {
const onChooseResult = if (selectedItem) {
(item: Mod) =>
(e: React.MouseEvent<HTMLElement> | React.TouchEvent<HTMLElement>) => {
router.push({ query: { mod: item.id } });
setSearch("");
setResults([]);
setClickingResult(false);
setSearchFocused(false); setSearchFocused(false);
}; router.push({ query: { mod: selectedItem.id } });
if (searchInput.current) searchInput.current.blur();
reset();
}
},
});
return ( return (
<>
<div <div
className={`${styles["search-bar"]} ${ className={`${styles["search-bar"]} ${
searchFocused ? styles["search-bar-focused"] : "" searchFocused ? styles["search-bar-focused"] : ""
}`} }`}
{...getComboboxProps()}
> >
<input <input
type="text" {...getInputProps({
placeholder="Search mods or cells…" type: "text",
onChange={onChange} placeholder: "Search mods or cells…",
onFocus={() => setSearchFocused(true)} onFocus: () => setSearchFocused(true),
onBlur={() => { onBlur: () => {
if (!clickingResult) { if (!isOpen) setSearchFocused(false);
setSearch(""); },
setResults([]); disabled: !data,
setSearchFocused(false); ref: searchInput,
} })}
}}
value={search}
ref={searchInput}
disabled={!data}
/> />
{results.length > 0 && ( <ul
<ul className={styles["search-results"]}> className={styles["search-results"]}
{results style={!isOpen ? { display: "none" } : {}}
{...getMenuProps()}
>
{isOpen &&
results
.sort((resultA, resultB) => { .sort((resultA, resultB) => {
if (counts) { if (counts) {
const countA = counts[resultA.id]; const countA = counts[resultA.id];
@ -133,19 +123,22 @@ const SearchBar: React.FC<Props> = ({ clearSelectedCell, counts, map }) => {
} }
return 0; return 0;
}) })
.map((result) => ( .map((result, index) => (
<li <li
key={result.id} key={result.id}
onClick={onChooseResult({ id: result.id, name: result.name })} {...getItemProps({ item: result, index })}
onTouchStart={() => setClickingResult(true)} className={`${
onMouseDown={() => setClickingResult(true)} highlightedIndex === index
? styles["highlighted-result"]
: ""
}`}
> >
{result.name} {result.name}
</li> </li>
))} ))}
</ul> </ul>
)}
</div> </div>
</>
); );
}; };

32
package-lock.json generated
View File

@ -25,7 +25,6 @@
"version": "7.16.7", "version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz",
"integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==", "integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==",
"dev": true,
"requires": { "requires": {
"regenerator-runtime": "^0.13.4" "regenerator-runtime": "^0.13.4"
} }
@ -555,6 +554,11 @@
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
"integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==" "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="
}, },
"compute-scroll-into-view": {
"version": "1.0.17",
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz",
"integrity": "sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg=="
},
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -657,6 +661,25 @@
"esutils": "^2.0.2" "esutils": "^2.0.2"
} }
}, },
"downshift": {
"version": "6.1.7",
"resolved": "https://registry.npmjs.org/downshift/-/downshift-6.1.7.tgz",
"integrity": "sha512-cVprZg/9Lvj/uhYRxELzlu1aezRcgPWBjTvspiGTVEU64gF5pRdSRKFVLcxqsZC637cLAGMbL40JavEfWnqgNg==",
"requires": {
"@babel/runtime": "^7.14.8",
"compute-scroll-into-view": "^1.0.17",
"prop-types": "^15.7.2",
"react-is": "^17.0.2",
"tslib": "^2.3.0"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}
}
},
"earcut": { "earcut": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.3.tgz", "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.3.tgz",
@ -1976,7 +1999,6 @@
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"requires": { "requires": {
"loose-envify": "^1.4.0", "loose-envify": "^1.4.0",
"object-assign": "^4.1.1", "object-assign": "^4.1.1",
@ -1986,8 +2008,7 @@
"react-is": { "react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
"dev": true
} }
} }
}, },
@ -2055,8 +2076,7 @@
"regenerator-runtime": { "regenerator-runtime": {
"version": "0.13.9", "version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
"dev": true
}, },
"regexp.prototype.flags": { "regexp.prototype.flags": {
"version": "1.3.2", "version": "1.3.2",

View File

@ -11,6 +11,7 @@
"@types/javascript-color-gradient": "^1.3.0", "@types/javascript-color-gradient": "^1.3.0",
"@types/mapbox-gl": "^2.6.0", "@types/mapbox-gl": "^2.6.0",
"date-fns": "^2.28.0", "date-fns": "^2.28.0",
"downshift": "^6.1.7",
"javascript-color-gradient": "^1.3.2", "javascript-color-gradient": "^1.3.2",
"mapbox-gl": "^2.6.1", "mapbox-gl": "^2.6.1",
"minisearch": "^3.2.0", "minisearch": "^3.2.0",

View File

@ -44,3 +44,7 @@
.search-results li:first-child { .search-results li:first-child {
border-top: none; border-top: none;
} }
.highlighted-result {
background-color: #bde4ff;
}