Skip to content

Commit 6736bcd

Browse files
committed
Fix cross-route fragment scrolling
1 parent 9d3acf6 commit 6736bcd

3 files changed

Lines changed: 32 additions & 2 deletions

File tree

.changeset/calm-anchors-scroll.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+
Scroll cross-route URL fragments to matching IDs or named anchors after the destination route commits.

src/router.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,26 @@ function isInScope(href) {
2323
);
2424
}
2525

26+
/** @param {string} url */
27+
function scrollToUrl(url) {
28+
const hash = new URL(url, location.origin).hash;
29+
if (hash) {
30+
try {
31+
const fragment = decodeURIComponent(hash.slice(1));
32+
const target = document.getElementById(fragment) ||
33+
Array.from(document.getElementsByName(fragment)).find(
34+
element => element.localName == 'a'
35+
);
36+
if (target) {
37+
target.scrollIntoView();
38+
return;
39+
}
40+
} catch {}
41+
}
42+
43+
scrollTo(0, 0);
44+
}
45+
2646
/**
2747
* @param {string} state
2848
* @param {MouseEvent | PopStateEvent | { url: string, replace?: boolean }} action
@@ -274,7 +294,7 @@ export function Router(props) {
274294

275295
// The route is loaded and rendered.
276296
if (prevRoute.current !== path) {
277-
if (wasPush) scrollTo(0, 0);
297+
if (wasPush) scrollToUrl(url);
278298
if (props.onRouteChange) props.onRouteChange(url);
279299

280300
prevRoute.current = path;

test/router.test.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -775,13 +775,15 @@ describe('Router', () => {
775775
scrollTo.restore();
776776
});
777777

778-
it('should ignore clicks on document fragment links', async () => {
778+
it('should handle document fragment links', async () => {
779779
const pushState = sinon.spy(history, 'pushState');
780+
const scrollIntoView = sinon.spy(Element.prototype, 'scrollIntoView');
780781

781782
const Route = sinon.fake(
782783
() => <div>
783784
<a href="#foo">just #foo</a>
784785
<a href="/other#bar">other #bar</a>
786+
<a name="bar">bar target</a>
785787
</div>
786788
);
787789
render(
@@ -815,8 +817,11 @@ describe('Router', () => {
815817
expect(loc).to.deep.include({ url: '/other#bar', path: '/other' });
816818
expect(pushState).to.have.been.called;
817819
expect(location.hash).to.equal('#bar');
820+
expect(scrollIntoView).to.have.been.calledOnce;
821+
expect(scrollIntoView).to.have.been.calledOn(scratch.querySelector('a[name="bar"]'));
818822

819823
pushState.restore();
824+
scrollIntoView.restore();
820825
});
821826

822827
it('should ignore clicks on download links', async () => {

0 commit comments

Comments
 (0)