Skip to content

Commit 3a90fff

Browse files
committed
feat: make the table of contents sections reactive to user-scroll
1 parent 35c904c commit 3a90fff

File tree

3 files changed

+67
-13
lines changed

3 files changed

+67
-13
lines changed

app/components/Doc.tsx

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,55 @@ export function Doc({
4343

4444
const isTocVisible = shouldRenderToc && headings && headings.length > 1
4545

46+
const markdownContainerRef = React.useRef<HTMLDivElement>(null)
47+
const [activeHeadings, setActiveHeadings] = React.useState<Array<string>>([])
48+
49+
const headingElementRefs = React.useRef<
50+
Record<string, IntersectionObserverEntry>
51+
>({})
52+
53+
React.useEffect(() => {
54+
const callback = (headingsList: Array<IntersectionObserverEntry>) => {
55+
headingElementRefs.current = headingsList.reduce(
56+
(map, headingElement) => {
57+
map[headingElement.target.id] = headingElement
58+
return map
59+
},
60+
headingElementRefs.current
61+
)
62+
63+
const visibleHeadings: Array<IntersectionObserverEntry> = []
64+
Object.keys(headingElementRefs.current).forEach((key) => {
65+
const headingElement = headingElementRefs.current[key]
66+
if (headingElement.isIntersecting) {
67+
visibleHeadings.push(headingElement)
68+
}
69+
})
70+
71+
if (visibleHeadings.length >= 1) {
72+
setActiveHeadings(visibleHeadings.map((h) => h.target.id))
73+
}
74+
}
75+
76+
const observer = new IntersectionObserver(callback, {
77+
rootMargin: '0px',
78+
threshold: 0.2,
79+
})
80+
81+
const headingElements = Array.from(
82+
markdownContainerRef.current?.querySelectorAll(
83+
'h2[id], h3[id], h4[id], h5[id], h6[id]'
84+
) ?? []
85+
)
86+
headingElements.forEach((el) => observer.observe(el))
87+
console.log(
88+
'observing',
89+
headingElements.map((h) => h.id)
90+
)
91+
92+
return () => observer.disconnect()
93+
}, [])
94+
4695
return (
4796
<div
4897
className={twMerge(
@@ -61,9 +110,11 @@ export function Doc({
61110
<div className="h-px bg-gray-500 opacity-20" />
62111
<div className="h-4" />
63112
<div
113+
ref={markdownContainerRef}
64114
className={twMerge(
65115
'prose prose-gray prose-sm prose-p:leading-7 dark:prose-invert max-w-none',
66-
isTocVisible && 'pr-4 lg:pr-6'
116+
isTocVisible && 'pr-4 lg:pr-6',
117+
'styled-markdown-content'
67118
)}
68119
>
69120
<Markdown htmlMarkup={markup} />
@@ -83,7 +134,12 @@ export function Doc({
83134

84135
{isTocVisible && (
85136
<div className="border-l border-gray-500/20 max-w-52 w-full hidden 2xl:block transition-all">
86-
<Toc headings={headings} colorFrom={colorFrom} colorTo={colorTo} />
137+
<Toc
138+
headings={headings}
139+
activeHeadings={activeHeadings}
140+
colorFrom={colorFrom}
141+
colorTo={colorTo}
142+
/>
87143
</div>
88144
)}
89145
</div>

app/components/Toc.tsx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as React from 'react'
22
import { twMerge } from 'tailwind-merge'
33
import { HeadingData } from 'marked-gfm-heading-id'
4-
import { useLocation } from '@tanstack/react-router'
54

65
const headingLevels: Record<number, string> = {
76
1: 'pl-2',
@@ -16,17 +15,15 @@ type TocProps = {
1615
headings: HeadingData[]
1716
colorFrom?: string
1817
colorTo?: string
18+
activeHeadings: Array<string>
1919
}
2020

21-
export function Toc({ headings, colorFrom, colorTo }: TocProps) {
22-
const location = useLocation()
23-
24-
const [hash, setHash] = React.useState('')
25-
26-
React.useEffect(() => {
27-
setHash(location.hash)
28-
}, [location])
29-
21+
export function Toc({
22+
headings,
23+
colorFrom,
24+
colorTo,
25+
activeHeadings,
26+
}: TocProps) {
3027
return (
3128
<nav className="flex flex-col sticky top-2 max-h-screen divide-y divide-gray-500/20">
3229
<div className="p-2">
@@ -48,7 +45,7 @@ export function Toc({ headings, colorFrom, colorTo }: TocProps) {
4845
<a
4946
title={heading.id}
5047
href={`#${heading.id}`}
51-
aria-current={hash === heading.id && 'location'}
48+
aria-current={activeHeadings.includes(heading.id) && 'location'}
5249
className={`truncate block aria-current:bg-gradient-to-r ${colorFrom} ${colorTo} aria-current:bg-clip-text aria-current:text-transparent`}
5350
dangerouslySetInnerHTML={{
5451
__html: heading.text,

app/routes/$libraryId/$version.docs.framework.$framework.$.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ function Docs() {
4848
return (
4949
<DocContainer>
5050
<Doc
51+
key={filePath}
5152
title={title}
5253
content={content}
5354
repo={library.repo}

0 commit comments

Comments
 (0)