Skip to main content
Glama
useActiveSection.ts3.85 kB
import { useGetElementOrWindow } from '@intlayer/design-system/hooks'; import { type RefObject, useEffect, useState } from 'react'; type UseActiveSectionOptions = { contentElement: HTMLElement | null; /** All headings to track */ headings: HTMLElement[]; /** Map of parent headings to their children */ headingMap: Map<HTMLElement, HTMLElement[]>; /** Optional ref to navigation element for click tracking */ navRef?: RefObject<HTMLElement | null>; /** Offset from top of viewport to consider a heading active (default: 1/3 of viewport height) */ scrollOffset?: number; }; type UseActiveSectionReturn = { /** Currently active parent heading */ activeParent: HTMLElement | null; /** Currently active child heading */ activeChild: HTMLElement | null; }; /** * Custom hook to detect and track the currently active section based on scroll position * @param options Configuration options for the hook * @returns Object containing active parent and child headings */ export const useActiveSection = ({ contentElement, headings, headingMap, navRef, scrollOffset, }: UseActiveSectionOptions): UseActiveSectionReturn => { const containerElement = useGetElementOrWindow(contentElement ?? undefined); const [activeParent, setActiveParent] = useState<HTMLElement | null>(null); const [activeChild, setActiveChild] = useState<HTMLElement | null>(null); useEffect(() => { const getActiveSection = () => { // Check if we're using a scrollable container or window const isWindow = !contentElement; const offset = scrollOffset ?? (isWindow ? window.innerHeight / 3 : (contentElement?.clientHeight ?? 0) / 3); const scrollPosition = isWindow ? window.scrollY : (contentElement?.scrollTop ?? 0); const scrollY = scrollPosition + offset; // Find the last heading that is above the scroll position const newActiveParent = headings.findLast((heading) => { const headingTop = isWindow ? heading.offsetTop : heading.offsetTop - (contentElement?.offsetTop ?? 0); return headingTop < scrollY; }); if (newActiveParent) { if (newActiveParent.id !== activeParent?.id) { setActiveParent(newActiveParent); } // Find active child within the active parent's children const children = headingMap.get(newActiveParent) ?? []; const activeChildHeading = children.findLast((child) => { const childTop = isWindow ? child.offsetTop : child.offsetTop - (contentElement?.offsetTop ?? 0); return childTop < scrollY; }); setActiveChild(activeChildHeading ?? null); } else { setActiveParent(null); setActiveChild(null); } }; // Initial detection getActiveSection(); const navigationElement = navRef?.current; // Event listeners for various triggers navigationElement?.addEventListener('click', getActiveSection); containerElement?.addEventListener('scroll', getActiveSection, { passive: true, }); containerElement?.addEventListener('resize', getActiveSection, { passive: true, }); containerElement?.addEventListener('orientationchange', getActiveSection); return () => { navigationElement?.removeEventListener('click', getActiveSection); containerElement?.removeEventListener('scroll', getActiveSection); containerElement?.removeEventListener('resize', getActiveSection); containerElement?.removeEventListener( 'orientationchange', getActiveSection ); }; }, [ contentElement, containerElement, headings, headingMap, activeParent, navRef, scrollOffset, ]); return { activeParent, activeChild, }; };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aymericzip/intlayer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server