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

32
package-lock.json generated
View File

@ -25,7 +25,6 @@
"version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz",
"integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==",
"dev": true,
"requires": {
"regenerator-runtime": "^0.13.4"
}
@ -555,6 +554,11 @@
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
"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": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -657,6 +661,25 @@
"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": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.3.tgz",
@ -1976,7 +1999,6 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"requires": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
@ -1986,8 +2008,7 @@
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
}
}
},
@ -2055,8 +2076,7 @@
"regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==",
"dev": true
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
},
"regexp.prototype.flags": {
"version": "1.3.2",

View File

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

View File

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