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