Skip to content

Commit f5997ad

Browse files
fix: scroll to content from TOC not working in the code tab (better-auth#251)
Co-authored-by: Maxwell <145994855+ping-maxwell@users.noreply.github.com>
1 parent c6ff5fc commit f5997ad

3 files changed

Lines changed: 61 additions & 24 deletions

File tree

apps/web/src/components/repo/code-content-wrapper.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ export function CodeContentWrapper({
342342
</div>
343343
)}
344344
<div
345+
data-scroll-container
345346
className={cn(
346347
"flex-1 min-h-0",
347348
isDetailRoute

apps/web/src/components/repo/document-outline.tsx

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ interface HeadingNode {
1616
}
1717

1818
interface DocumentOutlineProps {
19-
/** Ref to the container element that holds the rendered markdown (.ghmd) */
20-
contentRef: React.RefObject<HTMLDivElement | null>;
2119
/** Whether the outline should be visible (only in preview mode) */
2220
visible: boolean;
2321
}
@@ -168,7 +166,7 @@ function FlatOutlineItem({
168166
// Main component
169167
// ---------------------------------------------------------------------------
170168

171-
export function DocumentOutline({ contentRef, visible }: DocumentOutlineProps) {
169+
export function DocumentOutline({ visible }: DocumentOutlineProps) {
172170
const [headings, setHeadings] = useState<HeadingNode[]>([]);
173171
const [activeId, setActiveId] = useState<string | null>(null);
174172
const [isExpanded, setIsExpanded] = useState(() => {
@@ -180,6 +178,12 @@ export function DocumentOutline({ contentRef, visible }: DocumentOutlineProps) {
180178
const [focusedIndex, setFocusedIndex] = useState(-1);
181179
const [mobileOpen, setMobileOpen] = useState(false);
182180

181+
const scrollContainerRef = useRef<HTMLElement | null>(null);
182+
useEffect(() => {
183+
scrollContainerRef.current =
184+
document.querySelector<HTMLElement>("[data-scroll-container]") ?? null;
185+
}, []);
186+
183187
const outlineRef = useRef<HTMLDivElement>(null);
184188
const listRef = useRef<HTMLDivElement>(null);
185189
const searchInputRef = useRef<HTMLInputElement>(null);
@@ -195,7 +199,7 @@ export function DocumentOutline({ contentRef, visible }: DocumentOutlineProps) {
195199
// Extract headings from rendered content
196200
useEffect(() => {
197201
if (!visible) return;
198-
const container = contentRef.current;
202+
const container = scrollContainerRef.current;
199203
if (!container) return;
200204

201205
// Wait for next frame to ensure markdown is rendered
@@ -207,27 +211,46 @@ export function DocumentOutline({ contentRef, visible }: DocumentOutlineProps) {
207211
});
208212

209213
return () => cancelAnimationFrame(raf);
210-
}, [contentRef, visible]);
214+
}, [visible]);
211215

212216
// Deep-link: scroll to hash on mount
213217
useEffect(() => {
214-
if (!visible || headings.length === 0) return;
218+
if (!visible) return;
215219
const hash = window.location.hash.slice(1);
216-
if (hash) {
220+
if (!hash) return;
221+
222+
const container = scrollContainerRef.current;
223+
if (!container) return;
224+
225+
const scrollParent =
226+
container.closest<HTMLElement>("[data-scroll-container]") ?? null;
227+
228+
const raf = requestAnimationFrame(() => {
217229
const el = document.getElementById(hash);
218-
if (el) {
219-
setTimeout(() => {
220-
el.scrollIntoView({ behavior: "smooth", block: "start" });
221-
}, 100);
222-
setActiveId(hash);
230+
if (!el) return;
231+
232+
if (scrollParent) {
233+
const containerRect = scrollParent.getBoundingClientRect();
234+
const elRect = el.getBoundingClientRect();
235+
scrollParent.scrollTop =
236+
scrollParent.scrollTop +
237+
elRect.top -
238+
containerRect.top -
239+
SCROLL_OFFSET;
240+
} else {
241+
el.scrollIntoView({ block: "start" });
223242
}
224-
}
225-
}, [visible, headings]);
243+
244+
setActiveId(hash);
245+
});
246+
247+
return () => cancelAnimationFrame(raf);
248+
}, [visible]);
226249

227250
// Scroll tracking with IntersectionObserver
228251
useEffect(() => {
229252
if (!visible || headings.length === 0) return;
230-
const container = contentRef.current;
253+
const container = scrollContainerRef.current;
231254
if (!container) return;
232255

233256
const flat = flattenTree(headings);
@@ -240,7 +263,7 @@ export function DocumentOutline({ contentRef, visible }: DocumentOutlineProps) {
240263

241264
// Find the scroll parent
242265
const scrollParent =
243-
container.closest<HTMLElement>("[class*='overflow-y-auto']") ?? window;
266+
container.closest<HTMLElement>("[data-scroll-container]") ?? window;
244267

245268
if ("IntersectionObserver" in window) {
246269
const rootEl = scrollParent instanceof HTMLElement ? scrollParent : null;
@@ -366,7 +389,7 @@ export function DocumentOutline({ contentRef, visible }: DocumentOutlineProps) {
366389
clearTimeout(timer);
367390
target.removeEventListener("scroll", handleScroll);
368391
};
369-
}, [visible, headings, contentRef]);
392+
}, [visible, headings]);
370393

371394
// Auto-scroll outline to keep active item visible
372395
useEffect(() => {
@@ -391,14 +414,30 @@ export function DocumentOutline({ contentRef, visible }: DocumentOutlineProps) {
391414
const el = document.getElementById(id);
392415
if (!el) return;
393416

417+
const container = scrollContainerRef.current;
418+
if (!container) return;
419+
420+
const scrollParent =
421+
container.closest<HTMLElement>("[data-scroll-container]") ?? null;
422+
423+
if (scrollParent) {
424+
const containerRect = scrollParent.getBoundingClientRect();
425+
const elRect = el.getBoundingClientRect();
426+
const targetScrollTop =
427+
scrollParent.scrollTop +
428+
elRect.top -
429+
containerRect.top -
430+
SCROLL_OFFSET;
431+
scrollParent.scrollTop = targetScrollTop;
432+
} else {
433+
el.scrollIntoView({ behavior: "smooth", block: "start" });
434+
}
435+
394436
setActiveId(id);
395437
setMobileOpen(false);
396438

397439
// Update hash without full reload
398440
history.replaceState(null, "", `#${id}`);
399-
400-
// Smooth scroll
401-
el.scrollIntoView({ behavior: "smooth", block: "start" });
402441
}, []);
403442

404443
// Keyboard navigation

apps/web/src/components/repo/markdown-blob-view.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -205,10 +205,7 @@ export function MarkdownBlobView({
205205
<div ref={previewContainerRef} className="flex-1 min-w-0">
206206
{previewView}
207207
</div>
208-
<DocumentOutline
209-
contentRef={previewContainerRef}
210-
visible={mode === "preview"}
211-
/>
208+
<DocumentOutline visible={mode === "preview"} />
212209
</div>
213210
{mode === "edit" && (
214211
<div className="border border-border rounded-md">

0 commit comments

Comments
 (0)