mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-10-11 23:54:23 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d414f09b36 | |||
| be0d5d25f0 | |||
| 105c1d337a | |||
| a219ad87d9 | |||
| 2e182fd111 | |||
| c2d18048f1 | |||
| 06132fe0f0 | |||
| 0146877fc1 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix anchor links (e.g. `/page#heading`) not scrolling to the target heading during client-side navigation between pages.
|
||||
@@ -47,11 +47,20 @@ function useScrollPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hash) {
|
||||
scrollToHash(hash);
|
||||
// For a same-page anchor the context `hash` is authoritative (set on click, before Next
|
||||
// commits the URL). For a cross-page navigation the context hash is transiently empty while
|
||||
// the section remounts, so read the committed URL instead.
|
||||
const samePage = previous !== undefined && previous.pathname === pathname;
|
||||
const targetHash = samePage ? hash : window.location.hash.slice(1);
|
||||
|
||||
if (targetHash) {
|
||||
scrollToHash(targetHash);
|
||||
return;
|
||||
}
|
||||
|
||||
// No hash: cancel any in-flight scroll-to-hash from a previous navigation so it can't fight
|
||||
// this one. Next handles the actual scroll-to-top of the new page.
|
||||
cancelScrollToHash?.();
|
||||
window.scrollTo(0, 0);
|
||||
}, [hash, pathname, previous]);
|
||||
}
|
||||
@@ -77,17 +86,45 @@ export function useScrollToHash() {
|
||||
}, [hash]);
|
||||
}
|
||||
|
||||
/** Cancels an in-flight {@link scrollToHash} run, if any. */
|
||||
let cancelScrollToHash: (() => void) | null = null;
|
||||
|
||||
/** Treat a navigation as settled — and stop re-asserting the scroll — once the DOM is quiet this long. */
|
||||
const SCROLL_TO_HASH_SETTLE_MS = 400;
|
||||
|
||||
/**
|
||||
* Scroll to a hash, if scroll didn't work, return false.
|
||||
* Scroll to the element matching a hash after a client-side navigation.
|
||||
*
|
||||
* A soft navigation delivers the destination asynchronously: its content keeps mounting and reflowing
|
||||
* for a few hundred ms after the URL changes, so a single scroll lands before the target reaches its
|
||||
* final position. Re-scroll on every DOM change until it goes quiet — mirroring the fragment scrolling
|
||||
* the browser does for free during a full page load. Bail if the visitor scrolls, so we never fight
|
||||
* them. (`instant` so re-scrolling to an already-reached position is a no-op.)
|
||||
*/
|
||||
function scrollToHash(hash: string) {
|
||||
const element = document.getElementById(hash);
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
block: 'start',
|
||||
behavior: 'smooth',
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
cancelScrollToHash?.();
|
||||
|
||||
let settleTimer = 0;
|
||||
const stop = () => {
|
||||
observer.disconnect();
|
||||
window.clearTimeout(settleTimer);
|
||||
window.removeEventListener('wheel', stop);
|
||||
window.removeEventListener('touchmove', stop);
|
||||
cancelScrollToHash = null;
|
||||
};
|
||||
|
||||
const reassert = () => {
|
||||
document.getElementById(hash)?.scrollIntoView({ block: 'start', behavior: 'instant' });
|
||||
window.clearTimeout(settleTimer);
|
||||
settleTimer = window.setTimeout(stop, SCROLL_TO_HASH_SETTLE_MS);
|
||||
};
|
||||
|
||||
// Our own `scrollIntoView` dispatches `scroll`, not `wheel`/`touchmove`, so it can't self-cancel.
|
||||
const observer = new MutationObserver(reassert);
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
window.addEventListener('wheel', stop, { passive: true });
|
||||
window.addEventListener('touchmove', stop, { passive: true });
|
||||
|
||||
reassert();
|
||||
cancelScrollToHash = stop;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user