feat: highlight side nav items on scroll

This commit is contained in:
bitroy
2024-06-12 23:18:41 +05:30
parent e6f9a53c34
commit d1bacd6c0e
4 changed files with 54 additions and 5 deletions

View File

@@ -115,7 +115,7 @@ const SmallPrint = () => {
export const Footer = () => {
return (
<footer className="mx-auto max-w-2xl space-y-10 pb-16 lg:w-[calc(100%-20rem)]">
<footer className="mb-10 mt-10 flex-auto pb-16">
<PageNavigation />
<SmallPrint />
</footer>

View File

@@ -36,8 +36,8 @@ export const Layout = ({
</div>
</motion.header>
<div className="flex h-screen flex-col">
<div className="relative flex flex-1 flex-col px-4 pt-14 sm:px-6 lg:px-8">
<main className="flex-auto overflow-y-auto lg:w-[calc(100%-20rem)]">{children}</main>
<div className="flex flex-col px-4 pt-14 sm:px-6 lg:w-[calc(100%-20rem)] lg:px-8">
<main className="overflow-y-auto overflow-x-hidden">{children}</main>
<Footer />
</div>
<SideNavigation pathname={pathname} />

View File

@@ -1,3 +1,4 @@
import useTableContentObserver from "@/hooks/useTableContentObserver";
import Link from "next/link";
import { useEffect, useState } from "react";
@@ -11,6 +12,8 @@ const SideNavigation = ({ pathname }) => {
const [headings, setHeadings] = useState<Heading[]>([]);
const [selectedId, setSelectedId] = useState<string | null>(null);
useTableContentObserver(setSelectedId, pathname);
useEffect(() => {
const getHeadings = () => {
const headingElements = document.querySelectorAll("h2[id], h3[id], h4[id]");
@@ -20,7 +23,9 @@ const SideNavigation = ({ pathname }) => {
level: parseInt(heading.tagName.slice(1)),
}));
setHeadings(headings);
const hasH2 = headings.some((heading) => heading.level === 2);
setHeadings(hasH2 ? headings : []);
};
getHeadings();
@@ -62,7 +67,7 @@ const SideNavigation = ({ pathname }) => {
return (
<aside className="fixed right-0 top-0 hidden h-[calc(100%-2.5rem)] w-80 overflow-hidden overflow-y-auto pt-16 [scrollbar-width:none] lg:mt-10 lg:block">
<div className="border-l-2 border-gray-700">
<h3 className="ml-2 mt-1">On this page</h3>
<h3 className="ml-2 mt-1 uppercase">on this page</h3>
{renderHeading(headings, 2)}
</div>
</aside>

View File

@@ -0,0 +1,44 @@
import { useEffect, useRef } from "react";
const useTableContentObserver = (setActiveId, pathname) => {
const headingElementsRef = useRef({});
useEffect(() => {
const callback = (headings) => {
headingElementsRef.current = headings.reduce((map, headingElement) => {
return { ...map, [headingElement.target.id]: headingElement };
}, {});
const visibleHeadings = [];
Object.keys(headingElementsRef.current).forEach((key) => {
const headingElement = headingElementsRef.current[key];
if (headingElement.isIntersecting) visibleHeadings.push(headingElement);
});
const getIndexFromId = (id) => headingElements.findIndex((heading) => heading.id === id);
if (visibleHeadings.length === 1) {
setActiveId(visibleHeadings[0].target.id);
} else if (visibleHeadings.length > 1) {
const sortedVisibleHeadings = visibleHeadings.sort(
(a, b) => getIndexFromId(a.target.id) > getIndexFromId(b.target.id)
);
setActiveId(sortedVisibleHeadings[0].target.id);
}
};
const observer = new IntersectionObserver(callback, {
rootMargin: "-40px 0px -40% 0px",
});
const headingElements = Array.from(document.querySelectorAll("h2[id], h3[id], h4[id]"));
headingElements.forEach((element) => observer.observe(element));
return () => {
observer.disconnect();
headingElementsRef.current = {};
};
}, [setActiveId, pathname]);
};
export default useTableContentObserver;