From bda9fe2d7621ddc8763dd67df581c6cf91f0cfcb Mon Sep 17 00:00:00 2001 From: Claire Chabas Date: Tue, 14 Apr 2026 19:41:51 +0200 Subject: [PATCH] Make ToC groups collapsible and other styles fixes/improvements (#4177) --- .../TableOfContents/PageGroupItem.tsx | 119 ++++++++++++++++-- .../TableOfContents/PageLinkItem.tsx | 2 +- .../TableOfContents/ToggleableLinkItem.tsx | 4 +- .../src/components/TableOfContents/styles.ts | 22 +++- .../components/primitives/StyleProvider.tsx | 9 +- 5 files changed, 132 insertions(+), 24 deletions(-) diff --git a/packages/gitbook/src/components/TableOfContents/PageGroupItem.tsx b/packages/gitbook/src/components/TableOfContents/PageGroupItem.tsx index d4a34dff2..bde9713db 100644 --- a/packages/gitbook/src/components/TableOfContents/PageGroupItem.tsx +++ b/packages/gitbook/src/components/TableOfContents/PageGroupItem.tsx @@ -1,41 +1,140 @@ 'use client'; +import React from 'react'; import type { ClientTOCPageGroup } from './encodeClientTableOfContents'; import { tcls } from '@/lib/tailwind'; +import { ToggleChevron } from '../primitives'; import { PagesList } from './PagesList'; import { TOCPageIcon } from './TOCPageIcon'; +import { ToCButtonItemStyles } from './styles'; export function PageGroupItem(props: { page: ClientTOCPageGroup; isFirst?: boolean }) { const { page, isFirst } = props; + const descendants = page.descendants ?? []; + const hasDescendants = descendants.length > 0; + const [isOpen, setIsOpen] = React.useState(true); + const { sentinelRef, isSticking } = useIsSticking(); + + const handleToggle = () => { + if (!hasDescendants) { + return; + } + + setIsOpen((prev) => !prev); + }; return (
  • +
  • ); } + +/** + * Detect when a sticky element becomes "stuck" using an IntersectionObserver on a sentinel. + * Place the sentinel ref on a 0-height element right before the sticky element. + */ +function useIsSticking() { + const sentinelRef = React.useRef(null); + const [isSticking, setIsSticking] = React.useState(false); + + React.useEffect(() => { + const sentinel = sentinelRef.current; + if (!sentinel) return; + + // Find the closest scrollable ancestor to use as IntersectionObserver root + let scrollParent: Element | null = sentinel.parentElement; + while (scrollParent) { + const { overflowY } = getComputedStyle(scrollParent); + if (overflowY === 'auto' || overflowY === 'scroll') break; + scrollParent = scrollParent.parentElement; + } + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry) { + setIsSticking(!entry.isIntersecting); + } + }, + { root: scrollParent } + ); + + observer.observe(sentinel); + return () => observer.disconnect(); + }, []); + + return { sentinelRef, isSticking }; +} diff --git a/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx b/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx index e1f013093..8e8db7f20 100644 --- a/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx +++ b/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx @@ -18,7 +18,7 @@ export function PageLinkItem(props: { page: ClientTOCPageLink }) {
  • diff --git a/packages/gitbook/src/components/TableOfContents/styles.ts b/packages/gitbook/src/components/TableOfContents/styles.ts index e50bc9e11..c61ab9645 100644 --- a/packages/gitbook/src/components/TableOfContents/styles.ts +++ b/packages/gitbook/src/components/TableOfContents/styles.ts @@ -1,14 +1,26 @@ -export const ToggleableLinkItemStyles = [ - 'group/toclink toclink relative transition-colors', +export const ToCItemBaseStyles = [ 'flex flex-row justify-start items-center gap-3', 'circular-corners:rounded-2xl rounded-md straight-corners:rounded-none p-1.5 pl-3', - 'text-balance font-normal text-sm text-tint-strong/7 hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong', - 'contrast-more:hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-tint-12', + 'focus-visible:-outline-offset-2', 'before:contents[] before:-left-px before:absolute before:inset-y-0', 'sidebar-list-line:rounded-l-none! sidebar-list-line:before:w-px [&+div_a]:sidebar-list-default:rounded-l-none [&+div_a]:pl-5 [&+div_a]:sidebar-list-default:before:w-px', ]; -export const ToggleableLinkItemActiveStyles = [ +export const ToCLinkItemStyles = [ + 'group/toclink toclink relative transition-colors', + ToCItemBaseStyles, + 'text-balance font-normal text-sm text-tint-strong/7 hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong', + 'contrast-more:hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-tint-12', +]; + +export const ToCButtonItemStyles = [ + 'relative transition-colors', + ToCItemBaseStyles, + 'text-balance font-normal text-sm text-tint-strong hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong', + 'contrast-more:hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-tint-12', +]; + +export const ToCLinkItemActiveStyles = [ 'font-semibold', 'sidebar-list-line:before:w-0.5', diff --git a/packages/gitbook/src/components/primitives/StyleProvider.tsx b/packages/gitbook/src/components/primitives/StyleProvider.tsx index 1ed8edbd8..fd9e57357 100644 --- a/packages/gitbook/src/components/primitives/StyleProvider.tsx +++ b/packages/gitbook/src/components/primitives/StyleProvider.tsx @@ -2,10 +2,7 @@ import type { ClassValue } from '@/lib/tailwind'; import { RecordCardLinkStyles, RecordCardStyles } from '../DocumentView/Table/styles'; -import { - ToggleableLinkItemActiveStyles, - ToggleableLinkItemStyles, -} from '../TableOfContents/styles'; +import { ToCLinkItemActiveStyles, ToCLinkItemStyles } from '../TableOfContents/styles'; import { ButtonStyles, CardStyles, LinkStyles } from './styles'; const styles = { @@ -14,8 +11,8 @@ const styles = { ButtonStyles, RecordCardStyles, RecordCardLinkStyles, - ToggleableLinkItemStyles, - ToggleableLinkItemActiveStyles, + ToCLinkItemStyles, + ToCLinkItemActiveStyles, }; export type DesignTokenName = keyof typeof styles;