Skip to content

Commit 75e2c6f

Browse files
axwalkerdummdidumm
andauthored
feat: add data-sveltekit-replacestate and -keepfocus options to links (#9019)
Closes #9014 Closes #7895 --------- Co-authored-by: Simon H <[email protected]> Co-authored-by: Simon Holthausen <[email protected]>
1 parent 29ffc78 commit 75e2c6f

File tree

38 files changed

+207
-137
lines changed

38 files changed

+207
-137
lines changed

.changeset/odd-suits-lick.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': minor
3+
---
4+
5+
feat: add data-sveltekit-keepfocus and data-sveltekit-replacestate options to links (requires Svelte version 3.56 for type-checking with `svelte-check`)

documentation/docs/20-core-concepts/30-form-actions.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -496,8 +496,8 @@ Some forms don't need to `POST` data to the server — search inputs, for exampl
496496
</form>
497497
```
498498

499-
Submitting this form will navigate to `/search?q=...` and invoke your load function but will not invoke an action. As with `<a>` elements, you can set the [`data-sveltekit-reload`](link-options#data-sveltekit-reload) and [`data-sveltekit-noscroll`](link-options#data-sveltekit-noscroll) attributes on the `<form>` to control the router's behaviour.
499+
Submitting this form will navigate to `/search?q=...` and invoke your load function but will not invoke an action. As with `<a>` elements, you can set the [`data-sveltekit-reload`](link-options#data-sveltekit-reload), [`data-sveltekit-replacestate`](link-options#data-sveltekit-replacestate), [`data-sveltekit-keepfocus`](link-options#data-sveltekit-keepfocus) and [`data-sveltekit-noscroll`](link-options#data-sveltekit-noscroll) attributes on the `<form>` to control the router's behaviour.
500500

501501
## Further reading
502502

503-
- [Tutorial: Forms](https://learn.svelte.dev/tutorial/the-form-element)
503+
- [Tutorial: Forms](https://learn.svelte.dev/tutorial/the-form-element)

documentation/docs/30-advanced/30-link-options.md

+24
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,30 @@ Occasionally, we need to tell SvelteKit not to handle a link, but allow the brow
6666

6767
Links with a `rel="external"` attribute will receive the same treatment. In addition, they will be ignored during [prerendering](page-options#prerender).
6868

69+
## data-sveltekit-replacestate
70+
71+
Sometimes you don't want navigation to create a new entry in the browser's session history. Adding a `data-sveltekit-replacestate` attribute to a link...
72+
73+
```html
74+
<a data-sveltekit-replacestate href="/path">Path</a>
75+
```
76+
77+
...will replace the current `history` entry rather than creating a new one with `pushState` when the link is clicked.
78+
79+
## data-sveltekit-keepfocus
80+
81+
When creating a search input using a `<form>` which reflects its input value in the URL, you might not want it to lose focus after navigation. Adding a `data-sveltekit-keepfocus` attribute to it...
82+
83+
```html
84+
<form data-sveltekit-keepfocus>
85+
<input type="text" name="query">
86+
</form>
87+
```
88+
89+
...will cause the currently focused element to retain focus after navigation. Note that this only really makes sense for `<form>` elements. If you would add this to a link, the focused element would be the `<a>` tag, which is probably not what you want. You should also only use this on elements that still exist after navigation.
90+
91+
By default, focus will be reset to the body.
92+
6993
## data-sveltekit-noscroll
7094

7195
When navigating to internal links, SvelteKit mirrors the browser's default navigation behaviour: it will change the scroll position to 0,0 so that the user is at the very top left of the page (unless the link includes a `#hash`, in which case it will scroll to the element with a matching ID).

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"playwright": "^1.29.2",
3838
"prettier": "^2.8.0",
3939
"rollup": "^3.7.0",
40-
"svelte": "^3.55.1",
40+
"svelte": "^3.56.0",
4141
"tiny-glob": "^0.2.9",
4242
"typescript": "^4.9.4"
4343
},

packages/adapter-static/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"@sveltejs/kit": "workspace:^",
3333
"@types/node": "^16.18.6",
3434
"sirv": "^2.0.2",
35-
"svelte": "^3.55.1",
35+
"svelte": "^3.56.0",
3636
"typescript": "^4.9.4",
3737
"uvu": "^0.5.6",
3838
"vite": "^4.1.1"

packages/adapter-static/test/apps/prerendered/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
},
1010
"devDependencies": {
1111
"@sveltejs/kit": "workspace:^",
12-
"svelte": "^3.55.1",
12+
"svelte": "^3.56.0",
1313
"vite": "^4.1.1"
1414
},
1515
"type": "module"

