Skip to content

Commit bda9fe2

Browse files
authored
Make ToC groups collapsible and other styles fixes/improvements (#4177)
1 parent 2bdade3 commit bda9fe2

5 files changed

Lines changed: 132 additions & 24 deletions

File tree

Lines changed: 109 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,140 @@
11
'use client';
22

3+
import React from 'react';
34
import type { ClientTOCPageGroup } from './encodeClientTableOfContents';
45

56
import { tcls } from '@/lib/tailwind';
7+
import { ToggleChevron } from '../primitives';
68

79
import { PagesList } from './PagesList';
810
import { TOCPageIcon } from './TOCPageIcon';
11+
import { ToCButtonItemStyles } from './styles';
912

1013
export function PageGroupItem(props: { page: ClientTOCPageGroup; isFirst?: boolean }) {
1114
const { page, isFirst } = props;
15+
const descendants = page.descendants ?? [];
16+
const hasDescendants = descendants.length > 0;
17+
const [isOpen, setIsOpen] = React.useState(true);
18+
const { sentinelRef, isSticking } = useIsSticking();
19+
20+
const handleToggle = () => {
21+
if (!hasDescendants) {
22+
return;
23+
}
24+
25+
setIsOpen((prev) => !prev);
26+
};
1227

1328
return (
1429
<li className="page-group-item flex flex-col">
30+
<div ref={sentinelRef} className="h-0" aria-hidden="true" />
1531
<div
1632
className={tcls(
17-
'-top-4 sticky z-1 flex items-center gap-3 px-3',
18-
'font-semibold text-xs uppercase tracking-wide',
19-
'mt-2 pt-4 pb-3', // Add extra padding to make the header fade a bit nicer
20-
'-mb-1.5', // Then pull the page items a bit closer, effective bottom padding is 1.5 units / 6px.
21-
'mask-[linear-gradient(rgba(0,0,0,1)_70%,rgba(0,0,0,0))]', // Fade out effect of fixed page items. We want the fade to start past the header, this is a good approximation.
33+
'-top-4 sticky z-1 after:pointer-events-none after:absolute after:inset-x-0 after:top-full after:h-4 after:bg-linear-to-b after:from-tint-base after:to-transparent after:transition-opacity',
34+
isSticking ? '' : 'after:opacity-0',
35+
'mt-1 pt-2.5 pb-0',
2236
'bg-tint-base',
37+
'sidebar-filled:after:from-tint-subtle',
2338
'sidebar-filled:bg-tint-subtle',
39+
'theme-muted:after:from-tint-subtle',
2440
'theme-muted:bg-tint-subtle',
2541
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
42+
'[html.sidebar-filled.theme-bold.tint_&]:after:from-tint-subtle',
2643
'[html.sidebar-filled.theme-muted_&]:bg-tint-base',
44+
'[html.sidebar-filled.theme-muted_&]:after:from-tint-base',
2745
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
46+
'[html.sidebar-filled.theme-bold.tint_&]:after:from-tint-base',
2847
'lg:[html.sidebar-default.theme-gradient_&]:bg-gradient-primary',
48+
'lg:[html.sidebar-default.theme-gradient_&]:after:from-primary-2',
2949
'lg:[html.sidebar-default.theme-gradient.tint_&]:bg-gradient-tint',
30-
isFirst ? '-mt-2 -top-2 rounded-t-2xl pt-2' : ''
50+
'lg:[html.sidebar-default.theme-gradient.tint_&]:after:from-tint-subtle',
51+
isFirst ? '-mt-2 -top-2 circular-corners:rounded-t-2xl rounded-t-md pt-2' : ''
3152
)}
3253
>
33-
<TOCPageIcon page={page} />
34-
{page.title}
54+
<button
55+
type="button"
56+
disabled={!hasDescendants}
57+
aria-expanded={hasDescendants ? isOpen : undefined}
58+
onClick={handleToggle}
59+
className={tcls(
60+
ToCButtonItemStyles,
61+
'toc-group min-h-8 w-full border-0 text-left',
62+
'font-semibold text-xs uppercase tracking-wide',
63+
'appearance-none',
64+
'[&_.toc-group-chevron]:transition-opacity',
65+
'hover:[&_.toc-group-chevron]:opacity-11',
66+
'focus-visible:[&_.toc-group-chevron]:opacity-11',
67+
hasDescendants ? 'cursor-pointer' : ''
68+
)}
69+
>
70+
<TOCPageIcon page={page} />
71+
<span className="min-w-0 flex-1">{page.title}</span>
72+
{hasDescendants ? (
73+
<span
74+
className={tcls(
75+
'toc-group-chevron ml-auto flex shrink-0 transition-opacity duration-150',
76+
isOpen
77+
? 'pointer-events-none opacity-0 delay-75'
78+
: 'opacity-6 delay-0'
79+
)}
80+
>
81+
<ToggleChevron
82+
open={isOpen}
83+
orientation="right-to-down"
84+
className="m-0! size-3!"
85+
/>
86+
</span>
87+
) : null}
88+
</button>
3589
</div>
36-
{page.descendants && page.descendants.length > 0 ? (
37-
<PagesList pages={page.descendants} />
90+
{hasDescendants ? (
91+
<div
92+
className={tcls(
93+
'mt-px grid transition-[grid-template-rows,opacity] duration-200 ease-in-out',
94+
isOpen ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'
95+
)}
96+
>
97+
<div className="overflow-hidden">
98+
<PagesList pages={descendants} />
99+
</div>
100+
</div>
38101
) : null}
39102
</li>
40103
);
41104
}
105+
106+
/**
107+
* Detect when a sticky element becomes "stuck" using an IntersectionObserver on a sentinel.
108+
* Place the sentinel ref on a 0-height element right before the sticky element.
109+
*/
110+
function useIsSticking() {
111+
const sentinelRef = React.useRef<HTMLDivElement>(null);
112+
const [isSticking, setIsSticking] = React.useState(false);
113+
114+
React.useEffect(() => {
115+
const sentinel = sentinelRef.current;
116+
if (!sentinel) return;
117+
118+
// Find the closest scrollable ancestor to use as IntersectionObserver root
119+
let scrollParent: Element | null = sentinel.parentElement;
120+
while (scrollParent) {
121+
const { overflowY } = getComputedStyle(scrollParent);
122+
if (overflowY === 'auto' || overflowY === 'scroll') break;
123+
scrollParent = scrollParent.parentElement;
124+
}
125+
126+
const observer = new IntersectionObserver(
127+
([entry]) => {
128+
if (entry) {
129+
setIsSticking(!entry.isIntersecting);
130+
}
131+
},
132+
{ root: scrollParent }
133+
);
134+
135+
observer.observe(sentinel);
136+
return () => observer.disconnect();
137+
}, []);
138+
139+
return { sentinelRef, isSticking };
140+
}

packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function PageLinkItem(props: { page: ClientTOCPageLink }) {
1818
<li className="page-link-item flex flex-col [.page-group-item+&]:mt-4">
1919
<Link
2020
href={page.href ?? '#'}
21-
classNames={['ToggleableLinkItemStyles']}
21+
classNames={['ToCLinkItemStyles']}
2222
insights={{
2323
type: 'link_click',
2424
link: {

packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ function LinkItem(
113113
insights={insights}
114114
aria-current={isActive ? 'page' : undefined}
115115
classNames={[
116-
'ToggleableLinkItemStyles',
117-
...(isActive ? ['ToggleableLinkItemActiveStyles' as const] : []),
116+
'ToCLinkItemStyles',
117+
...(isActive ? ['ToCLinkItemActiveStyles' as const] : []),
118118
]}
119119
onClick={handleClick}
120120
>

packages/gitbook/src/components/TableOfContents/styles.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
1-
export const ToggleableLinkItemStyles = [
2-
'group/toclink toclink relative transition-colors',
1+
export const ToCItemBaseStyles = [
32
'flex flex-row justify-start items-center gap-3',
43
'circular-corners:rounded-2xl rounded-md straight-corners:rounded-none p-1.5 pl-3',
5-
'text-balance font-normal text-sm text-tint-strong/7 hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong',
6-
'contrast-more:hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-tint-12',
4+
'focus-visible:-outline-offset-2',
75
'before:contents[] before:-left-px before:absolute before:inset-y-0',
86
'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',
97
];
108

11-
export const ToggleableLinkItemActiveStyles = [
9+
export const ToCLinkItemStyles = [
10+
'group/toclink toclink relative transition-colors',
11+
ToCItemBaseStyles,
12+
'text-balance font-normal text-sm text-tint-strong/7 hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong',
13+
'contrast-more:hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-tint-12',
14+
];
15+
16+
export const ToCButtonItemStyles = [
17+
'relative transition-colors',
18+
ToCItemBaseStyles,
19+
'text-balance font-normal text-sm text-tint-strong hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong',
20+
'contrast-more:hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-tint-12',
21+
];
22+
23+
export const ToCLinkItemActiveStyles = [
1224
'font-semibold',
1325
'sidebar-list-line:before:w-0.5',
1426

packages/gitbook/src/components/primitives/StyleProvider.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@
22
import type { ClassValue } from '@/lib/tailwind';
33

44
import { RecordCardLinkStyles, RecordCardStyles } from '../DocumentView/Table/styles';
5-
import {
6-
ToggleableLinkItemActiveStyles,
7-
ToggleableLinkItemStyles,
8-
} from '../TableOfContents/styles';
5+
import { ToCLinkItemActiveStyles, ToCLinkItemStyles } from '../TableOfContents/styles';
96
import { ButtonStyles, CardStyles, LinkStyles } from './styles';
107

118
const styles = {
@@ -14,8 +11,8 @@ const styles = {
1411
ButtonStyles,
1512
RecordCardStyles,
1613
RecordCardLinkStyles,
17-
ToggleableLinkItemStyles,
18-
ToggleableLinkItemActiveStyles,
14+
ToCLinkItemStyles,
15+
ToCLinkItemActiveStyles,
1916
};
2017

2118
export type DesignTokenName = keyof typeof styles;

0 commit comments

Comments
 (0)