Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/grumpy-chefs-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: correctly set the sequential focus navigation point when using hash routing
5 changes: 5 additions & 0 deletions .changeset/long-cows-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: regression when navigating with hash routing
82 changes: 42 additions & 40 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -1602,11 +1602,7 @@ async function navigate({
const scroll = popped ? popped.scroll : noscroll ? scroll_state() : null;

if (autoscroll) {
const deep_linked =
url.hash &&
document.getElementById(
decodeURIComponent(app.hash ? (url.hash.split('#')[2] ?? '') : url.hash.slice(1))
);
const deep_linked = url.hash && document.getElementById(get_id(url));
if (scroll) {
scrollTo(scroll.x, scroll.y);
} else if (deep_linked) {
Expand All @@ -1627,7 +1623,7 @@ async function navigate({
document.activeElement !== document.body;

if (!keepfocus && !changed_focus) {
reset_focus();
reset_focus(url);
}

autoscroll = true;
Expand Down Expand Up @@ -2194,7 +2190,7 @@ export async function applyAction(result) {
root.$set(navigation_result.props);
update(navigation_result.props.page);

void tick().then(reset_focus);
void tick().then(() => reset_focus(url));
}
} else if (result.type === 'redirect') {
await _goto(result.location, { invalidateAll: true }, 0);
Expand All @@ -2215,7 +2211,7 @@ export async function applyAction(result) {
root.$set({ form: result.data });

if (result.type === 'success') {
reset_focus();
reset_focus(page.url);
}
}
}
Expand Down Expand Up @@ -2793,50 +2789,48 @@ function deserialize_uses(uses) {
};
}

function reset_focus() {
/**
* @param {URL} url
*/
function reset_focus(url) {
const autofocus = document.querySelector('[autofocus]');
if (autofocus) {
// @ts-ignore
autofocus.focus();
} else {
// Reset page selection and focus
if (location.hash && document.querySelector(location.hash)) {
const { x, y } = scroll_state();

setTimeout(() => {
const history_state = history.state;
// Mimic the browsers' behaviour and set the sequential focus navigation
// starting point to the fragment identifier
location.replace(location.hash);
// but Firefox has a bug that sets the history state as null so we
// need to restore the history state
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1199924
history.replaceState(history_state, '', location.hash);

// Scroll management has already happened earlier so we need to restore
// the scroll position after setting the sequential focus navigation starting point
scrollTo(x, y);
});
} else {
// 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;
const id = get_id(url);

// 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
// If there's a fragment identifier, we mimic the browsers' behaviour and set the
// sequential focus navigation starting point to it
const element = document.getElementById(id) ?? document.body;

const { x, y } = scroll_state();

// doing this in a `setTimeout` makes `.focus()` work on Firefox
setTimeout(() => {
const tabindex = element.getAttribute('tabindex');
element.tabIndex = -1;
// @ts-expect-error options.focusVisible is only supported in Firefox
// See https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#browser_compatibility
root.focus({ preventScroll: true, focusVisible: false });
element.focus({ preventScroll: true, focusVisible: false });

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

if (id) {
// Scroll management has already happened earlier so we need to restore
// the scroll position after setting the sequential focus navigation starting point
scrollTo(x, y);
}
});

// capture current selection, so we can compare the state after
// snapshot restoration and afterNavigate callbacks have run
Expand Down Expand Up @@ -2960,6 +2954,14 @@ function decode_hash(url) {
return new_url;
}

/**
* @param {URL} url
* @returns {string}
*/
function get_id(url) {
return decodeURIComponent(app.hash ? (url.hash.split('#')[2] ?? '') : url.hash.slice(1));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is url hash formatted like https://example.com/#<apphash>#<otherhash> ?

in that case, is it sure that apphash and otherhash both can't contain # char?

otherwise it would be better to take app.hash and remove it from the beginning of url hash if it is there 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could've sworn we have a similar function already (I haven't looked closer yet though)

}

if (DEV) {
// Nasty hack to silence harmless warnings the user can do nothing about
const console_warn = console.warn;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<a href="#/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>
11 changes: 11 additions & 0 deletions packages/kit/test/apps/hash-based-routing/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,15 @@ test.describe('hash based navigation', () => {
await page.goForward();
expect(page.locator('p')).toHaveText('b');
});

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