Skip to content

Commit b91a253

Browse files
gaearonclaude
andcommitted
Highlight the table of contents by its own entries
useTocHighlight built its own list of headings by scanning the DOM for anchor elements inside H1/H2/H3, and assumed that list lined up with the TOC. It only did so because the page title rendered a broken #undefined anchor that happened to pad index 0 ("Overview"); with that gone, the previous entry got highlighted. Headings nested in <Note> etc. have anchors but aren't in the TOC, which skewed it on those pages too. Resolve the TOC entries' own ids to elements instead, so there is one source of truth. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 3d9ebe9 commit b91a253

2 files changed

Lines changed: 23 additions & 27 deletions

File tree

src/components/Layout/Toc.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,7 @@ import {useTocHighlight} from './useTocHighlight';
1717
import type {Toc} from '../MDX/TocContext';
1818

1919
export function Toc({headings}: {headings: Toc}) {
20-
const {currentIndex} = useTocHighlight();
21-
// TODO: We currently have a mismatch between the headings in the document
22-
// and the headings we find in MarkdownPage (i.e. we don't find Recap or Challenges).
23-
// Select the max TOC item we have here for now, but remove this after the fix.
24-
const selectedIndex = Math.min(currentIndex, headings.length - 1);
20+
const {currentIndex: selectedIndex} = useTocHighlight(headings);
2521
return (
2622
<nav role="navigation" className="pt-20 sticky top-0 end-0">
2723
{headings.length > 0 && (

src/components/Layout/useTocHighlight.tsx

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,53 +10,53 @@
1010
*/
1111

1212
import {useState, useRef, useEffect} from 'react';
13+
import type {Toc} from '../MDX/TocContext';
1314

1415
const TOP_OFFSET = 85;
1516

16-
export function getHeaderAnchors(): HTMLAnchorElement[] {
17-
return Array.prototype.filter.call(
18-
document.getElementsByClassName('mdx-header-anchor'),
19-
function (testElement) {
20-
return (
21-
testElement.parentNode.nodeName === 'H1' ||
22-
testElement.parentNode.nodeName === 'H2' ||
23-
testElement.parentNode.nodeName === 'H3'
24-
);
25-
}
17+
// Resolves each TOC entry to its heading element. The first entry
18+
// ("Overview", href "#") is the top of the page and has no element.
19+
function getTocTargets(headings: Toc): Array<HTMLElement | null> {
20+
return headings.map((heading) =>
21+
heading.url.length > 1
22+
? document.getElementById(heading.url.slice(1))
23+
: null
2624
);
2725
}
2826

2927
/**
3028
* Sets up Table of Contents highlighting.
3129
*/
32-
export function useTocHighlight() {
30+
export function useTocHighlight(headings: Toc) {
3331
const [currentIndex, setCurrentIndex] = useState<number>(0);
3432
const timeoutRef = useRef<number | null>(null);
3533

3634
useEffect(() => {
3735
function updateActiveLink() {
3836
const pageHeight = document.body.scrollHeight;
3937
const scrollPosition = window.scrollY + window.innerHeight;
40-
const headersAnchors = getHeaderAnchors();
38+
const targets = getTocTargets(headings);
4139

4240
if (scrollPosition >= 0 && pageHeight - scrollPosition <= 0) {
4341
// Scrolled to bottom of page.
44-
setCurrentIndex(headersAnchors.length - 1);
42+
setCurrentIndex(targets.length - 1);
4543
return;
4644
}
4745

48-
let index = -1;
49-
while (index < headersAnchors.length - 1) {
50-
const headerAnchor = headersAnchors[index + 1];
51-
const {top} = headerAnchor.getBoundingClientRect();
52-
53-
if (top >= TOP_OFFSET) {
46+
// The last heading that has scrolled past the top of the viewport.
47+
let index = 0;
48+
for (let i = 1; i < targets.length; i++) {
49+
const target = targets[i];
50+
if (target == null) {
51+
continue;
52+
}
53+
if (target.getBoundingClientRect().top >= TOP_OFFSET) {
5454
break;
5555
}
56-
index += 1;
56+
index = i;
5757
}
5858

59-
setCurrentIndex(Math.max(index, 0));
59+
setCurrentIndex(index);
6060
}
6161

6262
function throttledUpdateActiveLink() {
@@ -81,7 +81,7 @@ export function useTocHighlight() {
8181
document.removeEventListener('scroll', throttledUpdateActiveLink);
8282
document.removeEventListener('resize', throttledUpdateActiveLink);
8383
};
84-
}, []);
84+
}, [headings]);
8585

8686
return {
8787
currentIndex,

0 commit comments

Comments
 (0)