Skip to content

Commit 48ef7f1

Browse files
authored
fix: avoid TOC anchors being hidden by header (#468)
* fix: avoid toc headings being hidden by header * fix: preserve markdown heading classes
1 parent 1215599 commit 48ef7f1

3 files changed

Lines changed: 52 additions & 10 deletions

File tree

client/src/components/header.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useContext, useEffect, useState } from "react";
1+
import { useContext, useEffect, useRef, useState } from "react";
22
import { ProfileContext } from "../state/profile";
33
import { Padding } from "./padding";
44
import { useSiteConfig } from "../hooks/useSiteConfig";
@@ -13,6 +13,7 @@ export function Header({ children }: { children?: React.ReactNode }) {
1313
const layoutDefinition = getHeaderLayoutDefinition(headerLayout);
1414
const [isRevealed, setIsRevealed] = useState(true);
1515
const [isAtTop, setIsAtTop] = useState(true);
16+
const headerRef = useRef<HTMLDivElement | null>(null);
1617

1718
useEffect(() => {
1819
let lastScrollY = window.scrollY;
@@ -33,6 +34,31 @@ export function Header({ children }: { children?: React.ReactNode }) {
3334
};
3435
}, [headerBehavior]);
3536

37+
useEffect(() => {
38+
const root = document.documentElement;
39+
const setHeaderScrollOffset = () => {
40+
const headerHeight = headerRef.current?.getBoundingClientRect().height ?? 0;
41+
root.style.setProperty("--header-scroll-offset", `${Math.ceil(headerHeight + 16)}px`);
42+
};
43+
44+
setHeaderScrollOffset();
45+
46+
const resizeObserver = new ResizeObserver(() => {
47+
setHeaderScrollOffset();
48+
});
49+
50+
if (headerRef.current) {
51+
resizeObserver.observe(headerRef.current);
52+
}
53+
54+
window.addEventListener("resize", setHeaderScrollOffset);
55+
56+
return () => {
57+
resizeObserver.disconnect();
58+
window.removeEventListener("resize", setHeaderScrollOffset);
59+
};
60+
}, [headerBehavior, headerLayout]);
61+
3662
const useTopHeader = layoutDefinition.kind === "top";
3763
const headerPaddingClassName = headerLayout === "compact" ? "mx-0 mt-0" : "mx-4 mt-4";
3864
const containerClassName =
@@ -48,7 +74,7 @@ export function Header({ children }: { children?: React.ReactNode }) {
4874
{headerLayout === "compact" ? (
4975
<div className="pointer-events-none fixed inset-x-0 top-0 -z-10 h-64 bg-gradient-to-b from-theme/15 to-white/0 dark:from-theme/20 dark:to-transparent" />
5076
) : null}
51-
<div className={containerClassName}>
77+
<div ref={headerRef} className={containerClassName}>
5278
<div className="w-screen">
5379
{headerLayout === "compact" ? (
5480
<div className="w-full">

client/src/components/markdown.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -298,8 +298,9 @@ export function Markdown({ content }: { content: string }) {
298298
return (
299299
<h1
300300
id={children?.toString()}
301-
className="text-3xl font-bold mt-4"
302301
{...props}
302+
className={`${props.className || ""} text-3xl font-bold mt-4`.trim()}
303+
style={{ ...props.style, scrollMarginTop: "var(--header-scroll-offset, 7rem)" }}
303304
>
304305
{children}
305306
</h1>
@@ -309,8 +310,9 @@ export function Markdown({ content }: { content: string }) {
309310
return (
310311
<h2
311312
id={children?.toString()}
312-
className="text-2xl font-bold mt-4"
313313
{...props}
314+
className={`${props.className || ""} text-2xl font-bold mt-4`.trim()}
315+
style={{ ...props.style, scrollMarginTop: "var(--header-scroll-offset, 7rem)" }}
314316
>
315317
{children}
316318
</h2>
@@ -320,8 +322,9 @@ export function Markdown({ content }: { content: string }) {
320322
return (
321323
<h3
322324
id={children?.toString()}
323-
className="text-xl font-bold mt-4"
324325
{...props}
326+
className={`${props.className || ""} text-xl font-bold mt-4`.trim()}
327+
style={{ ...props.style, scrollMarginTop: "var(--header-scroll-offset, 7rem)" }}
325328
>
326329
{children}
327330
</h3>
@@ -331,8 +334,9 @@ export function Markdown({ content }: { content: string }) {
331334
return (
332335
<h4
333336
id={children?.toString()}
334-
className="text-lg font-bold mt-4"
335337
{...props}
338+
className={`${props.className || ""} text-lg font-bold mt-4`.trim()}
339+
style={{ ...props.style, scrollMarginTop: "var(--header-scroll-offset, 7rem)" }}
336340
>
337341
{children}
338342
</h4>
@@ -342,8 +346,9 @@ export function Markdown({ content }: { content: string }) {
342346
return (
343347
<h5
344348
id={children?.toString()}
345-
className="text-base font-bold mt-4"
346349
{...props}
350+
className={`${props.className || ""} text-base font-bold mt-4`.trim()}
351+
style={{ ...props.style, scrollMarginTop: "var(--header-scroll-offset, 7rem)" }}
347352
>
348353
{children}
349354
</h5>
@@ -353,8 +358,9 @@ export function Markdown({ content }: { content: string }) {
353358
return (
354359
<h6
355360
id={children?.toString()}
356-
className="text-sm font-bold mt-4"
357361
{...props}
362+
className={`${props.className || ""} text-sm font-bold mt-4`.trim()}
363+
style={{ ...props.style, scrollMarginTop: "var(--header-scroll-offset, 7rem)" }}
358364
>
359365
{children}
360366
</h6>

client/src/hooks/useTableOfContents.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ export interface TableOfContent {
88
element: HTMLElement
99
}
1010

11+
const getHeaderScrollOffset = () => {
12+
const rawValue = getComputedStyle(document.documentElement)
13+
.getPropertyValue('--header-scroll-offset')
14+
.trim()
15+
const offset = Number.parseFloat(rawValue)
16+
return Number.isFinite(offset) ? offset : 0
17+
}
18+
1119
const useTableOfContents = (selector: string) => {
1220
const intersectingListRef = useRef<boolean[]>([]) // isIntersecting array
1321
const [tableOfContents, setTableOfContents] = useState<TableOfContent[]>([])
@@ -86,9 +94,11 @@ const useTableOfContents = (selector: string) => {
8694
className={`cursor-pointer hover:opacity-50 ${activeIndex === item.index ? "text-theme" : ""}`}
8795
style={{ marginLeft: item.marginLeft }}
8896
onClick={() => {
89-
item.element.scrollIntoView({
97+
const top = item.element.getBoundingClientRect().top + window.scrollY - getHeaderScrollOffset()
98+
window.scrollTo({
99+
top: Math.max(top, 0),
90100
behavior: 'smooth'
91-
});
101+
})
92102
}}
93103
>
94104
{item.text}

0 commit comments

Comments
 (0)