fix(web): SearchInput passes keepFocus to goto() to prevent blur on debounce
The actual cause of the search-field focus loss: SvelteKit's `goto()`
defaults to resetting focus on navigation, so each debounced URL update
blurred the input mid-typing. Pass `keepFocus: true` to both `goto()`
calls in `navigate()`.
The earlier `bind:value` change in 6931358 didn't address this — it was
solving a hypothetical DOM-write-side cursor-jump issue rather than the
real focus-reset behavior. Leaving `bind:value` in place since it's
still cleaner Svelte 5.
Tests updated to match the new goto args.
This commit is contained in:
@@ -17,11 +17,17 @@
|
||||
function navigate(next: string) {
|
||||
const onSearchPage = page.url.pathname.startsWith('/search');
|
||||
const trimmed = next.trim();
|
||||
// keepFocus: SvelteKit blurs the active element on navigation by default.
|
||||
// For a typeahead-style URL update we want the user to keep typing
|
||||
// without re-clicking the input every 250 ms.
|
||||
if (trimmed === '') {
|
||||
if (onSearchPage) goto('/search', { replaceState: true });
|
||||
if (onSearchPage) goto('/search', { replaceState: true, keepFocus: true });
|
||||
return;
|
||||
}
|
||||
goto(`/search?q=${encodeURIComponent(trimmed)}`, { replaceState: onSearchPage });
|
||||
goto(`/search?q=${encodeURIComponent(trimmed)}`, {
|
||||
replaceState: onSearchPage,
|
||||
keepFocus: true,
|
||||
});
|
||||
}
|
||||
|
||||
function onInput() {
|
||||
|
||||
@@ -41,7 +41,7 @@ describe('SearchInput', () => {
|
||||
expect(goto).not.toHaveBeenCalled();
|
||||
vi.advanceTimersByTime(250);
|
||||
expect(goto).toHaveBeenCalledTimes(1);
|
||||
expect(goto).toHaveBeenCalledWith('/search?q=h', { replaceState: false });
|
||||
expect(goto).toHaveBeenCalledWith('/search?q=h', { replaceState: false, keepFocus: true });
|
||||
});
|
||||
|
||||
test('rapid keystrokes debounce to a single goto', async () => {
|
||||
@@ -54,7 +54,7 @@ describe('SearchInput', () => {
|
||||
await fireEvent.input(input, { target: { value: 'hel' } });
|
||||
vi.advanceTimersByTime(250);
|
||||
expect(goto).toHaveBeenCalledTimes(1);
|
||||
expect(goto).toHaveBeenCalledWith('/search?q=hel', { replaceState: false });
|
||||
expect(goto).toHaveBeenCalledWith('/search?q=hel', { replaceState: false, keepFocus: true });
|
||||
});
|
||||
|
||||
test('typing while on /search uses replaceState=true', async () => {
|
||||
@@ -63,7 +63,7 @@ describe('SearchInput', () => {
|
||||
const input = screen.getByRole('searchbox') as HTMLInputElement;
|
||||
await fireEvent.input(input, { target: { value: 'he' } });
|
||||
vi.advanceTimersByTime(250);
|
||||
expect(goto).toHaveBeenCalledWith('/search?q=he', { replaceState: true });
|
||||
expect(goto).toHaveBeenCalledWith('/search?q=he', { replaceState: true, keepFocus: true });
|
||||
});
|
||||
|
||||
test('clearing input on /search calls goto("/search") (drops ?q=)', async () => {
|
||||
@@ -72,7 +72,7 @@ describe('SearchInput', () => {
|
||||
const input = screen.getByRole('searchbox') as HTMLInputElement;
|
||||
await fireEvent.input(input, { target: { value: '' } });
|
||||
vi.advanceTimersByTime(250);
|
||||
expect(goto).toHaveBeenCalledWith('/search', { replaceState: true });
|
||||
expect(goto).toHaveBeenCalledWith('/search', { replaceState: true, keepFocus: true });
|
||||
});
|
||||
|
||||
test('Escape clears the input value', async () => {
|
||||
@@ -89,6 +89,6 @@ describe('SearchInput', () => {
|
||||
const input = screen.getByRole('searchbox') as HTMLInputElement;
|
||||
await fireEvent.input(input, { target: { value: 'a b&c' } });
|
||||
vi.advanceTimersByTime(250);
|
||||
expect(goto).toHaveBeenCalledWith('/search?q=a%20b%26c', { replaceState: false });
|
||||
expect(goto).toHaveBeenCalledWith('/search?q=a%20b%26c', { replaceState: false, keepFocus: true });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user