Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Table of Contents component to documentation layout #1471

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
11 changes: 10 additions & 1 deletion components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import CarbonAds from './CarbonsAds';
import { useTheme } from 'next-themes';
import ExternalLinkIcon from '../public/icons/external-link-black.svg';
import Image from 'next/image';
import TableOfContents from './TableOfContents';
const DocLink = ({
uri,
label,
Expand Down Expand Up @@ -252,9 +253,17 @@ export const SidebarLayout = ({ children }: { children: React.ReactNode }) => {
/>
</div>
</div>
<div className='col-span-4 md:col-span-3 lg:mt-20 lg:w-5/6 mx-4 md:mx-0'>

<div
id='main-content'
className='col-span-4 md:col-span-2 lg:col-span-2 lg:mt-20 lg:w-5/6 mx-4 md:mx-0'
>
{children}
</div>

<div className='sticky top-16 hidden lg:block'>
<TableOfContents />
</div>
</div>
</section>
</div>
Expand Down
95 changes: 95 additions & 0 deletions components/TableOfContents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/router';

interface Heading {
id: string;
text: string;
level: number;
}

const TableOfContents: React.FC = () => {
const router = useRouter();
const [headings, setHeadings] = useState<Heading[]>([]);
const [activeId, setActiveId] = useState<string | null>(null);

useEffect(() => {
const mainContent = document.getElementById('main-content');
const elements = mainContent
? mainContent.querySelectorAll('h1, h2, h3, h4')
: [];
const newHeadings: Heading[] = [];

elements.forEach((el) => {
const text = el.textContent || '';
if (text.trim().toLowerCase() === 'on this page') return;
if (el.closest('#sidebar')) return;

const currentFolder = router.pathname.split('/').pop()?.toLowerCase();
if (text.trim().toLowerCase() === currentFolder) return;
if (
text.includes('/') ||
text.includes('\\') ||
/\.md$|\.tsx$|\.jsx$|\.js$/i.test(text.trim())
)
return;
const level = parseInt(el.tagName.replace('H', ''));
if (!el.id) {
const generatedId = text
.toLowerCase()
.trim()
.replace(/\s+/g, '-')
.replace(/[^\w-]+/g, '');
if (generatedId) {
el.id = generatedId;
}
}

newHeadings.push({
id: el.id,
text,
level,
});
});

setHeadings(newHeadings);

const handleScroll = () => {
let currentId: string | null = null;
elements.forEach((el) => {
const rect = (el as HTMLElement).getBoundingClientRect();
if (rect.top <= 100) {
currentId = el.id;
}
});
setActiveId(currentId);
};

window.addEventListener('scroll', handleScroll);
handleScroll();
return () => window.removeEventListener('scroll', handleScroll);
}, [router.asPath]);

return (
<nav className='w-full p-4 sticky top-24 overflow-y-auto scrollbar-hide'>
<h2 className='text-xl font-bold uppercase text-slate-400 mb-4'>
On this page
</h2>
<ul>
{headings.map((heading) => (
<li
key={heading.id}
className={`pl-${(heading.level - 1) * 2} ${
activeId === heading.id
? 'text-primary font-semibold'
: 'text-slate-600'
}`}
>
<a href={`#${heading.id}`}>{heading.text}</a>
</li>
))}
</ul>
</nav>
);
};

export default TableOfContents;