-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathSearchInput.tsx
More file actions
106 lines (97 loc) · 3.42 KB
/
SearchInput.tsx
File metadata and controls
106 lines (97 loc) · 3.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/20/solid";
import { motion } from "framer-motion";
import { useCallback, useEffect, useRef, useState } from "react";
import { Input } from "~/components/primitives/Input";
import { ShortcutKey } from "~/components/primitives/ShortcutKey";
import { useSearchParams } from "~/hooks/useSearchParam";
import { cn } from "~/utils/cn";
export type SearchInputProps = {
placeholder?: string;
/** Additional URL params to reset when searching or clearing (e.g. pagination). Defaults to ["cursor", "direction"]. */
resetParams?: string[];
};
export function SearchInput({
placeholder = "Search logs…",
resetParams = ["cursor", "direction"],
}: SearchInputProps) {
const inputRef = useRef<HTMLInputElement>(null);
const { value, replace, del } = useSearchParams();
const initialSearch = value("search") ?? "";
const [text, setText] = useState(initialSearch);
const [isFocused, setIsFocused] = useState(false);
useEffect(() => {
const urlSearch = value("search") ?? "";
if (urlSearch !== text && !isFocused) {
setText(urlSearch);
}
}, [value, text, isFocused]);
const handleSubmit = useCallback(() => {
const resetValues = Object.fromEntries(resetParams.map((p) => [p, undefined]));
if (text.trim()) {
replace({ search: text.trim(), ...resetValues });
} else {
del(["search", ...resetParams]);
}
}, [text, replace, del, resetParams]);
const handleClear = useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
setText("");
del(["search", ...resetParams]);
},
[del, resetParams]
);
return (
<div className="flex items-center gap-1">
<motion.div
initial={{ width: "auto" }}
animate={{ width: isFocused && text.length > 0 ? "24rem" : "auto" }}
transition={{
type: "spring",
stiffness: 300,
damping: 30,
}}
className="relative h-6 min-w-52"
>
<Input
type="text"
ref={inputRef}
variant="secondary-small"
placeholder={placeholder}
value={text}
onChange={(e) => setText(e.target.value)}
fullWidth
className={cn("", isFocused && "placeholder:text-text-dimmed/70")}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleSubmit();
}
if (e.key === "Escape") {
e.currentTarget.blur();
}
}}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
icon={<MagnifyingGlassIcon className="size-4" />}
accessory={
text.length > 0 ? (
<div className="-mr-1 flex items-center gap-1.5">
<ShortcutKey shortcut={{ key: "enter" }} variant="medium" className="border-none" />
<button
type="button"
onClick={handleClear}
className="flex size-4.5 items-center justify-center rounded-[2px] border border-text-dimmed/40 text-text-dimmed transition hover:bg-charcoal-600 hover:text-text-bright"
title="Clear search"
>
<XMarkIcon className="size-3" />
</button>
</div>
) : undefined
}
/>
</motion.div>
</div>
);
}