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 && (
+
+ {item.children.map((item, index) => (
+
+ ))}
+
+ )}
+ >
+ );
+}
+
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
-
-
+
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 }
+}