From 2a3818ce9862e740aa0025f0c27da6cd1c32b1c1 Mon Sep 17 00:00:00 2001 From: Mathias Picker Date: Fri, 27 Feb 2026 22:51:35 +0100 Subject: [PATCH 1/2] fix: blur active element before component update during navigation (#14575) Blur the active element before `root.$set()` so that blur/focusout handlers fire while the old component's data is still valid. Previously, data was nulled out before blur fired, causing TypeErrors in blur handlers that accessed component data. Guarded by `!keepfocus` and only applies when the active element is not the body. Co-Authored-By: Claude Opus 4.6 --- .changeset/blur-during-navigation.md | 5 +++++ packages/kit/src/runtime/client/client.js | 10 ++++++++++ .../blur-during-navigation/other/+page.svelte | 2 ++ .../page-with-input/+page.js | 3 +++ .../page-with-input/+page.svelte | 14 ++++++++++++++ .../basics/test/cross-platform/client.test.js | 19 +++++++++++++++++++ 6 files changed, 53 insertions(+) create mode 100644 .changeset/blur-during-navigation.md create mode 100644 packages/kit/test/apps/basics/src/routes/accessibility/blur-during-navigation/other/+page.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/accessibility/blur-during-navigation/page-with-input/+page.js create mode 100644 packages/kit/test/apps/basics/src/routes/accessibility/blur-during-navigation/page-with-input/+page.svelte diff --git a/.changeset/blur-during-navigation.md b/.changeset/blur-during-navigation.md new file mode 100644 index 000000000000..aa51e5b2ce79 --- /dev/null +++ b/.changeset/blur-during-navigation.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: blur active element before component update during navigation so that blur/focusout handlers fire while old component data is still valid diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 0faada3d4b01..683960163f62 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -1739,6 +1739,16 @@ async function navigate({ navigation_result.props.page.url = url; } + // Remove focus before updating the component tree, so that blur/focusout + // handlers fire while the old component's data is still valid (#14575) + if ( + !keepfocus && + document.activeElement instanceof HTMLElement && + document.activeElement !== document.body + ) { + document.activeElement.blur(); + } + const fork = load_cache_fork && (await load_cache_fork); if (fork) { diff --git a/packages/kit/test/apps/basics/src/routes/accessibility/blur-during-navigation/other/+page.svelte b/packages/kit/test/apps/basics/src/routes/accessibility/blur-during-navigation/other/+page.svelte new file mode 100644 index 000000000000..77976b8a85f6 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/accessibility/blur-during-navigation/other/+page.svelte @@ -0,0 +1,2 @@ +

Other page

+Back diff --git a/packages/kit/test/apps/basics/src/routes/accessibility/blur-during-navigation/page-with-input/+page.js b/packages/kit/test/apps/basics/src/routes/accessibility/blur-during-navigation/page-with-input/+page.js new file mode 100644 index 000000000000..bc2650dd05ed --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/accessibility/blur-during-navigation/page-with-input/+page.js @@ -0,0 +1,3 @@ +export function load() { + return { message: 'hello' }; +} diff --git a/packages/kit/test/apps/basics/src/routes/accessibility/blur-during-navigation/page-with-input/+page.svelte b/packages/kit/test/apps/basics/src/routes/accessibility/blur-during-navigation/page-with-input/+page.svelte new file mode 100644 index 000000000000..e35f82e2abfc --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/accessibility/blur-during-navigation/page-with-input/+page.svelte @@ -0,0 +1,14 @@ + + +

Blur test

+ +Go to other diff --git a/packages/kit/test/apps/basics/test/cross-platform/client.test.js b/packages/kit/test/apps/basics/test/cross-platform/client.test.js index 33afa996226e..42a24031e720 100644 --- a/packages/kit/test/apps/basics/test/cross-platform/client.test.js +++ b/packages/kit/test/apps/basics/test/cross-platform/client.test.js @@ -122,6 +122,25 @@ test.describe('a11y', () => { ).toBe('BODY'); expect(await page.evaluate(() => document.activeElement?.nodeName)).toBe('BODY'); }); + + test('blur handler can access data during navigation', async ({ page, app }) => { + const errors = /** @type {string[]} */ ([]); + page.on('pageerror', (err) => errors.push(err.message)); + + await page.goto('/accessibility/blur-during-navigation/page-with-input'); + + // Focus the input + await page.locator('#blur-input').focus(); + + // Navigate away — this triggers blur on the focused input. + // Without the fix, data would be nulled before blur fired, causing a TypeError. + await app.goto('/accessibility/blur-during-navigation/other'); + + expect(errors).toEqual([]); + + // The blur handler should have been able to read data.message + expect(await page.evaluate(() => /** @type {any} */ (window).__blur_test_result)).toBe('hello'); + }); }); test.describe('Navigation lifecycle functions', () => { From 3894e2660c244bacb1b1923a5f0e72632b9687fb Mon Sep 17 00:00:00 2001 From: Mathias Picker Date: Sat, 28 Feb 2026 11:42:03 +0100 Subject: [PATCH 2/2] fix: cast window to any for custom property in blur test The svelte-check type checker flags `window.__blur_test_result` as an error since the property doesn't exist on the Window type. Use the same `/** @type {any} */ (window)` cast pattern used in the test file. Co-Authored-By: Claude Opus 4.6 --- .../blur-during-navigation/page-with-input/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/test/apps/basics/src/routes/accessibility/blur-during-navigation/page-with-input/+page.svelte b/packages/kit/test/apps/basics/src/routes/accessibility/blur-during-navigation/page-with-input/+page.svelte index e35f82e2abfc..3bba40880950 100644 --- a/packages/kit/test/apps/basics/src/routes/accessibility/blur-during-navigation/page-with-input/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/accessibility/blur-during-navigation/page-with-input/+page.svelte @@ -5,7 +5,7 @@ // Without the fix, data was already nulled when blur fired during // navigation, causing "Cannot read properties of undefined". // We write to window so the result survives component teardown. - window.__blur_test_result = data.message; + /** @type {any} */ (window).__blur_test_result = data.message; }