Skip to content

Commit 34f18ae

Browse files
authored
[fix] reset scroll when navigated from scrolled page (#2735)
1 parent 7d7fdc6 commit 34f18ae

File tree

8 files changed

+87
-41
lines changed

8 files changed

+87
-41
lines changed

.changeset/wet-papayas-live.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
[fix] reset scroll when navigated from scrolled page

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

+27-21
Original file line numberDiff line numberDiff line change
@@ -275,28 +275,34 @@ export class Renderer {
275275
this._init(navigation_result);
276276
}
277277

278-
if (!opts?.keepfocus) {
279-
document.body.focus();
280-
}
278+
if (!opts) {
279+
await 0;
280+
} else {
281+
const { hash, scroll, keepfocus } = opts;
281282

282-
await 0;
283-
284-
// After `await 0`, the onMount() function in the component executed.
285-
// If there was no scrolling happening (checked via pageYOffset),
286-
// continue on our custom scroll handling
287-
if (pageYOffset === 0 && opts) {
288-
const { hash, scroll } = opts;
289-
290-
const deep_linked = hash && document.getElementById(hash.slice(1));
291-
if (scroll) {
292-
scrollTo(scroll.x, scroll.y);
293-
} else if (deep_linked) {
294-
// Here we use `scrollIntoView` on the element instead of `scrollTo`
295-
// because it natively supports the `scroll-margin` and `scroll-behavior`
296-
// CSS properties.
297-
deep_linked.scrollIntoView();
298-
} else {
299-
scrollTo(0, 0);
283+
if (!keepfocus) {
284+
document.body.focus();
285+
}
286+
287+
const oldPageYOffset = pageYOffset;
288+
await 0;
289+
const maxPageYOffset = document.body.scrollHeight - innerHeight;
290+
291+
// After `await 0`, the `onMount()` function in the component executed.
292+
// If there was no scrolling happening (checked via `pageYOffset`),
293+
// continue on our custom scroll handling
294+
if (pageYOffset === Math.min(oldPageYOffset, maxPageYOffset)) {
295+
const deep_linked = hash && document.getElementById(hash.slice(1));
296+
if (scroll) {
297+
scrollTo(scroll.x, scroll.y);
298+
} else if (deep_linked) {
299+
// Here we use `scrollIntoView` on the element instead of `scrollTo`
300+
// because it natively supports the `scroll-margin` and `scroll-behavior`
301+
// CSS properties.
302+
deep_linked.scrollIntoView();
303+
} else {
304+
scrollTo(0, 0);
305+
}
300306
}
301307
}
302308

packages/kit/test/apps/basics/src/routes/anchor-with-manual-scroll/_tests.js

+4-6
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,20 @@ export default function (test) {
99
test(
1010
'url-supplied anchor is ignored with onMount() scrolling on direct page load',
1111
'/anchor-with-manual-scroll/anchor#go-to-element',
12-
async ({ page, js }) => {
12+
async ({ is_intersecting_viewport, js }) => {
1313
if (js) {
14-
const p = await page.$('#abcde');
15-
assert.ok(p && (await p.isVisible()));
14+
assert.ok(is_intersecting_viewport('#abcde'));
1615
}
1716
}
1817
);
1918

2019
test(
2120
'url-supplied anchor is ignored with onMount() scrolling on navigation to page',
2221
'/anchor-with-manual-scroll',
23-
async ({ page, clicknav, js }) => {
22+
async ({ clicknav, is_intersecting_viewport, js }) => {
2423
await clicknav('[href="/anchor-with-manual-scroll/anchor#go-to-element"]');
2524
if (js) {
26-
const p = await page.$('#abcde');
27-
assert.ok(p && (await p.isVisible()));
25+
assert.ok(is_intersecting_viewport('#abcde'));
2826
}
2927
}
3028
);

packages/kit/test/apps/basics/src/routes/anchor/_tests.js

+16-7
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,31 @@ export default function (test) {
99
test(
1010
'url-supplied anchor works on direct page load',
1111
'/anchor/anchor#go-to-element',
12-
async ({ page, js }) => {
12+
async ({ is_intersecting_viewport, js }) => {
1313
if (js) {
14-
const p = await page.$('#go-to-element');
15-
assert.ok(p && (await p.isVisible()));
14+
assert.ok(is_intersecting_viewport('#go-to-element'));
1615
}
1716
}
1817
);
1918

2019
test(
2120
'url-supplied anchor works on navigation to page',
2221
'/anchor',
23-
async ({ page, clicknav, js }) => {
24-
await clicknav('[href="/anchor/anchor#go-to-element"]');
22+
async ({ clicknav, is_intersecting_viewport, js }) => {
23+
await clicknav('#first-anchor');
2524
if (js) {
26-
const p = await page.$('#go-to-element');
27-
assert.ok(p && (await p.isVisible()));
25+
assert.ok(is_intersecting_viewport('#go-to-element'));
26+
}
27+
}
28+
);
29+
30+
test(
31+
'url-supplied anchor works when navigated from scrolled page',
32+
'/anchor',
33+
async ({ clicknav, is_intersecting_viewport, js }) => {
34+
await clicknav('#second-anchor');
35+
if (js) {
36+
assert.ok(is_intersecting_viewport('#go-to-element'));
2837
}
2938
}
3039
);

packages/kit/test/apps/basics/src/routes/anchor/index.svelte

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<h1>Welcome to a test project</h1>
2-
<a href="/anchor/anchor#go-to-element">Anchor demo</a>
2+
<a id="first-anchor" href="/anchor/anchor#go-to-element">Anchor demo</a>
3+
<div>Spacing</div>
4+
<a id="second-anchor" href="/anchor/anchor#go-to-element">Anchor demo below</a>
35

46
<style>
57
:global(body) {
@@ -12,4 +14,9 @@
1214
display: block;
1315
margin: 20px;
1416
}
17+
18+
div {
19+
background-color: hotpink;
20+
height: 180vh;
21+
}
1522
</style>

packages/kit/test/apps/basics/src/routes/use-action/_tests.js

+4-6
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@ export default function (test) {
99
test(
1010
'app-supplied scroll and focus work on direct page load',
1111
'/use-action/focus-and-scroll',
12-
async ({ page, js }) => {
12+
async ({ page, is_intersecting_viewport, js }) => {
1313
if (js) {
14-
const input = await page.$('#input');
15-
assert.ok(input && (await input.isVisible()));
14+
assert.ok(await is_intersecting_viewport('#input'));
1615
assert.ok(await page.$eval('#input', (el) => el === document.activeElement));
1716
}
1817
}
@@ -21,11 +20,10 @@ export default function (test) {
2120
test(
2221
'app-supplied scroll and focus work on navigation to page',
2322
'/use-action',
24-
async ({ page, clicknav, js }) => {
23+
async ({ page, clicknav, is_intersecting_viewport, js }) => {
2524
await clicknav('[href="/use-action/focus-and-scroll"]');
2625
if (js) {
27-
const input = await page.$('#input');
28-
assert.ok(input && (await input.isVisible()));
26+
assert.ok(await is_intersecting_viewport('#input'));
2927
assert.ok(await page.$eval('#input', (el) => el === document.activeElement));
3028
}
3129
}

packages/kit/test/test.js

+19
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,10 @@ function duplicate(test_fn, config, is_build) {
181181
page: context.pages.nojs,
182182
clicknav: (selector) => context.pages.nojs.click(selector),
183183
back: () => context.pages.nojs.goBack().then(() => void 0),
184+
is_intersecting_viewport: async () => {
185+
console.warn('is_intersecting_viewport is not supported in nojs mode');
186+
return false;
187+
},
184188
// @ts-expect-error
185189
response,
186190
js: false
@@ -254,6 +258,21 @@ function duplicate(test_fn, config, is_build) {
254258
context.pages.js.evaluate(() => window.navigated)
255259
]);
256260
},
261+
// Reference from Puppeteer: https://github.com./puppeteer/puppeteer/blob/943477cc1eb4b129870142873b3554737d5ef252/experimental/puppeteer-firefox/lib/JSHandle.js#L190-L204
262+
is_intersecting_viewport: async (selector) => {
263+
return await context.pages.js.$eval(selector, async (element) => {
264+
const visibleRatio = await new Promise((resolve) => {
265+
const observer = new IntersectionObserver((entries) => {
266+
resolve(entries[0].intersectionRatio);
267+
observer.disconnect();
268+
});
269+
observer.observe(element);
270+
// Firefox doesn't call IntersectionObserver callback unless there are rafs
271+
requestAnimationFrame(() => {});
272+
});
273+
return visibleRatio > 0;
274+
});
275+
},
257276
js: true,
258277
// @ts-expect-error
259278
response

packages/kit/test/types.d.ts

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ export interface TestContext {
1717
response: PlaywrightResponse;
1818
clicknav(selector: string): Promise<void>;
1919
back(): Promise<void>;
20+
/**
21+
* Only supported in js mode
22+
*/
23+
is_intersecting_viewport(selector: string): Promise<boolean>;
2024
fetch(url: RequestInfo, opts?: RequestInit): Promise<NodeFetchResponse>;
2125
capture_requests(fn: () => Promise<void>): Promise<string[]>;
2226
errors(): string;

0 commit comments

Comments
 (0)