diff --git a/site/hooks/useHeadingsObserver.js b/site/hooks/useHeadingsObserver.js index e197c02..81d2337 100644 --- a/site/hooks/useHeadingsObserver.js +++ b/site/hooks/useHeadingsObserver.js @@ -1,35 +1,43 @@ import { useState, useEffect } from "react"; -/* Creates an Intersection Observer to keep track of headings intersecting - * a section of the viewport defined by the rootMargin */ -const getIntersectionObserver = (callback) => { - return; -}; - const useHeadingsObserver = () => { - const [activeHeading, setActiveHeading] = useState(""); const [observer, setObserver] = useState(null); + const activeHeading = ""; + const timeoutId = null; + /* Runs only after the first render, in order to preserve the observer - * between component rerenderings (e.g. after activeHeading change). */ + * between component rerenderings. */ useEffect(() => { const observer = new IntersectionObserver( (entries) => { - // entries.forEach((entry) => { - // if (entry.isIntersecting) { - // setActiveHeading(entry.target.id); - // } - // }); + // highlight only the first intersecting heading in the ToC const firstIntersectingHeading = entries.find( (entry) => entry.isIntersecting ); - if (firstIntersectingHeading) { - setActiveHeading(firstIntersectingHeading.target.id); + const newActiveHeading = firstIntersectingHeading?.target.id; + if (!newActiveHeading || activeHeading === newActiveHeading) { + return; } + if (timeoutId) { + clearTimeout(timeoutId); + } + timeoutId = setTimeout(() => { + // remove highlight of the previous active heading in the ToC + document + .querySelector(`.toc-link[href="#${activeHeading}"]`) + ?.classList.remove("active"); + + // add highlight to the new active heading in the ToC + document + .querySelector(`.toc-link[href="#${newActiveHeading}"]`) + ?.classList.add("active"); + + activeHeading = newActiveHeading; + }, 250); }, { root: null, - threshold: 0.55, rootMargin: "-65px 0% -85% 0%", // 65px is a navbar height } ); @@ -41,21 +49,6 @@ const useHeadingsObserver = () => { }; }, []); - useEffect(() => { - if (!activeHeading) { - return; - } - - const tocLink = document.querySelector( - `.toc-link[href="#${activeHeading}"]` - ); - tocLink.classList.add("active"); - - return () => { - tocLink.classList.remove("active"); - }; - }, [activeHeading]); - return observer; }; diff --git a/site/styles/global.css b/site/styles/global.css index 0cb4b65..357cc57 100644 --- a/site/styles/global.css +++ b/site/styles/global.css @@ -90,19 +90,25 @@ body { @apply p-0; } -.toc-link { - @apply transition-colors; -} - /* link (a) element */ -.toc-item .toc-link { - @apply text-sm text-slate-400 no-underline break-normal +.toc-link { + @apply transition-colors relative text-sm text-slate-400 no-underline break-normal; } -.toc-item .toc-link:not(.active) { - @apply hover:text-white +.toc-link:not(.active) { + @apply hover:text-white; } .toc-link.active { - @apply text-yellow-li + @apply text-yellow-li; +} + +.toc-link::before { + position: absolute; + content: ""; + @apply transition-opacity opacity-0 border-l border-yellow-li h-[110%] -top-[5%] -left-[0.5rem]; +} + +.toc-link.active::before { + @apply opacity-100; }