packages/adapter-static/test/apps/spa/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"@sveltejs/adapter-node": "workspace:^",
1212
"@sveltejs/kit": "workspace:^",
1313
"sirv-cli": "^2.0.2",
14-
"svelte": "^3.55.1",
14+
"svelte": "^3.56.0",
1515
"vite": "^4.1.1"
1616
},
1717
"type": "module"

packages/create-svelte/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"prettier": "^2.8.0",
2424
"prettier-plugin-svelte": "^2.8.1",
2525
"sucrase": "^3.29.0",
26-
"svelte": "^3.55.1",
26+
"svelte": "^3.56.0",
2727
"tiny-glob": "^0.2.9",
2828
"uvu": "^0.5.6"
2929
},

packages/create-svelte/templates/default/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"@neoconfetti/svelte": "^1.0.0",
1212
"@sveltejs/adapter-auto": "workspace:*",
1313
"@sveltejs/kit": "workspace:*",
14-
"svelte": "^3.55.1",
14+
"svelte": "^3.56.0",
1515
"typescript": "^4.9.4",
1616
"vite": "^4.1.1"
1717
},

packages/kit/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"@types/set-cookie-parser": "^2.4.2",
3535
"marked": "^4.2.3",
3636
"rollup": "^3.7.0",
37-
"svelte": "^3.55.1",
37+
"svelte": "^3.56.0",
3838
"svelte-preprocess": "^5.0.0",
3939
"typescript": "^4.9.4",
4040
"uvu": "^0.5.6",

