From a2d7dbac6d959266f64677e18219834aab4cbf88 Mon Sep 17 00:00:00 2001 From: Ashish Padhy <100484401+Shurtu-gal@users.noreply.github.com> Date: Wed, 8 Nov 2023 14:53:43 +0530 Subject: [PATCH] feat: add collapsing and custom hook --- components/CaseTOC.js | 132 ++++++++++++++++++---- components/helpers/useHeadingsObserver.js | 36 ++++++ 2 files changed, 143 insertions(+), 25 deletions(-) create mode 100644 components/helpers/useHeadingsObserver.js diff --git a/components/CaseTOC.js b/components/CaseTOC.js index 1104b6502eef..4af86af88049 100644 --- a/components/CaseTOC.js +++ b/components/CaseTOC.js @@ -1,12 +1,13 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import Scrollspy from "react-scrollspy"; import { twMerge } from "tailwind-merge"; import ArrowRight from "./icons/ArrowRight"; +import { useHeadingsObserver } from "./helpers/useHeadingsObserver"; const convertContentToTocItems = (content, level = 1) => { const tocItems = []; - for(let section of content) { + for (let section of content) { const item = { lvl: level, content: section.title, @@ -16,16 +17,67 @@ const convertContentToTocItems = (content, level = 1) => { .toLowerCase(), }; - tocItems.push(item); if (section.children && section.children.length > 0) { const children = convertContentToTocItems(section.children, level + 1); - tocItems.push(...children); + item.children = children; } + + tocItems.push(item); } return tocItems; }; +function TOCItem({ item, index, currSelected, closeMenu }) { + const [open, setOpen] = useState(false); + const handleClick = () => { + closeMenu(); + setOpen(false); + }; + + return ( + <> + {item.children && item.children.length > 0 && ( + setOpen(!open)}> + + + )} + + {item.content} + + {item.children && item.children.length > 0 && ( + + )} + + ); +} + export default function CaseTOC({ className, cssBreakingPoint = "xl", @@ -33,40 +85,70 @@ export default function CaseTOC({ contentSelector, }) { if (!toc || !toc.length) return null; - const tocItems = convertContentToTocItems(toc); - + const tocItems = useMemo(() => convertContentToTocItems(toc), [toc]); const [open, setOpen] = useState(false); + const { currActive: selected } = useHeadingsObserver(); return ( -
setOpen(!open)}> -
-
+
+
+
On this page
-
- +
setOpen(!open)} + > +
-
+
item.slug)} + items={tocItems.map((item) => item.slug)} currentClassName="text-primary-500 font-bold" componentTag="div" rootEl={contentSelector} offset={-120} > - { - tocItems.map((item, index) => ( - - {item.content} - - )) - } + {tocItems.map((item, index) => ( + setOpen(false)} + currSelected={selected} + /> + ))}
diff --git a/components/helpers/useHeadingsObserver.js b/components/helpers/useHeadingsObserver.js new file mode 100644 index 000000000000..56de62808c8b --- /dev/null +++ b/components/helpers/useHeadingsObserver.js @@ -0,0 +1,36 @@ +import { useEffect, useRef, useState } from "react"; + +/** + * @description Custom hook to observe headings and set the current active heading + * @example const { currActive } = useHeadingsObserver(); + * @returns {object} currActive - current active heading + */ +export function useHeadingsObserver() { + const observer = useRef(null); + const headingsRef = useRef([]); + const [currActive, setCurrActive] = useState(null); + + useEffect(() => { + const callback = (entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + setCurrActive(entry.target.id); + } + }) + } + + // The heading in from top 20% of the viewport to top 30% of the viewport will be considered as active + observer.current = new IntersectionObserver(callback, { + rootMargin: '-20% 0px -70% 0px', + }); + + headingsRef.current = document.querySelectorAll('h2, h3'); + headingsRef.current.forEach(heading => { + observer.current.observe(heading); + }) + + return () => observer.current.disconnect(); + }, []); + + return { currActive } +}