mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-01 22:23:07 +00:00
website: submit-based plugin search on the home page, back button works on browse (#6256)
* website: submit-based plugin search on the home page, back button works on browse * website: cover Forward in the browse search history test
This commit is contained in:
@@ -80,7 +80,7 @@ test("filters by search and clears to all plugins", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await openPlugins(page);
|
||||
|
||||
await searchPlugins(page, "graphite");
|
||||
await submitSearch(page, "graphite");
|
||||
await expect(page).toHaveURL(/\/plugins\/all\?q=graphite$/);
|
||||
await expect(
|
||||
page.getByRole("heading", { level: 1, name: /^Results for “graphite”/ }),
|
||||
@@ -98,7 +98,7 @@ test("filters by search and clears to all plugins", async ({ page }) => {
|
||||
|
||||
test("keeps the directory's ranking window when searching", async ({ page }) => {
|
||||
await page.goto("/plugins?window=month");
|
||||
await searchPlugins(page, "graphite");
|
||||
await submitSearch(page, "graphite");
|
||||
await expect(page).toHaveURL(/\/plugins\/all\?q=graphite&window=month$/);
|
||||
await expect(page.getByRole("link", { name: "This month" })).toHaveAttribute(
|
||||
"aria-current",
|
||||
@@ -122,13 +122,52 @@ test("clears the search with the clear button", async ({ page }) => {
|
||||
await expect(page.getByRole("heading", { level: 1, name: /^All plugins/ })).toBeVisible();
|
||||
});
|
||||
|
||||
test("replaces history while typing a search", async ({ page }) => {
|
||||
test("searches from the directory on submit, and Back returns to the directory", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await openPlugins(page);
|
||||
const entries = await historyLength(page);
|
||||
|
||||
await typeSearch(page, "gra");
|
||||
await expect(page).toHaveURL(/\/plugins$/);
|
||||
expect(await historyLength(page)).toBe(entries);
|
||||
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(page).toHaveURL(/\/plugins\/all\?q=gra$/);
|
||||
await expect(page.getByRole("heading", { level: 1, name: /^Results for “gra”/ })).toBeVisible();
|
||||
expect(await historyLength(page)).toBe(entries + 1);
|
||||
|
||||
await page.goBack();
|
||||
await expect(page).toHaveURL(/\/plugins$/);
|
||||
await expect(page.getByRole("heading", { level: 1, name: /^Plugins/ })).toBeVisible();
|
||||
});
|
||||
|
||||
test("filters browse results as you type in one history entry", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.goto("/plugins/all");
|
||||
await searchPlugins(page, "graphite");
|
||||
await expect(page).toHaveURL(/\/plugins\/all\?q=graphite$/);
|
||||
const entries = await historyLength(page);
|
||||
|
||||
await typeSearch(page, "gra");
|
||||
await expect(page).toHaveURL(/\/plugins\/all\?q=gra$/);
|
||||
await expect(page.getByRole("heading", { level: 1, name: /^Results for “gra”/ })).toBeVisible();
|
||||
await expect(page.getByRole("main").getByRole("link", { name: /Graphite/ })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: /Dracula/ })).toHaveCount(0);
|
||||
expect(await historyLength(page)).toBe(entries + 1);
|
||||
|
||||
await page.goBack();
|
||||
await expect(page).toHaveURL(/\/$/);
|
||||
await expect(page).toHaveURL(/\/plugins\/all$/);
|
||||
await expect(page.getByRole("heading", { level: 1, name: /^All plugins/ })).toBeVisible();
|
||||
await expect(page.getByRole("searchbox", { name: "Search plugins" })).toHaveValue("");
|
||||
|
||||
await page.goForward();
|
||||
await expect(page).toHaveURL(/\/plugins\/all\?q=gra$/);
|
||||
await expect(page.getByRole("heading", { level: 1, name: /^Results for “gra”/ })).toBeVisible();
|
||||
await expect(page.getByRole("searchbox", { name: "Search plugins" })).toHaveValue("gra");
|
||||
|
||||
await page.reload();
|
||||
await expect(page.getByRole("heading", { level: 1, name: /^Results for “gra”/ })).toBeVisible();
|
||||
await expect(page.getByRole("searchbox", { name: "Search plugins" })).toHaveValue("gra");
|
||||
});
|
||||
|
||||
test("keeps old category links working", async ({ page }) => {
|
||||
@@ -262,6 +301,19 @@ async function searchPlugins(page: Page, term: string) {
|
||||
await page.getByRole("searchbox", { name: "Search plugins" }).fill(term);
|
||||
}
|
||||
|
||||
async function submitSearch(page: Page, term: string) {
|
||||
await searchPlugins(page, term);
|
||||
await page.keyboard.press("Enter");
|
||||
}
|
||||
|
||||
async function typeSearch(page: Page, term: string) {
|
||||
await page.getByRole("searchbox", { name: "Search plugins" }).pressSequentially(term);
|
||||
}
|
||||
|
||||
async function historyLength(page: Page): Promise<number> {
|
||||
return page.evaluate(() => window.history.length);
|
||||
}
|
||||
|
||||
async function expectPageMetadata(page: Page, title: string, path: string) {
|
||||
await expect(page).toHaveTitle(title);
|
||||
await expect(page.locator('link[rel="canonical"]')).toHaveAttribute(
|
||||
|
||||
@@ -56,7 +56,7 @@ export function BrowsePage({
|
||||
<SiteShell width="wide">
|
||||
<div className="lg:flex lg:gap-12">
|
||||
<aside className="mb-8 lg:mb-0 lg:w-48 lg:flex-shrink-0">
|
||||
<PluginSearch scope={query} className="mb-4 lg:mb-6" />
|
||||
<PluginSearch scope={query} live className="mb-4 lg:mb-6" />
|
||||
<CategoryNav plugins={matches} query={query} />
|
||||
</aside>
|
||||
<div className="min-w-0 flex-1">
|
||||
|
||||
@@ -1,37 +1,67 @@
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { useNavigate, useRouter } from "@tanstack/react-router";
|
||||
import { Search, X } from "lucide-react";
|
||||
import { type ChangeEvent, type FormEvent, useCallback, useRef, useState } from "react";
|
||||
import { type BrowseQuery, browseHref, DEFAULT_WINDOW } from "./links";
|
||||
import { type ChangeEvent, type FormEvent, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { type BrowseQuery, browseHref, DEFAULT_WINDOW, parseSearchTerm } 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.
|
||||
* Search box for the plugin pages. Submitting opens the browse page for the term, keeping
|
||||
* `scope`'s category and sort. With `live`, typing filters in place instead: the first character
|
||||
* of a query pushes one history entry and later edits replace it, so Back leaves the search.
|
||||
* Without JavaScript the form submits the same URL.
|
||||
*/
|
||||
export function PluginSearch({ scope, className }: { scope: BrowseQuery; className?: string }) {
|
||||
export function PluginSearch({
|
||||
scope,
|
||||
live = false,
|
||||
className,
|
||||
}: {
|
||||
scope: BrowseQuery;
|
||||
live?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
const [term, setTerm] = useState(scope.q ?? "");
|
||||
const search = useCallback(
|
||||
(next: string) => {
|
||||
setTerm(next);
|
||||
void navigate({
|
||||
href: browseHref({ ...scope, q: next.trim() ? next : undefined }),
|
||||
replace: true,
|
||||
});
|
||||
// Back and Forward between results of the same page keep this box mounted; show their term.
|
||||
useEffect(
|
||||
() =>
|
||||
router.history.subscribe(({ location, action }) => {
|
||||
if (action.type === "PUSH" || action.type === "REPLACE") return;
|
||||
setTerm(parseSearchTerm(new URLSearchParams(location.search).get("q")) ?? "");
|
||||
}),
|
||||
[router],
|
||||
);
|
||||
const show = useCallback(
|
||||
(next: string, { replace }: { replace: boolean }) => {
|
||||
void navigate({ href: browseHref({ ...scope, q: next.trim() ? next : undefined }), replace });
|
||||
},
|
||||
[navigate, scope],
|
||||
);
|
||||
const edit = useCallback(
|
||||
(next: string) => {
|
||||
setTerm(next);
|
||||
if (live) show(next, { replace: Boolean(term.trim()) });
|
||||
},
|
||||
[live, show, term],
|
||||
);
|
||||
const handleChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => search(event.target.value),
|
||||
[search],
|
||||
(event: ChangeEvent<HTMLInputElement>) => edit(event.target.value),
|
||||
[edit],
|
||||
);
|
||||
const handleClear = useCallback(() => {
|
||||
search("");
|
||||
edit("");
|
||||
input.current?.focus();
|
||||
}, [search]);
|
||||
const handleSubmit = useCallback((event: FormEvent) => event.preventDefault(), []);
|
||||
}, [edit]);
|
||||
const handleSubmit = useCallback(
|
||||
(event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
// Live results already show the term.
|
||||
if (!live) show(term, { replace: false });
|
||||
},
|
||||
[live, show, term],
|
||||
);
|
||||
return (
|
||||
<form
|
||||
role="search"
|
||||
|
||||
Reference in New Issue
Block a user