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..3bba40880950 --- /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', () => {