fix(website): replace the native search clear control with a muted X button (#6082)

This commit is contained in:
Mohamed Boudra
2026-10-04 21:15:23 +01:00
committed by GitHub
parent b3ed7363c6
commit 8a4ce5a482
2 changed files with 48 additions and 9 deletions
+16
View File
@@ -104,6 +104,22 @@ test("keeps the directory's ranking window when searching", async ({ page }) =>
);
});
test("clears the search with the clear button", async ({ page }) => {
await page.goto("/plugins/all");
const searchbox = page.getByRole("searchbox", { name: "Search plugins" });
const clear = page.getByRole("button", { name: "Clear search" });
await expect(clear).toHaveCount(0);
await searchPlugins(page, "graphite");
await expect(page).toHaveURL(/\/plugins\/all\?q=graphite$/);
await clear.click();
await expect(page).toHaveURL(/\/plugins\/all$/);
await expect(searchbox).toHaveValue("");
await expect(searchbox).toBeFocused();
await expect(clear).toHaveCount(0);
await expect(page.getByRole("heading", { level: 1, name: /^All plugins/ })).toBeVisible();
});
test("replaces history while typing a search", async ({ page }) => {
await page.goto("/");
await page.goto("/plugins/all");
+32 -9
View File
@@ -1,18 +1,20 @@
import { useNavigate } from "@tanstack/react-router";
import { Search } from "lucide-react";
import { type ChangeEvent, type FormEvent, useCallback, useState } from "react";
import { Search, X } from "lucide-react";
import { type ChangeEvent, type FormEvent, useCallback, useRef, useState } from "react";
import { type BrowseQuery, browseHref, DEFAULT_WINDOW } from "./links";
const ICON_CLASS = "h-3.5 w-3.5 text-extra-muted-foreground";
/**
* Search box for the plugin pages. Typing replaces the URL with the browse page for the term,
* keeping `scope`'s category and sort. Without JavaScript the form submits the same URL.
*/
export function PluginSearch({ scope, className }: { scope: BrowseQuery; className?: string }) {
const navigate = useNavigate();
const input = useRef<HTMLInputElement>(null);
const [term, setTerm] = useState(scope.q ?? "");
const handleChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
const next = event.target.value;
const search = useCallback(
(next: string) => {
setTerm(next);
void navigate({
href: browseHref({ ...scope, q: next.trim() ? next : undefined }),
@@ -21,6 +23,14 @@ export function PluginSearch({ scope, className }: { scope: BrowseQuery; classNa
},
[navigate, scope],
);
const handleChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => search(event.target.value),
[search],
);
const handleClear = useCallback(() => {
search("");
input.current?.focus();
}, [search]);
const handleSubmit = useCallback((event: FormEvent) => event.preventDefault(), []);
return (
<form
@@ -34,9 +44,12 @@ export function PluginSearch({ scope, className }: { scope: BrowseQuery; classNa
{scope.sort === "installs" && scope.window !== DEFAULT_WINDOW && (
<input type="hidden" name="window" value={scope.window} />
)}
<label className="relative block">
<Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-extra-muted-foreground" />
<div className="relative">
<Search
className={`pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 ${ICON_CLASS}`}
/>
<input
ref={input}
type="search"
name="q"
value={term}
@@ -45,9 +58,19 @@ export function PluginSearch({ scope, className }: { scope: BrowseQuery; classNa
aria-label="Search plugins"
// Typing on the directory lands here; keep the caret in the box.
autoFocus={Boolean(scope.q)}
className="w-full rounded-lg border border-white/10 bg-white/[0.03] py-1.5 pl-8 pr-3 text-sm text-foreground placeholder:text-extra-muted-foreground focus:border-white/20 focus:outline-none"
className="w-full rounded-lg border border-white/10 bg-white/[0.03] py-1.5 pl-8 pr-8 text-sm text-foreground placeholder:text-extra-muted-foreground focus:border-white/20 focus:outline-none [&::-webkit-search-cancel-button]:appearance-none"
/>
</label>
{term && (
<button
type="button"
onClick={handleClear}
aria-label="Clear search"
className="absolute right-1.5 top-1/2 -translate-y-1/2 rounded p-1 transition-colors hover:bg-white/[0.06]"
>
<X className={ICON_CLASS} />
</button>
)}
</div>
</form>
);
}