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) {
|
function navigate(next: string) {
|
||||||
const onSearchPage = page.url.pathname.startsWith('/search');
|
const onSearchPage = page.url.pathname.startsWith('/search');
|
||||||
const trimmed = next.trim();
|
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 (trimmed === '') {
|
||||||
if (onSearchPage) goto('/search', { replaceState: true });
|
if (onSearchPage) goto('/search', { replaceState: true, keepFocus: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
goto(`/search?q=${encodeURIComponent(trimmed)}`, { replaceState: onSearchPage });
|
goto(`/search?q=${encodeURIComponent(trimmed)}`, {
|
||||||
|
replaceState: onSearchPage,
|
||||||
|
keepFocus: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onInput() {
|
function onInput() {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ describe('SearchInput', () => {
|
|||||||
expect(goto).not.toHaveBeenCalled();
|
expect(goto).not.toHaveBeenCalled();
|
||||||
vi.advanceTimersByTime(250);
|
vi.advanceTimersByTime(250);
|
||||||
expect(goto).toHaveBeenCalledTimes(1);
|
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 () => {
|
test('rapid keystrokes debounce to a single goto', async () => {
|
||||||
@@ -54,7 +54,7 @@ describe('SearchInput', () => {
|
|||||||
await fireEvent.input(input, { target: { value: 'hel' } });
|
await fireEvent.input(input, { target: { value: 'hel' } });
|
||||||
vi.advanceTimersByTime(250);
|
vi.advanceTimersByTime(250);
|
||||||
expect(goto).toHaveBeenCalledTimes(1);
|
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 () => {
|
test('typing while on /search uses replaceState=true', async () => {
|
||||||
@@ -63,7 +63,7 @@ describe('SearchInput', () => {
|
|||||||
const input = screen.getByRole('searchbox') as HTMLInputElement;
|
const input = screen.getByRole('searchbox') as HTMLInputElement;
|
||||||
await fireEvent.input(input, { target: { value: 'he' } });
|
await fireEvent.input(input, { target: { value: 'he' } });
|
||||||
vi.advanceTimersByTime(250);
|
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 () => {
|
test('clearing input on /search calls goto("/search") (drops ?q=)', async () => {
|
||||||
@@ -72,7 +72,7 @@ describe('SearchInput', () => {
|
|||||||
const input = screen.getByRole('searchbox') as HTMLInputElement;
|
const input = screen.getByRole('searchbox') as HTMLInputElement;
|
||||||
await fireEvent.input(input, { target: { value: '' } });
|
await fireEvent.input(input, { target: { value: '' } });
|
||||||
vi.advanceTimersByTime(250);
|
vi.advanceTimersByTime(250);
|
||||||
expect(goto).toHaveBeenCalledWith('/search', { replaceState: true });
|
expect(goto).toHaveBeenCalledWith('/search', { replaceState: true, keepFocus: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Escape clears the input value', async () => {
|
test('Escape clears the input value', async () => {
|
||||||
@@ -89,6 +89,6 @@ describe('SearchInput', () => {
|
|||||||
const input = screen.getByRole('searchbox') as HTMLInputElement;
|
const input = screen.getByRole('searchbox') as HTMLInputElement;
|
||||||
await fireEvent.input(input, { target: { value: 'a b&c' } });
|
await fireEvent.input(input, { target: { value: 'a b&c' } });
|
||||||
vi.advanceTimersByTime(250);
|
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