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} /> - - - + ); 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]); } 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', + }); +}