From acf6fc05adffa64a718c0906afdf8b964f4f45d7 Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 20 Mar 2026 20:57:23 +0100 Subject: [PATCH] fix(search): prevent stale navigation from clobbering focused search input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sync $effect on the home page unconditionally overwrote the local `q` state with the URL value after every navigation. When users typed faster than a navigation round-trip (debounce fires → goto() → data reloads), the completed navigation wrote the stale URL value back into the input, dropping the characters typed in the interim. Guard the `q` assignment in the effect with a `qFocused` flag (set via onfocus/onblur on the text input). Covers issue #34. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/routes/+page.svelte | 8 ++++++-- frontend/src/routes/page.svelte.spec.ts | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index d595f52f..fbdd9f93 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -11,6 +11,7 @@ import { formatDate } from '$lib/utils/date'; let { data } = $props(); let q = $state(untrack(() => data.filters?.q || '')); +let qFocused = $state(false); let from = $state(untrack(() => data.filters?.from || '')); let to = $state(untrack(() => data.filters?.to || '')); let senderId = $state(untrack(() => data.filters?.senderId || '')); @@ -61,9 +62,10 @@ $effect(() => { } }); -// Sync local state with server data after navigation +// Sync local state with server data after navigation. +// Guard q: skip overwrite while the user is actively typing in the search field. $effect(() => { - q = data.filters?.q || ''; + if (!qFocused) q = data.filters?.q || ''; from = data.filters?.from || ''; to = data.filters?.to || ''; senderId = data.filters?.senderId || ''; @@ -85,6 +87,8 @@ $effect(() => { type="text" bind:value={q} oninput={handleTextSearch} + onfocus={() => (qFocused = true)} + onblur={() => (qFocused = false)} placeholder={m.docs_search_placeholder()} class="block w-full border-gray-300 py-2.5 pr-10 pl-3 placeholder-gray-400 shadow-sm focus:border-brand-navy focus:ring-brand-navy" /> diff --git a/frontend/src/routes/page.svelte.spec.ts b/frontend/src/routes/page.svelte.spec.ts index c05c70a2..497f8d48 100644 --- a/frontend/src/routes/page.svelte.spec.ts +++ b/frontend/src/routes/page.svelte.spec.ts @@ -164,6 +164,27 @@ describe('Home page – document list', () => { }); }); +// ─── Keystroke preservation (issue #34) ────────────────────────────────────── + +describe('Home page – search input keystroke preservation', () => { + it('does not overwrite the search input while the user is focused and stale data arrives', async () => { + const { rerender } = render(Page, { data: emptyData }); + + const input = page.getByPlaceholder('Suche in Titel, Inhalt, Ort...'); + + // User types "abc" — input is focused + await input.click(); + await input.fill('abc'); + + // Simulate a navigation completing with stale data (q='a') while the user is still typing + await rerender({ data: { ...emptyData, filters: { ...emptyData.filters, q: 'a' } } }); + await tick(); + + // Input must still show what the user typed, not the stale URL value + await expect.element(input).toHaveValue('abc'); + }); +}); + // ─── Error state ────────────────────────────────────────────────────────────── describe('Home page – error state', () => {