diff --git a/.changeset/chilled-cats-hang.md b/.changeset/chilled-cats-hang.md new file mode 100644 index 000000000000..47dae6ac5f68 --- /dev/null +++ b/.changeset/chilled-cats-hang.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: ensure element is focused after subsequent clicks of the same hash link diff --git a/.changeset/strange-buckets-sell.md b/.changeset/strange-buckets-sell.md new file mode 100644 index 000000000000..3cbaa6a7184c --- /dev/null +++ b/.changeset/strange-buckets-sell.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: avoid reloading behaviour for hash links with data-sveltekit-reload if the hash is on the same page diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 432c7165fb02..1a6695c385e9 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -2089,8 +2089,11 @@ function _start_router() { if (download) return; + const [nonhash, hash] = url.href.split('#'); + const same_pathname = nonhash === strip_hash(location); + // Ignore the following but fire beforeNavigate - if (external || options.reload) { + if (external || (options.reload && (!same_pathname || !hash))) { if (_before_navigate({ url, type: 'link' })) { // set `navigating` to `true` to prevent `beforeNavigate` callbacks // being called when the page unloads @@ -2105,8 +2108,7 @@ function _start_router() { // Check if new url only differs by hash and use the browser default behavior in that case // This will ensure the `hashchange` event is fired // Removing the hash does a full page navigation in the browser, so make sure a hash is present - const [nonhash, hash] = url.href.split('#'); - if (hash !== undefined && nonhash === strip_hash(location)) { + if (hash !== undefined && same_pathname) { // If we are trying to navigate to the same hash, we should only // attempt to scroll to that element and avoid any history changes. // Otherwise, this can cause Firefox to incorrectly assign a null @@ -2121,7 +2123,11 @@ function _start_router() { if (hash === '' || (hash === 'top' && a.ownerDocument.getElementById('top') === null)) { window.scrollTo({ top: 0 }); } else { - a.ownerDocument.getElementById(decodeURIComponent(hash))?.scrollIntoView(); + const element = a.ownerDocument.getElementById(decodeURIComponent(hash)); + if (element) { + element.scrollIntoView(); + element.focus(); + } } return; diff --git a/packages/kit/test/apps/basics/src/routes/data-sveltekit/reload/hash/+page.svelte b/packages/kit/test/apps/basics/src/routes/data-sveltekit/reload/hash/+page.svelte new file mode 100644 index 000000000000..ca54461e368f --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/data-sveltekit/reload/hash/+page.svelte @@ -0,0 +1,3 @@ +focus + +new page diff --git a/packages/kit/test/apps/basics/src/routes/data-sveltekit/reload/hash/new/+page.svelte b/packages/kit/test/apps/basics/src/routes/data-sveltekit/reload/hash/new/+page.svelte new file mode 100644 index 000000000000..48aa4cb69f99 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/data-sveltekit/reload/hash/new/+page.svelte @@ -0,0 +1 @@ +
hello world
diff --git a/packages/kit/test/apps/basics/src/routes/routing/hashes/focus/+page.svelte b/packages/kit/test/apps/basics/src/routes/routing/hashes/focus/+page.svelte new file mode 100644 index 000000000000..17954338b764 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/routing/hashes/focus/+page.svelte @@ -0,0 +1,2 @@ +focus + 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 c2e9bf6cc474..71338a8422ce 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 @@ -715,6 +715,30 @@ test.describe('Routing', () => { expect(await page.textContent('#page-url-hash')).toBe('#target'); }); + test('clicking on a hash link focuses the associated element', async ({ page }) => { + await page.goto('/routing/hashes/focus'); + await page.locator('a[href="#example"]').click(); + await expect(page.getByRole('textbox')).toBeFocused(); + // check it still works when the hash is already present in the URL + await page.locator('a[href="#example"]').click(); + await expect(page.getByRole('textbox')).toBeFocused(); + }); + + test('backwards navigation works after clicking a hash link with data-sveltekit-reload', async ({ + page, + clicknav, + baseURL + }) => { + await page.goto('/data-sveltekit/reload/hash'); + await page.locator('a[href="#example"]').click(); + expect(page.url()).toBe(`${baseURL}/data-sveltekit/reload/hash#example`); + await clicknav('a[href="/data-sveltekit/reload/hash/new"]'); + expect(page.url()).toBe(`${baseURL}/data-sveltekit/reload/hash/new`); + await page.goBack(); + expect(page.url()).toBe(`${baseURL}/data-sveltekit/reload/hash#example`); + await expect(page.getByRole('textbox')).toBeVisible(); + }); + test('back button returns to previous route when previous route has been navigated to via hash anchor', async ({ page, clicknav