diff --git a/.changeset/calm-anchors-scroll.md b/.changeset/calm-anchors-scroll.md new file mode 100644 index 0000000..853e5dd --- /dev/null +++ b/.changeset/calm-anchors-scroll.md @@ -0,0 +1,5 @@ +--- +'preact-iso': patch +--- + +Scroll cross-route URL fragments to matching IDs or named anchors after the destination route commits. diff --git a/src/router.js b/src/router.js index 5f27f21..be66a2a 100644 --- a/src/router.js +++ b/src/router.js @@ -23,6 +23,26 @@ function isInScope(href) { ); } +/** @param {string} url */ +function scrollToUrl(url) { + const hash = new URL(url, location.origin).hash; + if (hash) { + try { + const fragment = decodeURIComponent(hash.slice(1)); + const target = document.getElementById(fragment) || + Array.from(document.getElementsByName(fragment)).find( + element => element.localName == 'a' + ); + if (target) { + target.scrollIntoView(); + return; + } + } catch {} + } + + scrollTo(0, 0); +} + /** * @param {string} state * @param {MouseEvent | PopStateEvent | { url: string, replace?: boolean }} action @@ -274,7 +294,7 @@ export function Router(props) { // The route is loaded and rendered. if (prevRoute.current !== path) { - if (wasPush) scrollTo(0, 0); + if (wasPush) scrollToUrl(url); if (props.onRouteChange) props.onRouteChange(url); prevRoute.current = path; diff --git a/test/router.test.js b/test/router.test.js index a0e1a4a..feacfa6 100644 --- a/test/router.test.js +++ b/test/router.test.js @@ -775,13 +775,15 @@ describe('Router', () => { scrollTo.restore(); }); - it('should ignore clicks on document fragment links', async () => { + it('should handle document fragment links', async () => { const pushState = sinon.spy(history, 'pushState'); + const scrollIntoView = sinon.spy(Element.prototype, 'scrollIntoView'); const Route = sinon.fake( () =>
just #foo other #bar + bar target
); render( @@ -815,8 +817,11 @@ describe('Router', () => { expect(loc).to.deep.include({ url: '/other#bar', path: '/other' }); expect(pushState).to.have.been.called; expect(location.hash).to.equal('#bar'); + expect(scrollIntoView).to.have.been.calledOnce; + expect(scrollIntoView).to.have.been.calledOn(scratch.querySelector('a[name="bar"]')); pushState.restore(); + scrollIntoView.restore(); }); it('should ignore clicks on download links', async () => {