Skip to content

Commit 0733298

Browse files
authored
Merge pull request #8797 from QwikDev/v2-scroll-action
fix: preserve scroll after spa action submits
2 parents 2a2e355 + 3f1d5d5 commit 0733298

5 files changed

Lines changed: 82 additions & 5 deletions

File tree

.changeset/route-action-scroll.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@qwik.dev/router": patch
3+
---
4+
5+
fix: preserve scroll after spa action submits
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { component$, useStylesScoped$ } from '@qwik.dev/core';
2+
import { Form, routeAction$ } from '@qwik.dev/router';
3+
4+
export const useScrollAction = routeAction$((form) => {
5+
return {
6+
submitted: form.value,
7+
};
8+
});
9+
10+
export default component$(() => {
11+
const action = useScrollAction();
12+
13+
useStylesScoped$(`
14+
.spacer {
15+
height: 900px;
16+
}
17+
18+
.tail {
19+
height: 1400px;
20+
}
21+
`);
22+
23+
return (
24+
<main>
25+
<h1 id="scroll-action-heading">Action Scroll</h1>
26+
<div class="spacer" />
27+
<Form action={action} id="scroll-action-form">
28+
<input type="hidden" name="value" value="keep-scroll" />
29+
<button id="scroll-action-submit">Submit Action</button>
30+
</Form>
31+
<p id="scroll-action-result">
32+
{action.value?.submitted ? `submitted: ${action.value.submitted}` : 'idle'}
33+
</p>
34+
<div class="tail" />
35+
</main>
36+
);
37+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { component$ } from '@qwik.dev/core';
2+
import { Link } from '@qwik.dev/router';
3+
4+
export default component$(() => {
5+
return (
6+
<main>
7+
<h1>Action Source</h1>
8+
<Link id="to-scroll-action" href="/qwikrouter-test/scroll-restoration/action-form/">
9+
To Action Form
10+
</Link>
11+
</main>
12+
);
13+
});

e2e/qwik-e2e/tests/qwikrouter/nav.e2e.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,30 @@ test.describe('nav', () => {
235235
);
236236
});
237237

238+
test('should preserve scroll when submitting routeAction$ after SPA navigation', async ({
239+
page,
240+
}) => {
241+
await page.goto('/qwikrouter-test/scroll-restoration/action-source/');
242+
await page.locator('#to-scroll-action').click();
243+
244+
await expect(page).toHaveURL('/qwikrouter-test/scroll-restoration/action-form/');
245+
await expect(page.locator('#scroll-action-heading')).toHaveText('Action Scroll');
246+
247+
await scrollTo(page, 0, 700);
248+
await expect.poll(async () => (await getWindowScrollXY(page))[1]).toBe(700);
249+
250+
await page.locator('#scroll-action-submit').click();
251+
252+
await expect(page.locator('#scroll-action-result')).toHaveText('submitted: keep-scroll');
253+
await page.evaluate(
254+
() =>
255+
new Promise<void>((resolve) => {
256+
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
257+
})
258+
);
259+
expect((await getWindowScrollXY(page))[1]).toBeGreaterThan(600);
260+
});
261+
238262
test('issue4502 (link)', async ({ page }) => {
239263
await page.goto('/qwikrouter-test/issue4502/');
240264
const count = page.locator('#count');

packages/qwik-router/src/runtime/src/qwik-router-component.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ export const useQwikRouter = (props?: QwikRouterProps) => {
257257
NoSerialize<{
258258
routeName: string;
259259
navType: NavigationType;
260+
prevUrl: URL;
260261
replaceState: boolean | undefined;
261262
shouldForcePrevUrl: boolean;
262263
shouldForceUrl: boolean;
@@ -709,6 +710,7 @@ export const useQwikRouter = (props?: QwikRouterProps) => {
709710
navContext.value = noSerialize({
710711
routeName: $routeName$,
711712
navType,
713+
prevUrl,
712714
replaceState,
713715
shouldForcePrevUrl,
714716
shouldForceUrl,
@@ -770,12 +772,8 @@ export const useQwikRouter = (props?: QwikRouterProps) => {
770772
const container = _getContextContainer();
771773
const navigation = routeInternal.untrackedValue;
772774

773-
const { navType, replaceState, routeName } = nav;
775+
const { navType, prevUrl, replaceState, routeName } = nav;
774776
const trackUrl = routeLocation.url;
775-
// prevUrl is only assigned when the path changes (see nav task). On the first SPA nav
776-
// after SSR, or on same-path/hash-only navs, prevUrl is undefined — fall back to
777-
// trackUrl so isSamePath() returns true and scroll/history logic no-ops correctly.
778-
const prevUrl = routeLocation.prevUrl ?? trackUrl;
779777

780778
const scroller = getScroller();
781779
// Scroll restore setup — must happen before navigation commits

0 commit comments

Comments
 (0)