fix(search): prevent stale navigation from clobbering focused search input
Some checks failed
CI / E2E Tests (push) Waiting to run
CI / Unit & Component Tests (push) Successful in 10m14s
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (pull_request) Failing after 13m49s
CI / Backend Unit Tests (pull_request) Failing after 13m59s
CI / Unit & Component Tests (pull_request) Failing after 14m9s
Some checks failed
CI / E2E Tests (push) Waiting to run
CI / Unit & Component Tests (push) Successful in 10m14s
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (pull_request) Failing after 13m49s
CI / Backend Unit Tests (pull_request) Failing after 13m59s
CI / Unit & Component Tests (pull_request) Failing after 14m9s
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 <noreply@anthropic.com>
This commit was merged in pull request #41.
This commit is contained in:
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user