Skip to content

Commit f3bd85d

Browse files
MathiasWPclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 355e797 commit f3bd85d

File tree

5 files changed

+48
-0
lines changed

5 files changed

+48
-0
lines changed

packages/kit/src/runtime/client/client.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1739,6 +1739,16 @@ async function navigate({
17391739
navigation_result.props.page.url = url;
17401740
}
17411741

1742+
// Remove focus before updating the component tree, so that blur/focusout
1743+
// handlers fire while the old component's data is still valid (#14575)
1744+
if (
1745+
!keepfocus &&
1746+
document.activeElement instanceof HTMLElement &&
1747+
document.activeElement !== document.body
1748+
) {
1749+
document.activeElement.blur();
1750+
}
1751+
17421752
const fork = load_cache_fork && (await load_cache_fork);
17431753

17441754
if (fork) {
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<h1>Other page</h1>
2+
<a href="/accessibility/blur-during-navigation/page-with-input">Back</a>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function load() {
2+
return { message: 'hello' };
3+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script>
2+
export let data;
3+
4+
function handleBlur() {
5+
// Without the fix, data was already nulled when blur fired during
6+
// navigation, causing "Cannot read properties of undefined".
7+
// We write to window so the result survives component teardown.
8+
window.__blur_test_result = data.message;
9+
}
10+
</script>
11+
12+
<h1>Blur test</h1>
13+
<input id="blur-input" on:blur={handleBlur} />
14+
<a href="/accessibility/blur-during-navigation/other">Go to other</a>

packages/kit/test/apps/basics/test/cross-platform/client.test.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,25 @@ test.describe('a11y', () => {
122122
).toBe('BODY');
123123
expect(await page.evaluate(() => document.activeElement?.nodeName)).toBe('BODY');
124124
});
125+
126+
test('blur handler can access data during navigation', async ({ page, app }) => {
127+
const errors = /** @type {string[]} */ ([]);
128+
page.on('pageerror', (err) => errors.push(err.message));
129+
130+
await page.goto('/accessibility/blur-during-navigation/page-with-input');
131+
132+
// Focus the input
133+
await page.locator('#blur-input').focus();
134+
135+
// Navigate away — this triggers blur on the focused input.
136+
// Without the fix, data would be nulled before blur fired, causing a TypeError.
137+
await app.goto('/accessibility/blur-during-navigation/other');
138+
139+
expect(errors).toEqual([]);
140+
141+
// The blur handler should have been able to read data.message
142+
expect(await page.evaluate(() => /** @type {any} */ (window).__blur_test_result)).toBe('hello');
143+
});
125144
});
126145

127146
test.describe('Navigation lifecycle functions', () => {

0 commit comments

Comments
 (0)