From 9560bcf150d9b59454e5d8213825fa0f88b81d5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= Date: Sat, 25 Oct 2025 10:20:05 +0200 Subject: [PATCH 1/3] Remove useless Suspense boundary --- packages/gitbook/src/components/SitePage/SitePage.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/gitbook/src/components/SitePage/SitePage.tsx b/packages/gitbook/src/components/SitePage/SitePage.tsx index 4f0643ceec..b519543b83 100644 --- a/packages/gitbook/src/components/SitePage/SitePage.tsx +++ b/packages/gitbook/src/components/SitePage/SitePage.tsx @@ -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} /> - - - + ); From 51490ab4288439300cc66a26aaae53b046428283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= Date: Sat, 25 Oct 2025 10:21:00 +0200 Subject: [PATCH 2/3] Fix scroll not reset when navigating pages --- .../src/components/hooks/useScrollPage.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/gitbook/src/components/hooks/useScrollPage.ts b/packages/gitbook/src/components/hooks/useScrollPage.ts index 100174f627..fc000b6fbe 100644 --- a/packages/gitbook/src/components/hooks/useScrollPage.ts +++ b/packages/gitbook/src/components/hooks/useScrollPage.ts @@ -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]); } From 17b6f2ab4e6d45196f134f68a887c66be3cd7a34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= Date: Sat, 25 Oct 2025 10:21:10 +0200 Subject: [PATCH 3/3] Fix scroll jump on page load --- .../components/primitives/ScrollContainer.tsx | 82 ++++++++++++------- 1 file changed, 52 insertions(+), 30 deletions(-) diff --git a/packages/gitbook/src/components/primitives/ScrollContainer.tsx b/packages/gitbook/src/components/primitives/ScrollContainer.tsx index edbab415f5..f8b87a600d 100644 --- a/packages/gitbook/src/components/primitives/ScrollContainer.tsx +++ b/packages/gitbook/src/components/primitives/ScrollContainer.tsx @@ -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'; /** @@ -32,56 +33,53 @@ export function ScrollContainer(props: ScrollContainerProps) { const language = useLanguage(); - React.useEffect(() => { + useScrollListener(() => { 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); + setScrollPosition( + orientation === 'horizontal' ? container.scrollLeft : container.scrollTop + ); + }, containerRef); + + React.useEffect(() => { + const container = containerRef.current; + if (!container) { + return; + } // 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 = () => { @@ -89,6 +87,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, @@ -101,6 +100,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, @@ -175,3 +175,25 @@ export function ScrollContainer(props: ScrollContainerProps) { ); } + +/** + * 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', + }); +}