Skip to content
5 changes: 5 additions & 0 deletions .changeset/big-pants-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: hash links to new pages focuses the correct element
38 changes: 23 additions & 15 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -1146,6 +1146,8 @@ export function create_client(app, target) {
reset_focus();
}

console.log(document.activeElement);

autoscroll = true;

if (navigation_result.props.page) {
Expand Down Expand Up @@ -1962,22 +1964,28 @@ function reset_focus() {
autofocus.focus();
} else {
// Reset page selection and focus
// We try to mimic browsers' behaviour as closely as possible by targeting the
// first scrollable region, but unfortunately it's not a perfect match — e.g.
// shift-tabbing won't immediately cycle up from the end of the page on Chromium
// See https://html.spec.whatwg.org/multipage/interaction.html#get-the-focusable-area
const root = document.body;
const tabindex = root.getAttribute('tabindex');

root.tabIndex = -1;
// @ts-expect-error
root.focus({ preventScroll: true, focusVisible: false });

// restore `tabindex` as to prevent `root` from stealing input from elements
if (tabindex !== null) {
root.setAttribute('tabindex', tabindex);
// Mimic browsers' behaviour and set the sequential focus navigation starting point
// to the fragment identifier
if (location.hash) {
location.replace(location.hash);
} else {
root.removeAttribute('tabindex');
// We try to mimic browsers' behaviour as closely as possible by targeting the
// first scrollable region, but unfortunately it's not a perfect match — e.g.
// shift-tabbing won't immediately cycle up from the end of the page on Chromium
// See https://html.spec.whatwg.org/multipage/interaction.html#get-the-focusable-area
const root = document.body;
const tabindex = root.getAttribute('tabindex');

root.tabIndex = -1;
// @ts-expect-error
root.focus({ preventScroll: true, focusVisible: false });

// restore `tabindex` as to prevent `root` from stealing input from elements
if (tabindex !== null) {
root.setAttribute('tabindex', tabindex);
} else {
root.removeAttribute('tabindex');
}
}

// capture current selection, so we can compare the state after
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<a href="/routing/focus/a#p">click me!</a>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<button>button 1</button>
<button>button 2</button>
<p id="p">cannot be focused</p>
<button id="button3">button 3</button>
12 changes: 12 additions & 0 deletions packages/kit/test/apps/basics/test/cross-platform/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,18 @@ test.describe('Routing', () => {
expect(await page.textContent('#page-url-hash')).toBe('#target');
});

test('sequential focus navigation starting point is set correctly on navigation', async ({
page,
browserName
}) => {
const tab = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
await page.goto('/routing/focus');
await page.locator('[href="/routing/focus/a#p"]').click();
expect(await page.evaluate(() => (document.activeElement || {}).nodeName)).toBe('BODY');
await page.keyboard.press(tab);
await expect(page.locator('#button3')).toBeFocused();
});

test('back button returns to previous route when previous route has been navigated to via hash anchor', async ({
page,
clicknav
Expand Down