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/pushstate-page-url-key.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: load the URL shown in the address bar when returning to a `pushState`/`replaceState` history entry
22 changes: 18 additions & 4 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
20 changes: 19 additions & 1 deletion packages/kit/test/apps/basics/test/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"]');
Expand Down Expand Up @@ -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');
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<a href="/#/reroute-b">/#/reroute-b</a>
<button data-goto onclick={() => goto('/#/b')}>goto /#/b</button>
<button data-push onclick={() => pushState('/#/b', {})}>pushState /#/b</button>
<button data-push-current onclick={() => pushState('', {})}>pushState ''</button>
<button data-replace onclick={() => replaceState('/#/a#b', {})}>replaceState /#/a#b</button>

{@render children()}
17 changes: 17 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 @@ -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');
Expand Down
Loading