fix: LiveSearchForm — clear remet tout à zéro + focus préservé
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 47s

- Clear supprime le cookie ET force un remount du form (defaultValues à vide)
- Navigation propre via useEffect au lieu de mutations pendant le render
- Le focus est préservé lors de nos propres navigations (recherche, filtres)
- Remount uniquement sur navigation externe (back/forward du navigateur)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 20:47:45 +02:00
parent c005d408bf
commit 9da70021f9

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useRef, useCallback, useEffect, useTransition } from "react"; import { useRef, useCallback, useEffect, useTransition, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { useTranslation } from "../../lib/i18n/context"; import { useTranslation } from "../../lib/i18n/context";
import { Icon } from "./ui"; import { Icon } from "./ui";
@@ -61,6 +61,9 @@ export function LiveSearchForm({ fields, basePath, debounceMs = 300 }: LiveSearc
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null); const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const formRef = useRef<HTMLFormElement>(null); const formRef = useRef<HTMLFormElement>(null);
// Incremented to force remount only on external navigation
const [formKey, setFormKey] = useState(0);
const lastNavParamsRef = useRef<string | null>(null);
const cookieName = filterCookieName(basePath); const cookieName = filterCookieName(basePath);
@@ -93,20 +96,23 @@ export function LiveSearchForm({ fields, basePath, debounceMs = 300 }: LiveSearc
} catch {} } catch {}
}, [cookieName]); }, [cookieName]);
const doNavigate = useCallback((url: string) => {
lastNavParamsRef.current = new URL(url, "http://x").search.replace(/^\?/, "");
startTransition(() => { router.replace(url as any); });
}, [router]);
const navigate = useCallback((immediate: boolean) => { const navigate = useCallback((immediate: boolean) => {
if (timerRef.current) clearTimeout(timerRef.current); if (timerRef.current) clearTimeout(timerRef.current);
if (immediate) { if (immediate) {
saveFilters(); saveFilters();
isOwnNavRef.current = true; doNavigate(buildUrl());
startTransition(() => { router.replace(buildUrl() as any); });
} else { } else {
timerRef.current = setTimeout(() => { timerRef.current = setTimeout(() => {
saveFilters(); saveFilters();
isOwnNavRef.current = true; doNavigate(buildUrl());
startTransition(() => { router.replace(buildUrl() as any); });
}, debounceMs); }, debounceMs);
} }
}, [router, buildUrl, debounceMs, saveFilters]); }, [buildUrl, debounceMs, saveFilters, doNavigate]);
useEffect(() => { useEffect(() => {
return () => { return () => {
@@ -114,6 +120,17 @@ export function LiveSearchForm({ fields, basePath, debounceMs = 300 }: LiveSearc
}; };
}, []); }, []);
// Detect external navigation (back/forward) and remount form to sync values.
// Our own navigations store the expected params so we can skip the remount.
useEffect(() => {
const current = searchParams.toString();
if (lastNavParamsRef.current !== null && lastNavParamsRef.current !== current) {
// Params changed externally → remount to sync defaultValues
setFormKey(k => k + 1);
}
lastNavParamsRef.current = null;
}, [searchParams]);
const hasFilters = fields.some((f) => { const hasFilters = fields.some((f) => {
const val = searchParams.get(f.name); const val = searchParams.get(f.name);
return val && val.trim() !== ""; return val && val.trim() !== "";
@@ -122,32 +139,15 @@ export function LiveSearchForm({ fields, basePath, debounceMs = 300 }: LiveSearc
const textFields = fields.filter((f) => f.type === "text"); const textFields = fields.filter((f) => f.type === "text");
const selectFields = fields.filter((f) => f.type === "select"); const selectFields = fields.filter((f) => f.type === "select");
// Track whether the current navigation was initiated by us (not back/forward)
const isOwnNavRef = useRef(false);
// Force remount only on external navigation (back/forward, cookie redirect)
// Our own navigations skip remount to preserve focus.
const prevParamsRef = useRef(searchParams.toString());
const formKey = useRef(0);
const currentParams = searchParams.toString();
if (currentParams !== prevParamsRef.current) {
if (!isOwnNavRef.current) {
formKey.current += 1; // External nav → remount
}
isOwnNavRef.current = false;
prevParamsRef.current = currentParams;
}
return ( return (
<form <form
key={formKey.current} key={formKey}
ref={formRef} ref={formRef}
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
if (timerRef.current) clearTimeout(timerRef.current); if (timerRef.current) clearTimeout(timerRef.current);
saveFilters(); saveFilters();
isOwnNavRef.current = true; doNavigate(buildUrl());
startTransition(() => { router.replace(buildUrl() as any); });
}} }}
className="space-y-4" className="space-y-4"
> >
@@ -206,8 +206,10 @@ export function LiveSearchForm({ fields, basePath, debounceMs = 300 }: LiveSearc
onClick={() => { onClick={() => {
formRef.current?.reset(); formRef.current?.reset();
try { deleteCookie(cookieName); } catch {} try { deleteCookie(cookieName); } catch {}
isOwnNavRef.current = true; // Navigate to base path without any params
startTransition(() => { router.replace(basePath as any); }); doNavigate(basePath);
// Force remount so defaultValues reset to empty
setFormKey(k => k + 1);
}} }}
className=" className="
inline-flex items-center gap-1 inline-flex items-center gap-1