Automatically scroll to the active page in table of contents (#2425)

This commit is contained in:
spastorelli
2024-08-15 11:42:48 +02:00
committed by GitHub
parent aa321989c4
commit 9f6c3347cb
4 changed files with 74 additions and 3 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Automatically scroll to active page in table of contents
@@ -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;
@@ -55,7 +56,7 @@ export function TableOfContents(props: {
)}
>
{header ? header : null}
<div
<TOCScrollContainerProvider
className={tcls(
withHeaderOffset ? 'pt-4' : ['pt-4', 'lg:pt-0'],
'hidden',
@@ -87,7 +88,7 @@ export function TableOfContents(props: {
{customization.trademark.enabled ? (
<Trademark space={space} customization={customization} />
) : null}
</div>
</TOCScrollContainerProvider>
</aside>
);
}
@@ -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<HTMLAnchorElement>();
// 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 (
<div>
<Link
href={href}
ref={linkRef}
aria-selected={isActive}
className={tcls(
'group/toclink',
@@ -0,0 +1,60 @@
'use client';
import React from 'react';
const TOCScrollContainerContext = React.createContext<React.RefObject<HTMLDivElement> | null>(null);
export function TOCScrollContainerProvider(
props: React.PropsWithChildren<{
className: React.HTMLAttributes<HTMLDivElement>['className'];
}>,
) {
const { className, children } = props;
const scrollContainerRef = React.createRef<HTMLDivElement>();
return (
<TOCScrollContainerContext.Provider value={scrollContainerRef}>
<div ref={scrollContainerRef} className={className}>
{children}
</div>
</TOCScrollContainerContext.Provider>
);
}
// Offset to scroll the table of contents item by.
const TOC_ITEM_OFFSET = 200;
/**
* 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<HTMLAnchorElement>;
}) {
const { isActive, linkRef } = tocItem;
const scrollContainerRef = React.useContext(TOCScrollContainerContext);
React.useLayoutEffect(() => {
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,
});
}
}
}
}, [isActive, linkRef, scrollContainerRef]);
}