diff --git a/package-lock.json b/package-lock.json index 1ebcdb3..89d898f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3576,6 +3576,7 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -4317,6 +4318,7 @@ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6" } diff --git a/src/components/AnchorsPanel.tsx b/src/components/AnchorsPanel.tsx index d0120a1..feeb073 100644 --- a/src/components/AnchorsPanel.tsx +++ b/src/components/AnchorsPanel.tsx @@ -13,7 +13,6 @@ import { useAsync } from "@/hooks/useAsync"; import { useToast } from "@/hooks/useToast"; import { useFocusShortcut } from "@/hooks/useFocusShortcut"; import { useQueryState } from "@/hooks/useQueryState"; -import { useDebouncedValue } from "@/hooks/useDebouncedValue"; import { Card } from "./Card"; import { TableSkeleton } from "./TableSkeleton"; import { AnchorForm } from "./AnchorForm"; @@ -80,30 +79,6 @@ export function AnchorsPanel() { const searchRef = useRef(null); useFocusShortcut("/", searchRef); - // One ref per filter button for imperative focus management (roving tabindex). - const filterRefs = useRef>( - Array(FILTERS.length).fill(null), - ); - - /** Arrow-key / Home / End roving focus across the filter button group. */ - function onFilterKeyDown( - event: React.KeyboardEvent, - index: number, - ) { - const last = FILTERS.length - 1; - let next: number | null = null; - - if (event.key === "ArrowRight") next = index === last ? 0 : index + 1; - else if (event.key === "ArrowLeft") next = index === 0 ? last : index - 1; - else if (event.key === "Home") next = 0; - else if (event.key === "End") next = last; - - if (next !== null) { - event.preventDefault(); - filterRefs.current[next]?.focus(); - } - } - // Sync status filter and search query to the URL querystring. // Initial values are hydrated from the URL on first render. const [rawStatus, setStatus] = useQueryState("status", "all"); @@ -247,7 +222,6 @@ export function AnchorsPanel() { filterRefs.current[i] = el; }} onClick={() => setStatus(f.value)} - onKeyDown={(e) => onFilterKeyDown(e, i)} aria-pressed={filter === f.value} tabIndex={filter === f.value ? 0 : -1} className={`rounded-full px-3 py-1 text-xs font-medium transition-colors ${ diff --git a/src/components/SettlementsPanel.test.tsx b/src/components/SettlementsPanel.test.tsx index 66e90b2..b690cb7 100644 --- a/src/components/SettlementsPanel.test.tsx +++ b/src/components/SettlementsPanel.test.tsx @@ -31,6 +31,19 @@ vi.mock("next/navigation", () => ({ usePathname: () => "/settlements", })); +// --------------------------------------------------------------------------- +// Mock next/navigation so the panel can run in jsdom. +// --------------------------------------------------------------------------- + +const mockReplace = vi.fn(); +let mockSearchParamsString = ""; + +vi.mock("next/navigation", () => ({ + useRouter: () => ({ replace: mockReplace }), + useSearchParams: () => new URLSearchParams(mockSearchParamsString), + usePathname: () => "/settlements", +})); + vi.mock("@/lib/settlementsApi", () => ({ fetchSettlements: vi.fn(), openSettlement: vi.fn(), @@ -50,7 +63,6 @@ vi.mock("@/lib/api", async (importOriginal) => { beforeEach(() => { vi.clearAllMocks(); mockSearchParamsString = ""; - vi.mocked(fetchPools).mockResolvedValue([]); }); function page( diff --git a/src/lib/search.ts b/src/lib/search.ts index 2a5591e..6daa874 100644 --- a/src/lib/search.ts +++ b/src/lib/search.ts @@ -12,10 +12,7 @@ export function matchesQuery( fields: Array, query: string, ): boolean { - const trimmed = query.trim().replace(/\s+/g, " ").toLowerCase(); - if (trimmed === "") return true; - const terms = trimmed.split(" "); - return terms.every((term) => - fields.some((field) => String(field).toLowerCase().includes(term)) - ); + const needle = query.trim().replace(/\s+/g, " ").toLowerCase(); + if (needle === "") return true; + return fields.some((field) => String(field).toLowerCase().includes(needle)); } diff --git a/src/lib/settlementsApi.ts b/src/lib/settlementsApi.ts index c737562..5c437ee 100644 --- a/src/lib/settlementsApi.ts +++ b/src/lib/settlementsApi.ts @@ -32,12 +32,7 @@ export async function exportSettlementsCsv( options: FetchSettlementsOptions = {}, ): Promise { const { anchor, page, pageSize, signal } = options; - const params = new URLSearchParams(); - if (anchor) params.set("anchor", anchor); - if (page) params.set("page", String(page)); - if (pageSize) params.set("pageSize", String(pageSize)); - params.set("format", "csv"); - const query = params.toString() ? `?${params.toString()}` : ""; + const query = buildQueryParams({ anchor, page, pageSize, format: "csv" }); return apiTextRequest(`/api/v1/settlements${query}`, { signal }); }