Fix scroll jump and scroll reset (#3756)

This commit is contained in:
Greg Bergé
2025-10-25 13:04:34 +02:00
committed by GitHub
parent f9f80117f0
commit a2fb627ae1
3 changed files with 61 additions and 42 deletions
@@ -7,7 +7,6 @@ import {
} from '@gitbook/api';
import type { Metadata, Viewport } from 'next';
import { notFound, redirect } from 'next/navigation';
import React from 'react';
import { PageAside } from '@/components/PageAside';
import { PageBody, PageCover } from '@/components/PageBody';
@@ -75,9 +74,7 @@ export async function SitePage(props: SitePageProps) {
insightsDisplayContext={SiteInsightsDisplayContext.Site}
/>
</div>
<React.Suspense fallback={null}>
<PageClientLayout />
</React.Suspense>
<PageClientLayout />
</div>
</PageContextProvider>
);
@@ -7,9 +7,8 @@ import { useHash } from './useHash';
import { usePrevious } from './usePrevious';
/**
* Scroll the page to an anchor point or
* to the top of the page when navigating between pages (pathname)
* or sections of a page (hash).
* Scroll the page to the hash or reset scroll to the top.
* Only triggered while navigating in the app, not for initial load.
*/
export function useScrollPage() {
const hash = useHash();
@@ -17,6 +16,10 @@ export function useScrollPage() {
const pathname = usePathname();
const previousPathname = usePrevious(pathname);
React.useLayoutEffect(() => {
if (!previousHash && !previousPathname) {
return;
}
// If there is no change in pathname or hash, do nothing
if (previousHash === hash && previousPathname === pathname) {
return;
@@ -31,13 +34,10 @@ export function useScrollPage() {
block: 'start',
behavior: 'smooth',
});
return;
}
return;
}
// If there was a hash but not anymore, scroll to top
if (previousHash && !hash) {
window.scrollTo(0, 0);
}
window.scrollTo(0, 0);
}, [hash, previousHash, pathname, previousPathname]);
}
@@ -3,6 +3,7 @@
import { tString, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
import * as React from 'react';
import { useScrollListener } from '../hooks/useScrollListener';
import { Button } from './Button';
/**
@@ -46,56 +47,53 @@ export function ScrollContainer(props: ScrollContainerProps) {
const language = useLanguage();
useScrollListener(() => {
const container = containerRef.current;
if (!container) {
return;
}
setScrollPosition(
orientation === 'horizontal' ? container.scrollLeft : container.scrollTop
);
}, containerRef);
React.useEffect(() => {
const container = containerRef.current;
if (!container) {
return;
}
// Update scroll position on scroll using requestAnimationFrame
const scrollListener: EventListener = () => {
requestAnimationFrame(() => {
setScrollPosition(
orientation === 'horizontal' ? container.scrollLeft : container.scrollTop
);
});
};
container.addEventListener('scroll', scrollListener);
// Update max scroll position using resize observer
const resizeObserver = new ResizeObserver((entries) => {
const containerEntry = entries.find((i) => i.target === containerRef.current);
if (containerEntry) {
const ro = new ResizeObserver((entries) => {
const [entry] = entries;
if (entry) {
setScrollSize(
orientation === 'horizontal'
? containerEntry.target.scrollWidth - containerEntry.target.clientWidth - 1
: containerEntry.target.scrollHeight -
containerEntry.target.clientHeight -
1
? entry.target.scrollWidth - entry.target.clientWidth - 1
: entry.target.scrollHeight - entry.target.clientHeight - 1
);
}
});
resizeObserver.observe(container);
return () => {
container.removeEventListener('scroll', scrollListener);
resizeObserver.disconnect();
};
ro.observe(container);
return () => ro.disconnect();
}, [orientation]);
// Scroll to the active item
React.useEffect(() => {
const container = containerRef.current;
if (!container || !activeId) {
if (!container) {
return;
}
const activeItem = container.querySelector(`#${CSS.escape(activeId)}`);
if (activeItem) {
activeItem.scrollIntoView({
inline: 'center',
block: 'center',
});
if (!activeId) {
return;
}
const activeItem = document.getElementById(activeId);
if (!activeItem || !container.contains(activeItem)) {
return;
}
scrollToElementInContainer(activeItem, container);
}, [activeId]);
const scrollFurther = () => {
@@ -103,6 +101,7 @@ export function ScrollContainer(props: ScrollContainerProps) {
if (!container) {
return;
}
container.scrollTo({
top: orientation === 'vertical' ? scrollPosition + container.clientHeight : undefined,
left: orientation === 'horizontal' ? scrollPosition + container.clientWidth : undefined,
@@ -115,6 +114,7 @@ export function ScrollContainer(props: ScrollContainerProps) {
if (!container) {
return;
}
container.scrollTo({
top: orientation === 'vertical' ? scrollPosition - container.clientHeight : undefined,
left: orientation === 'horizontal' ? scrollPosition - container.clientWidth : undefined,
@@ -194,3 +194,25 @@ export function ScrollContainer(props: ScrollContainerProps) {
</div>
);
}
/**
* Scroll to an element in a container.
*/
function scrollToElementInContainer(element: HTMLElement, container: HTMLElement) {
const containerRect = container.getBoundingClientRect();
const rect = element.getBoundingClientRect();
return container.scrollTo({
top:
container.scrollTop +
(rect.top - containerRect.top) -
container.clientHeight / 2 +
rect.height / 2,
left:
container.scrollLeft +
(rect.left - containerRect.left) -
container.clientWidth / 2 +
rect.width / 2,
behavior: 'smooth',
});
}