Improve TOC synchronization with contentKey prop
Better fix for TOC showing previous article headings. The issue was relying on pathname which could be out of sync with the actual content. Changes: - Pass contentKey as prop to PostToc instead of using usePathname() - Use contentKey in useEffect dependency for more reliable updates - Replace setTimeout with double requestAnimationFrame for DOM sync - Remove unused usePathname import This ensures the TOC effect runs exactly when the content changes, not just when the URL changes, providing more reliable synchronization between the TOC and the article content. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -56,7 +56,7 @@ export function PostLayout({ children, hasToc = true, contentKey }: { children:
|
||||
<div className="sticky top-24 h-[calc(100vh-6rem)] overflow-hidden">
|
||||
{isTocOpen && hasToc && (
|
||||
<div className="toc-sidebar h-full overflow-y-auto pr-2">
|
||||
<PostToc key={contentKey} />
|
||||
<PostToc contentKey={contentKey} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -67,7 +67,7 @@ export function PostLayout({ children, hasToc = true, contentKey }: { children:
|
||||
{isTocOpen && hasToc && (
|
||||
<div className="toc-mobile fixed bottom-24 right-4 z-40 w-72 rounded-2xl border border-white/20 bg-white/90 p-6 shadow-2xl backdrop-blur-xl dark:border-white/10 dark:bg-slate-900/90 lg:hidden">
|
||||
<div className="max-h-[60vh] overflow-y-auto">
|
||||
<PostToc key={contentKey} onLinkClick={() => setIsTocOpen(false)} />
|
||||
<PostToc contentKey={contentKey} onLinkClick={() => setIsTocOpen(false)} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { FiList } from 'react-icons/fi';
|
||||
|
||||
interface TocItem {
|
||||
@@ -10,63 +9,68 @@ interface TocItem {
|
||||
depth: number;
|
||||
}
|
||||
|
||||
export function PostToc({ onLinkClick }: { onLinkClick?: () => void }) {
|
||||
export function PostToc({ onLinkClick, contentKey }: { onLinkClick?: () => void; contentKey?: string }) {
|
||||
const [items, setItems] = useState<TocItem[]>([]);
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const listRef = useRef<HTMLDivElement | null>(null);
|
||||
const itemRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
||||
const [indicator, setIndicator] = useState({ top: 0, opacity: 0 });
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
// Clear items immediately when pathname changes
|
||||
// Clear items immediately when content changes
|
||||
setItems([]);
|
||||
setActiveId(null);
|
||||
|
||||
let observer: IntersectionObserver | null = null;
|
||||
let rafId1: number;
|
||||
let rafId2: number;
|
||||
|
||||
// Small delay to ensure DOM has been updated with new article content
|
||||
const timeoutId = setTimeout(() => {
|
||||
const headings = Array.from(
|
||||
document.querySelectorAll<HTMLElement>('article h2, article h3')
|
||||
);
|
||||
const mapped = headings
|
||||
.filter((el) => el.id)
|
||||
.map((el) => ({
|
||||
id: el.id,
|
||||
text: el.innerText,
|
||||
depth: el.tagName === 'H3' ? 3 : 2
|
||||
}));
|
||||
setItems(mapped);
|
||||
// Use double requestAnimationFrame to ensure DOM has been painted
|
||||
// This is more reliable than setTimeout for DOM updates
|
||||
rafId1 = requestAnimationFrame(() => {
|
||||
rafId2 = requestAnimationFrame(() => {
|
||||
const headings = Array.from(
|
||||
document.querySelectorAll<HTMLElement>('article h2, article h3')
|
||||
);
|
||||
const mapped = headings
|
||||
.filter((el) => el.id)
|
||||
.map((el) => ({
|
||||
id: el.id,
|
||||
text: el.innerText,
|
||||
depth: el.tagName === 'H3' ? 3 : 2
|
||||
}));
|
||||
setItems(mapped);
|
||||
|
||||
observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const id = (entry.target as HTMLElement).id;
|
||||
if (id) {
|
||||
setActiveId(id);
|
||||
observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const id = (entry.target as HTMLElement).id;
|
||||
if (id) {
|
||||
setActiveId(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
// Trigger when heading is in upper 40% of viewport
|
||||
rootMargin: '0px 0px -60% 0px',
|
||||
threshold: 0.1
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
{
|
||||
// Trigger when heading is in upper 40% of viewport
|
||||
rootMargin: '0px 0px -60% 0px',
|
||||
threshold: 0.1
|
||||
}
|
||||
);
|
||||
|
||||
headings.forEach((el) => observer?.observe(el));
|
||||
}, 50); // 50ms delay to ensure DOM is updated
|
||||
headings.forEach((el) => observer?.observe(el));
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
cancelAnimationFrame(rafId1);
|
||||
cancelAnimationFrame(rafId2);
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
}
|
||||
};
|
||||
}, [pathname]);
|
||||
}, [contentKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeId || !listRef.current) {
|
||||
|
||||
Reference in New Issue
Block a user