diff --git a/.changeset/pushstate-page-url-key.md b/.changeset/pushstate-page-url-key.md new file mode 100644 index 000000000000..e7f031cb9af7 --- /dev/null +++ b/.changeset/pushstate-page-url-key.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: load the URL shown in the address bar when returning to a `pushState`/`replaceState` history entry diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index c02e1274509d..9082afe6ab50 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -2559,6 +2559,15 @@ export async function preloadCode(pathname) { return _preload_code(url); } +/** + * Resolves a shallow routing `url` argument, where `''` means the current URL. + * `new URL('', ...)` drops the fragment, which would lose the hash route or anchor. + * @param {string | URL} url + */ +function resolve_shallow_url(url) { + return url === '' ? new URL(location.href) : resolve_url(url); +} + /** * Programmatically create a new history entry with the given `page.state`. To use the current URL, you can pass `''` as the first argument. Used for [shallow routing](https://svelte.dev/docs/kit/shallow-routing). * @@ -2587,14 +2596,17 @@ export function pushState(url, state) { update_scroll_positions(current_history_index); + const resolved = resolve_shallow_url(url); + const opts = { [HISTORY_INDEX]: (current_history_index += 1), [NAVIGATION_INDEX]: current_navigation_index, - [PAGE_URL_KEY]: page.url.href, + // store the address bar URL, so returning to this entry loads what the user saw + [PAGE_URL_KEY]: resolved.href, [STATES_KEY]: state }; - history.pushState(opts, '', resolve_url(url)); + history.pushState(opts, '', resolved); has_navigated = true; page.state = state; @@ -2628,14 +2640,16 @@ export function replaceState(url, state) { } } + const resolved = resolve_shallow_url(url); + const opts = { [HISTORY_INDEX]: current_history_index, [NAVIGATION_INDEX]: current_navigation_index, - [PAGE_URL_KEY]: page.url.href, + [PAGE_URL_KEY]: resolved.href, [STATES_KEY]: state }; - history.replaceState(opts, '', resolve_url(url)); + history.replaceState(opts, '', resolved); page.state = state; } diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js index 143244dcba84..1bb16aa3fc00 100644 --- a/packages/kit/test/apps/basics/test/client.test.js +++ b/packages/kit/test/apps/basics/test/client.test.js @@ -1709,6 +1709,24 @@ test.describe('Shallow routing', () => { await expect(page.locator('p')).toHaveText('active: true'); }); + test('Loads the pushed URL when returning from a subsequent navigation', async ({ + baseURL, + page, + clicknav + }) => { + await page.goto('/shallow-routing/push-state'); + await page.locator('[data-id="two"]').click(); + expect(page.url()).toBe(`${baseURL}/shallow-routing/push-state/a`); + await expect(page.locator('h1')).toHaveText('parent'); + + await clicknav('a[href="/shallow-routing/push-state/b"]'); + await page.goBack(); + + expect(page.url()).toBe(`${baseURL}/shallow-routing/push-state/a`); + await expect(page.locator('h1')).toHaveText('a'); + await expect(page.locator('p')).toHaveText('active: true'); + }); + test('Replaces state on the current URL', async ({ baseURL, page, clicknav }) => { await page.goto('/shallow-routing/replace-state/b'); await clicknav('[href="/shallow-routing/replace-state"]'); @@ -1739,7 +1757,7 @@ test.describe('Shallow routing', () => { await page.goForward(); expect(page.url()).toBe(`${baseURL}/shallow-routing/replace-state/a`); - await expect(page.locator('h1')).toHaveText('parent'); + await expect(page.locator('h1')).toHaveText('a'); await expect(page.locator('p')).toHaveText('active: true'); }); diff --git a/packages/kit/test/apps/hash-based-routing/src/routes/+layout.svelte b/packages/kit/test/apps/hash-based-routing/src/routes/+layout.svelte index 76cae24dfaf8..00941c588f7a 100644 --- a/packages/kit/test/apps/hash-based-routing/src/routes/+layout.svelte +++ b/packages/kit/test/apps/hash-based-routing/src/routes/+layout.svelte @@ -17,6 +17,7 @@ /#/reroute-b + {@render children()} diff --git a/packages/kit/test/apps/hash-based-routing/test/test.js b/packages/kit/test/apps/hash-based-routing/test/test.js index 6801d80444d0..7d17a1b472c1 100644 --- a/packages/kit/test/apps/hash-based-routing/test/test.js +++ b/packages/kit/test/apps/hash-based-routing/test/test.js @@ -50,6 +50,23 @@ test.describe('hash based navigation', () => { expect(url.hash).toBe('#/a#b'); }); + test('pushState with empty url keeps the hash route', async ({ page }) => { + await page.goto('/#/a'); + await expect(page.locator('p')).toHaveText('a'); + + await page.locator('button[data-push-current]').click(); + let url = new URL(page.url()); + expect(url.hash).toBe('#/a'); + + await page.locator('a[href="/#/b"]').click(); + await expect(page.locator('p')).toHaveText('b'); + + await page.goBack(); + await expect(page.locator('p')).toHaveText('a'); + url = new URL(page.url()); + expect(url.hash).toBe('#/a'); + }); + test('navigates to correct page on load', async ({ page }) => { await page.goto('/#/a'); await expect(page.locator('p')).toHaveText('a');