From 1266276a274bc05a5ba7ecb2ad85e76c9cc39976 Mon Sep 17 00:00:00 2001 From: Steeve Pastorelli Date: Fri, 9 Aug 2024 10:50:18 +0200 Subject: [PATCH 1/9] Add hook and provider --- .../useScrollToActiveTOCItem.tsx | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 packages/gitbook/src/components/TableOfContents/useScrollToActiveTOCItem.tsx diff --git a/packages/gitbook/src/components/TableOfContents/useScrollToActiveTOCItem.tsx b/packages/gitbook/src/components/TableOfContents/useScrollToActiveTOCItem.tsx new file mode 100644 index 0000000000..6904a53d9d --- /dev/null +++ b/packages/gitbook/src/components/TableOfContents/useScrollToActiveTOCItem.tsx @@ -0,0 +1,55 @@ +'use client'; + +import React from 'react'; + +const TOCScrollContainerContext = React.createContext(null); + +const SCROLL_OFFSET = 200; + +/** + * Provider that supplies the ID of the scroll container for the table of contents. + */ +export function TOCScrollContainerProvider( + props: React.PropsWithChildren<{ scrollContainerId: string }>, +) { + const { children, scrollContainerId } = props; + + return ( + + {children} + + ); +} + +/** + * Scrolls the table of contents container to the page item when it becomes active, + * but only if the item is outside the viewable area of the container. + */ +export function useScrollToActiveTOCItem(tocItem: { + isActive: boolean; + linkRef: React.RefObject; +}) { + const { isActive, linkRef } = tocItem; + const scrollContainerId = React.useContext(TOCScrollContainerContext); + + React.useEffect(() => { + if (isActive && linkRef.current && scrollContainerId) { + const container = document.getElementById(scrollContainerId); + + if (container) { + const itemTop = linkRef.current.offsetTop; + const itemBottom = itemTop + linkRef.current.offsetHeight; + const containerTop = container.scrollTop; + const containerBottom = containerTop + container.clientHeight; + + // Only scroll if the TOC item is outside the viewable area of the container + if (itemTop < containerTop || itemBottom > containerBottom) { + container.scrollTo({ + top: itemTop - SCROLL_OFFSET, + behavior: 'smooth', + }); + } + } + } + }, [isActive, linkRef, scrollContainerId]); +} From f039a84bd299d4cafeb78d8977fc0a55bdecb71c Mon Sep 17 00:00:00 2001 From: Steeve Pastorelli Date: Fri, 9 Aug 2024 10:51:04 +0200 Subject: [PATCH 2/9] Add hook to link item and provider to table of content --- .../TableOfContents/TableOfContents.tsx | 23 +++++++++++-------- .../TableOfContents/ToggleableLinkItem.tsx | 7 +++++- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx index aff93bbb77..5873d2a2c8 100644 --- a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx +++ b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx @@ -14,6 +14,7 @@ import { tcls } from '@/lib/tailwind'; import { PagesList } from './PagesList'; import { Trademark } from './Trademark'; +import { TOCScrollContainerProvider } from './useScrollToActiveTOCItem'; export function TableOfContents(props: { space: Space; @@ -26,6 +27,7 @@ export function TableOfContents(props: { withHeaderOffset: boolean; }) { const { space, customization, pages, ancestors, header, context, withHeaderOffset } = props; + const scrollContainerId = React.useId(); return ( ); diff --git a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx index 6610291b37..cf63cf078a 100644 --- a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx +++ b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { tcls } from '@/lib/tailwind'; +import { useScrollToActiveTOCItem } from './useScrollToActiveTOCItem'; import { Link } from '../primitives'; const show = { @@ -46,6 +47,7 @@ export function ToggleableLinkItem(props: { const [scope, animate] = useAnimate(); const [isVisible, setIsVisible] = React.useState(hasActiveDescendant); + const linkRef = React.createRef(); // Update the visibility of the children, if we are navigating to a descendant. React.useEffect(() => { @@ -90,12 +92,15 @@ export function ToggleableLinkItem(props: { const mountedRef = React.useRef(false); React.useEffect(() => { mountedRef.current = true; - }, []); + }); + + useScrollToActiveTOCItem({ linkRef, isActive }); return (
Date: Fri, 9 Aug 2024 10:59:46 +0200 Subject: [PATCH 3/9] Add changeset --- .changeset/stupid-guests-drive.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/stupid-guests-drive.md diff --git a/.changeset/stupid-guests-drive.md b/.changeset/stupid-guests-drive.md new file mode 100644 index 0000000000..405d4e5300 --- /dev/null +++ b/.changeset/stupid-guests-drive.md @@ -0,0 +1,5 @@ +--- +'gitbook': patch +--- + +Automatically scroll to active page in table of contents From 377cf34bafe5678ef01e16595aa4438815da4f7e Mon Sep 17 00:00:00 2001 From: Steeve Pastorelli Date: Fri, 9 Aug 2024 13:02:01 +0200 Subject: [PATCH 4/9] review: use scrollintoview --- .../TableOfContents/TableOfContents.tsx | 3 -- .../useScrollToActiveTOCItem.tsx | 44 ++----------------- 2 files changed, 4 insertions(+), 43 deletions(-) diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx index 5873d2a2c8..f00ed434ea 100644 --- a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx +++ b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx @@ -14,7 +14,6 @@ import { tcls } from '@/lib/tailwind'; import { PagesList } from './PagesList'; import { Trademark } from './Trademark'; -import { TOCScrollContainerProvider } from './useScrollToActiveTOCItem'; export function TableOfContents(props: { space: Space; @@ -81,7 +80,6 @@ export function TableOfContents(props: { customization.trademark.enabled ? 'lg:pb-20' : 'lg:pb-4', )} > - ) : null} -
); diff --git a/packages/gitbook/src/components/TableOfContents/useScrollToActiveTOCItem.tsx b/packages/gitbook/src/components/TableOfContents/useScrollToActiveTOCItem.tsx index 6904a53d9d..a3fd882dc5 100644 --- a/packages/gitbook/src/components/TableOfContents/useScrollToActiveTOCItem.tsx +++ b/packages/gitbook/src/components/TableOfContents/useScrollToActiveTOCItem.tsx @@ -2,54 +2,18 @@ import React from 'react'; -const TOCScrollContainerContext = React.createContext(null); - -const SCROLL_OFFSET = 200; - /** - * Provider that supplies the ID of the scroll container for the table of contents. - */ -export function TOCScrollContainerProvider( - props: React.PropsWithChildren<{ scrollContainerId: string }>, -) { - const { children, scrollContainerId } = props; - - return ( - - {children} - - ); -} - -/** - * Scrolls the table of contents container to the page item when it becomes active, - * but only if the item is outside the viewable area of the container. + * Scrolls the page item into the table of contents view when active. */ export function useScrollToActiveTOCItem(tocItem: { isActive: boolean; linkRef: React.RefObject; }) { const { isActive, linkRef } = tocItem; - const scrollContainerId = React.useContext(TOCScrollContainerContext); React.useEffect(() => { - if (isActive && linkRef.current && scrollContainerId) { - const container = document.getElementById(scrollContainerId); - - if (container) { - const itemTop = linkRef.current.offsetTop; - const itemBottom = itemTop + linkRef.current.offsetHeight; - const containerTop = container.scrollTop; - const containerBottom = containerTop + container.clientHeight; - - // Only scroll if the TOC item is outside the viewable area of the container - if (itemTop < containerTop || itemBottom > containerBottom) { - container.scrollTo({ - top: itemTop - SCROLL_OFFSET, - behavior: 'smooth', - }); - } - } + if (isActive && linkRef.current) { + linkRef.current.scrollIntoView({behavior: 'smooth', block: 'center'}) } - }, [isActive, linkRef, scrollContainerId]); + }, [isActive, linkRef]); } From 1d597785e19810351bb8c6c70fa9eaef74b76a85 Mon Sep 17 00:00:00 2001 From: Steeve Pastorelli Date: Fri, 9 Aug 2024 13:03:40 +0200 Subject: [PATCH 5/9] bun format --- .../TableOfContents/TableOfContents.tsx | 18 +++++++++--------- .../useScrollToActiveTOCItem.tsx | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx index f00ed434ea..da32fbb160 100644 --- a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx +++ b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx @@ -80,15 +80,15 @@ export function TableOfContents(props: { customization.trademark.enabled ? 'lg:pb-20' : 'lg:pb-4', )} > - - {customization.trademark.enabled ? ( - - ) : null} + + {customization.trademark.enabled ? ( + + ) : null} ); diff --git a/packages/gitbook/src/components/TableOfContents/useScrollToActiveTOCItem.tsx b/packages/gitbook/src/components/TableOfContents/useScrollToActiveTOCItem.tsx index a3fd882dc5..8283608ec4 100644 --- a/packages/gitbook/src/components/TableOfContents/useScrollToActiveTOCItem.tsx +++ b/packages/gitbook/src/components/TableOfContents/useScrollToActiveTOCItem.tsx @@ -13,7 +13,7 @@ export function useScrollToActiveTOCItem(tocItem: { React.useEffect(() => { if (isActive && linkRef.current) { - linkRef.current.scrollIntoView({behavior: 'smooth', block: 'center'}) + linkRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }, [isActive, linkRef]); } From 90e1e78188d3c7da1708151e54bd68447d305471 Mon Sep 17 00:00:00 2001 From: Steeve Pastorelli Date: Fri, 9 Aug 2024 13:04:36 +0200 Subject: [PATCH 6/9] remove id from scroll container --- .../gitbook/src/components/TableOfContents/TableOfContents.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx index da32fbb160..aff93bbb77 100644 --- a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx +++ b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx @@ -26,7 +26,6 @@ export function TableOfContents(props: { withHeaderOffset: boolean; }) { const { space, customization, pages, ancestors, header, context, withHeaderOffset } = props; - const scrollContainerId = React.useId(); return ( ); } diff --git a/packages/gitbook/src/components/TableOfContents/useScrollToActiveTOCItem.tsx b/packages/gitbook/src/components/TableOfContents/useScrollToActiveTOCItem.tsx index 8283608ec4..d9846a806d 100644 --- a/packages/gitbook/src/components/TableOfContents/useScrollToActiveTOCItem.tsx +++ b/packages/gitbook/src/components/TableOfContents/useScrollToActiveTOCItem.tsx @@ -2,8 +2,54 @@ import React from 'react'; +import { tcls } from '@/lib/tailwind'; + +const TOCScrollContainerContext = React.createContext | null>(null); + +export function TOCScrollContainerProvider( + props: React.PropsWithChildren<{ withHeaderOffset: boolean; withTrademarkEnabled: boolean }>, +) { + const { withHeaderOffset, withTrademarkEnabled, children } = props; + const scrollContainerRef = React.createRef(); + + return ( + +
+ {children} +
+
+ ); +} + +// Offset to scroll the table of contents item by. +const TOC_ITEM_OFFSET = 200; + /** - * Scrolls the page item into the table of contents view when active. + * Scrolls the table of contents container to the page item when it becomes active, + * but only if the item is outside the viewable area of the container. */ export function useScrollToActiveTOCItem(tocItem: { isActive: boolean; @@ -11,9 +57,28 @@ export function useScrollToActiveTOCItem(tocItem: { }) { const { isActive, linkRef } = tocItem; + const scrollContainerRef = React.useContext(TOCScrollContainerContext); React.useEffect(() => { - if (isActive && linkRef.current) { - linkRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' }); + if (isActive && linkRef.current && scrollContainerRef?.current) { + const tocItem = linkRef.current; + const tocContainer = scrollContainerRef.current; + + if (tocContainer) { + const tocItemTop = tocItem.offsetTop; + const containerTop = tocContainer.scrollTop; + const containerBottom = containerTop + tocContainer.clientHeight; + + // Only scroll if the TOC item is outside the viewable area of the container + if ( + tocItemTop < containerTop + TOC_ITEM_OFFSET || + tocItemTop > containerBottom - TOC_ITEM_OFFSET + ) { + tocContainer.scrollTo({ + top: tocItemTop - TOC_ITEM_OFFSET, + behavior: 'smooth', + }); + } + } } - }, [isActive, linkRef]); + }, [isActive, linkRef, scrollContainerRef]); } From 1302f6e79486c85a3a32caecfd7d22a42a32e220 Mon Sep 17 00:00:00 2001 From: Steeve Pastorelli Date: Thu, 15 Aug 2024 11:21:02 +0200 Subject: [PATCH 8/9] review --- .../TableOfContents/TableOfContents.tsx | 23 +++++++++++-- .../useScrollToActiveTOCItem.tsx | 34 ++++--------------- 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx index f158bb430f..ecb5633b7b 100644 --- a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx +++ b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx @@ -57,8 +57,27 @@ export function TableOfContents(props: { > {header ? header : null} | null>(null); export function TOCScrollContainerProvider( - props: React.PropsWithChildren<{ withHeaderOffset: boolean; withTrademarkEnabled: boolean }>, + props: React.PropsWithChildren<{ + className: React.HTMLAttributes['className']; + }>, ) { - const { withHeaderOffset, withTrademarkEnabled, children } = props; + const { className, children } = props; const scrollContainerRef = React.createRef(); return ( -
+
{children}
@@ -58,7 +37,7 @@ export function useScrollToActiveTOCItem(tocItem: { const { isActive, linkRef } = tocItem; const scrollContainerRef = React.useContext(TOCScrollContainerContext); - React.useEffect(() => { + React.useLayoutEffect(() => { if (isActive && linkRef.current && scrollContainerRef?.current) { const tocItem = linkRef.current; const tocContainer = scrollContainerRef.current; @@ -75,7 +54,6 @@ export function useScrollToActiveTOCItem(tocItem: { ) { tocContainer.scrollTo({ top: tocItemTop - TOC_ITEM_OFFSET, - behavior: 'smooth', }); } } From ced8b6d7c906983de8dce88050fb13462b32fce6 Mon Sep 17 00:00:00 2001 From: Steeve Pastorelli Date: Thu, 15 Aug 2024 11:21:40 +0200 Subject: [PATCH 9/9] Fix lint --- .../src/components/TableOfContents/useScrollToActiveTOCItem.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/gitbook/src/components/TableOfContents/useScrollToActiveTOCItem.tsx b/packages/gitbook/src/components/TableOfContents/useScrollToActiveTOCItem.tsx index 4fe27e130f..78b75f0bab 100644 --- a/packages/gitbook/src/components/TableOfContents/useScrollToActiveTOCItem.tsx +++ b/packages/gitbook/src/components/TableOfContents/useScrollToActiveTOCItem.tsx @@ -2,8 +2,6 @@ import React from 'react'; -import { tcls } from '@/lib/tailwind'; - const TOCScrollContainerContext = React.createContext | null>(null); export function TOCScrollContainerProvider(