Skip to content

Commit 8271c82

Browse files
authored
Merge pull request #496 from preactjs/iso-router-reset-scroll
[preact-iso] Router: reset scroll position when navigating forwards
2 parents 3954ddb + 9b425c5 commit 8271c82

3 files changed

Lines changed: 63 additions & 5 deletions

File tree

.changeset/rare-seals-burn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"preact-iso": patch
3+
---
4+
5+
[preact-iso] Router: reset page scroll position on forward navigations

packages/preact-iso/router.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
import { h, createContext, cloneElement } from 'preact';
22
import { useContext, useMemo, useReducer, useEffect, useLayoutEffect, useRef } from 'preact/hooks';
33

4+
let push;
45
const UPDATE = (state, url) => {
5-
let push = true;
6+
push = undefined;
67
if (url && url.type === 'click') {
78
const link = url.target.closest('a[href]');
89
if (!link || link.origin != location.origin || !/^(_?self)?$/i.test(link.target)) return state;
910

11+
push = true;
1012
url.preventDefault();
1113
url = link.href.replace(location.origin, '');
12-
} else if (typeof url !== 'string') {
14+
} else if (typeof url === 'string') {
15+
push = true;
16+
} else {
1317
url = location.pathname + location.search;
14-
push = undefined;
1518
}
1619

1720
if (push === true) history.pushState(null, '', url);
@@ -43,12 +46,13 @@ export const exec = (url, route, matches) => {
4346

4447
export function LocationProvider(props) {
4548
const [url, route] = useReducer(UPDATE, location.pathname + location.search);
49+
const wasPush = push === true;
4650

4751
const value = useMemo(() => {
4852
const u = new URL(url, location.origin);
4953
const path = u.pathname.replace(/(.)\/$/g, '$1');
5054
// @ts-ignore-next
51-
return { url, path, query: Object.fromEntries(u.searchParams), route };
55+
return { url, path, query: Object.fromEntries(u.searchParams), route, wasPush };
5256
}, [url]);
5357

5458
useEffect(() => {
@@ -70,7 +74,7 @@ export function Router(props) {
7074

7175
const loc = useLocation();
7276

73-
const { url, path, query } = loc;
77+
const { url, path, query, wasPush } = loc;
7478

7579
const cur = useRef(loc);
7680
const prev = useRef();
@@ -115,6 +119,7 @@ export function Router(props) {
115119
prev.current = prevChildren.current = pending.current = null;
116120
if (props.onLoadEnd) props.onLoadEnd(url);
117121
update(0);
122+
if (wasPush) scrollTo(0, 0);
118123
};
119124

120125
if (p) {

packages/preact-iso/test/router.test.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { html } from 'htm/preact';
44
import { LocationProvider, Router, useLocation } from '../router.js';
55
import lazy, { ErrorBoundary } from '../lazy.js';
66

7+
Object.defineProperty(window, 'scrollTo', { value() {} });
8+
79
const sleep = ms => new Promise(r => setTimeout(r, ms));
810

911
// delayed lazy()
@@ -299,4 +301,50 @@ describe('Router', () => {
299301
});
300302
}
301303
});
304+
305+
it('should scroll to top when navigating forward', async () => {
306+
const scrollTo = jest.spyOn(window, 'scrollTo');
307+
308+
const Route = jest.fn(() => html`<div style=${{ height: '1000px' }}><a href="/link">link</a></div>`);
309+
let loc;
310+
render(
311+
html`
312+
<${LocationProvider}>
313+
<${Router}>
314+
<${Route} default />
315+
<//>
316+
<${() => {
317+
loc = useLocation();
318+
}} />
319+
<//>
320+
`,
321+
scratch
322+
);
323+
324+
await sleep(20);
325+
326+
expect(scrollTo).not.toHaveBeenCalled();
327+
expect(Route).toHaveBeenCalledTimes(1);
328+
Route.mockClear();
329+
330+
loc.route('/programmatic');
331+
await sleep(10);
332+
expect(loc).toMatchObject({ url: '/programmatic' });
333+
expect(scrollTo).toHaveBeenCalledWith(0, 0);
334+
expect(scrollTo).toHaveBeenCalledTimes(1);
335+
expect(Route).toHaveBeenCalledTimes(1);
336+
Route.mockClear();
337+
scrollTo.mockClear();
338+
339+
scratch.querySelector('a').click();
340+
await sleep(10);
341+
expect(loc).toMatchObject({ url: '/link' });
342+
expect(scrollTo).toHaveBeenCalledWith(0, 0);
343+
expect(scrollTo).toHaveBeenCalledTimes(1);
344+
expect(Route).toHaveBeenCalledTimes(1);
345+
Route.mockClear();
346+
347+
await sleep(10);
348+
scrollTo.mockRestore();
349+
});
302350
});

0 commit comments

Comments
 (0)