packages/kit/src/runtime/client/client.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -1569,11 +1569,11 @@ export function create_client(app, target) {
15691569
navigate({
15701570
url,
15711571
scroll: options.noscroll ? scroll_state() : null,
1572-
keepfocus: false,
1572+
keepfocus: options.keep_focus ?? false,
15731573
redirect_chain: [],
15741574
details: {
15751575
state: {},
1576-
replaceState: url.href === location.href
1576+
replaceState: options.replace_state ?? url.href === location.href
15771577
},
15781578
accepted: () => event.preventDefault(),
15791579
blocked: () => event.preventDefault(),
@@ -1604,7 +1604,7 @@ export function create_client(app, target) {
16041604

16051605
const event_form = /** @type {HTMLFormElement} */ (event.target);
16061606

1607-
const { noscroll, reload } = get_router_options(event_form);
1607+
const { keep_focus, noscroll, reload, replace_state } = get_router_options(event_form);
16081608
if (reload) return;
16091609

16101610
event.preventDefault();
@@ -1623,11 +1623,11 @@ export function create_client(app, target) {
16231623
navigate({
16241624
url,
16251625
scroll: noscroll ? scroll_state() : null,
1626-
keepfocus: false,
1626+
keepfocus: keep_focus ?? false,
16271627
redirect_chain: [],
16281628
details: {
16291629
state: {},
1630-
replaceState: false
1630+
replaceState: replace_state ?? url.href === location.href
16311631
},
16321632
nav_token: {},
16331633
accepted: () => {},

packages/kit/src/runtime/client/utils.js

+14-2
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ const warned = new WeakSet();
3232
const valid_link_options = /** @type {const} */ ({
3333
'preload-code': ['', 'off', 'tap', 'hover', 'viewport', 'eager'],
3434
'preload-data': ['', 'off', 'tap', 'hover'],
35+
keepfocus: ['', 'off'],
3536
noscroll: ['', 'off'],
36-
reload: ['', 'off']
37+
reload: ['', 'off'],
38+
replacestate: ['', 'off']
3739
});
3840

3941
/**
@@ -141,6 +143,9 @@ export function get_link_info(a, base) {
141143
* @param {HTMLFormElement | HTMLAnchorElement | SVGAElement} element
142144
*/
143145
export function get_router_options(element) {
146+
/** @type {ValidLinkOptions<'keepfocus'> | null} */
147+
let keep_focus = null;
148+
144149
/** @type {ValidLinkOptions<'noscroll'> | null} */
145150
let noscroll = null;
146151

@@ -153,23 +158,30 @@ export function get_router_options(element) {
153158
/** @type {ValidLinkOptions<'reload'> | null} */
154159
let reload = null;
155160

161+
/** @type {ValidLinkOptions<'replacestate'> | null} */
162+
let replace_state = null;
163+
156164
/** @type {Element} */
157165
let el = element;
158166

159167
while (el && el !== document.documentElement) {
160168
if (preload_code === null) preload_code = link_option(el, 'preload-code');
161169
if (preload_data === null) preload_data = link_option(el, 'preload-data');
170+
if (keep_focus === null) keep_focus = link_option(el, 'keepfocus');
162171
if (noscroll === null) noscroll = link_option(el, 'noscroll');
163172
if (reload === null) reload = link_option(el, 'reload');
173+
if (replace_state === null) replace_state = link_option(el, 'replacestate');
164174

165175
el = /** @type {Element} */ (parent_element(el));
166176
}
167177

168178
return {
169179
preload_code: levels[preload_code ?? 'off'],
170180
preload_data: levels[preload_data ?? 'off'],
181+
keep_focus: keep_focus === 'off' ? false : keep_focus === '' ? true : null,
171182
noscroll: noscroll === 'off' ? false : noscroll === '' ? true : null,
172-
reload: reload === 'off' ? false : reload === '' ? true : null
183+
reload: reload === 'off' ? false : reload === '' ? true : null,
184+
replace_state: replace_state === 'off' ? false : replace_state === '' ? true : null
173185
};
174186
}
175187

packages/kit/test/apps/amp/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"@sveltejs/kit": "workspace:^",
1717
"cross-env": "^7.0.3",
1818
"purify-css": "^1.2.5",
19-
"svelte": "^3.55.1",
19+
"svelte": "^3.56.0",
2020
"svelte-check": "^3.0.2",
2121
"typescript": "^4.9.4",
2222
"vite": "^4.1.1"

packages/kit/test/apps/basics/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"@sveltejs/kit": "workspace:^",
1818
"cross-env": "^7.0.3",
1919
"rimraf": "^4.0.0",
20-
"svelte": "^3.55.1",
20+
"svelte": "^3.56.0",
2121
"svelte-check": "^3.0.2",
2222
"typescript": "^4.9.4",
2323
"vite": "^4.1.1"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<a id="one" href="/data-sveltekit/replacestate/target" data-sveltekit-replacestate>one</a>
2+
3+
<div data-sveltekit-replacestate>
4+
<a id="two" href="/data-sveltekit/replacestate/target">two</a>
5+
<a id="three" href="/data-sveltekit/replacestate/target" data-sveltekit-replacestate="off">
6+
three
7+
</a>
8+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<h1>target</h1>

packages/kit/test/apps/basics/test/client.test.js

+20
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,26 @@ test.describe('data-sveltekit attributes', () => {
608608
await clicknav('#three');
609609
expect(await page.evaluate(() => window.scrollY)).toBe(0);
610610
});
611+
612+
test('data-sveltekit-replacestate', async ({ page, clicknav }) => {
613+
await page.goto('/');
614+
await page.goto('/data-sveltekit/replacestate');
615+
await clicknav('#one');
616+
await page.goBack();
617+
await expect(page).not.toHaveURL(/replacestate/);
618+
619+
await page.goto('/');
620+
await page.goto('/data-sveltekit/replacestate');
621+
await clicknav('#two');
622+
await page.goBack();
623+
await expect(page).not.toHaveURL(/replacestate/);
624+
625+
await page.goto('/');
626+
await page.goto('/data-sveltekit/replacestate');
627+
await clicknav('#three');
628+
await page.goBack();
629+
await expect(page).toHaveURL(/replacestate$/);
630+
});
611631
});
612632

613633
test.describe('Content negotiation', () => {

packages/kit/test/apps/dev-only/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"devDependencies": {
1313
"@sveltejs/kit": "workspace:^",
1414
"cross-env": "^7.0.3",
15-
"svelte": "^3.55.1",
15+
"svelte": "^3.56.0",
1616
"svelte-check": "^3.0.2",
1717
"typescript": "^4.9.4",
1818
"vite": "^4.1.1"

packages/kit/test/apps/options-2/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"@sveltejs/adapter-node": "workspace:^",
1616
"@sveltejs/kit": "workspace:^",
1717
"cross-env": "^7.0.3",
18-
"svelte": "^3.55.1",
18+
"svelte": "^3.56.0",
1919
"svelte-check": "^3.0.2",
2020
"typescript": "^4.9.4",
2121
"vite": "^4.1.1"

packages/kit/test/apps/options/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"devDependencies": {
1515
"@sveltejs/kit": "workspace:^",
1616
"cross-env": "^7.0.3",
17-
"svelte": "^3.55.1",
17+
"svelte": "^3.56.0",
1818
"svelte-check": "^3.0.2",
1919
"typescript": "^4.9.4",
2020
"vite": "^4.1.1"

packages/kit/test/apps/writes/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"@sveltejs/kit": "workspace:^",
1616
"cross-env": "^7.0.3",
1717
"rimraf": "^4.0.0",
18-
"svelte": "^3.55.1",
18+
"svelte": "^3.56.0",
1919
"svelte-check": "^3.0.2",
2020
"typescript": "^4.9.4",
2121
"vite": "^4.1.1"

packages/kit/test/build-errors/apps/prerenderable-incorrect-fragment/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"devDependencies": {
1212
"@sveltejs/adapter-auto": "workspace:^",
1313
"@sveltejs/kit": "workspace:^",
14-
"svelte": "^3.55.1",
14+
"svelte": "^3.56.0",
1515
"svelte-check": "^3.0.2",
1616
"typescript": "^4.9.4",
1717
"vite": "^4.1.1"

packages/kit/test/build-errors/apps/prerenderable-not-prerendered/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"devDependencies": {
1212
"@sveltejs/adapter-auto": "workspace:^",
1313
"@sveltejs/kit": "workspace:^",
14-
"svelte": "^3.55.1",
14+
"svelte": "^3.56.0",
1515
"svelte-check": "^3.0.2",
1616
"typescript": "^4.9.4",
1717
"vite": "^4.1.1"

packages/kit/test/build-errors/apps/private-dynamic-env-dynamic-import/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
},
1212
"devDependencies": {
1313
"@sveltejs/kit": "workspace:^",
14-
"svelte": "^3.55.1",
14+
"svelte": "^3.56.0",
1515
"svelte-check": "^3.0.2",
1616
"typescript": "^4.9.4",
1717
"vite": "^4.1.1"

packages/kit/test/build-errors/apps/private-dynamic-env/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
},
1212
"devDependencies": {
1313
"@sveltejs/kit": "workspace:^",
14-
"svelte": "^3.55.1",
14+
"svelte": "^3.56.0",
1515
"svelte-check": "^3.0.2",
1616
"typescript": "^4.9.4",
1717
"vite": "^4.1.1"

packages/kit/test/build-errors/apps/private-static-env-dynamic-import/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
},
1212
"devDependencies": {
1313
"@sveltejs/kit": "workspace:^",
14-
"svelte": "^3.55.1",
14+
"svelte": "^3.56.0",
1515
"svelte-check": "^3.0.2",
1616
"typescript": "^4.9.4",
1717
"vite": "^4.1.1"

packages/kit/test/build-errors/apps/private-static-env/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"devDependencies": {
1313
"@sveltejs/kit": "workspace:^",
1414
"cross-env": "^7.0.3",
15-
"svelte": "^3.55.1",
15+
"svelte": "^3.56.0",
1616
"svelte-check": "^3.0.2",
1717
"typescript": "^4.9.4",
1818
"vite": "^4.1.1"

packages/kit/test/build-errors/apps/server-only-folder-dynamic-import/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
},
1212
"devDependencies": {
1313
"@sveltejs/kit": "workspace:^",
14-
"svelte": "^3.55.1",
14+
"svelte": "^3.56.0",
1515
"svelte-check": "^3.0.2",
1616
"typescript": "^4.9.4",
1717
"vite": "^4.1.1"

packages/kit/test/build-errors/apps/server-only-folder/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
},
1212
"devDependencies": {
1313
"@sveltejs/kit": "workspace:^",
14-
"svelte": "^3.55.1",
14+
"svelte": "^3.56.0",
1515
"svelte-check": "^3.0.2",
1616
"typescript": "^4.9.4",
1717
"vite": "^4.1.1"

packages/kit/test/build-errors/apps/server-only-module-dynamic-import/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
},
1212
"devDependencies": {
1313
"@sveltejs/kit": "workspace:^",
14-
"svelte": "^3.55.1",
14+
"svelte": "^3.56.0",
1515
"svelte-check": "^3.0.2",
1616
"typescript": "^4.9.4",
1717
"vite": "^4.1.1"

packages/kit/test/build-errors/apps/server-only-module/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
},
1212
"devDependencies": {
1313
"@sveltejs/kit": "workspace:^",
14-
"svelte": "^3.55.1",
14+
"svelte": "^3.56.0",
1515
"svelte-check": "^3.0.2",
1616
"typescript": "^4.9.4",
1717
"vite": "^4.1.1"

packages/kit/test/build-errors/apps/syntax-error/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
},
1010
"devDependencies": {
1111
"@sveltejs/kit": "workspace:^",
12-
"svelte": "^3.55.1",
12+
"svelte": "^3.56.0",
1313
"svelte-check": "^3.0.2",
1414
"typescript": "^4.9.4",
1515
"vite": "^4.1.1"

0 commit comments

Comments
 (0)