From ac7b270685d3b89c3d0b17dc4c7c37cd3cf3c1f4 Mon Sep 17 00:00:00 2001 From: Simao Rodrigues Date: Thu, 15 Feb 2024 15:20:54 +0000 Subject: [PATCH 1/8] Add useScrollSpy hook --- frontend/src/hooks/use-scroll-spy.ts | 72 ++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 frontend/src/hooks/use-scroll-spy.ts diff --git a/frontend/src/hooks/use-scroll-spy.ts b/frontend/src/hooks/use-scroll-spy.ts new file mode 100644 index 00000000..330ccc44 --- /dev/null +++ b/frontend/src/hooks/use-scroll-spy.ts @@ -0,0 +1,72 @@ +import { RefObject, useLayoutEffect, useState } from 'react'; + +type ScrollSpyItem = { + id: string; + ref: RefObject; +}; + +type ScrollSpyOptions = { + overBounds: boolean; +}; + +const DEFAULT_OPTIONS = { + overBounds: true, + thresholdPercentage: 50, +}; + +const useScrollSpy = (items: ScrollSpyItem[], options?: ScrollSpyOptions) => { + const [activeId, setActiveId] = useState(null); + + useLayoutEffect(() => { + const scrollListener = () => { + const windowHeight = window.innerHeight; + const viewingPosition = (DEFAULT_OPTIONS.thresholdPercentage * windowHeight) / 100; + + const itemsPositions = items.map(({ id, ref }) => { + const element = ref?.current; + if (!element) return null; + + const boundingRect = element.getBoundingClientRect(); + + return { + id, + top: boundingRect.top, + bottom: boundingRect.bottom, + }; + }); + + const activeItem = itemsPositions.find(({ top: itemTop, bottom: itemBottom }) => { + return itemTop < viewingPosition && itemBottom > viewingPosition; + }); + + if (!activeItem?.id && (options?.overBounds || DEFAULT_OPTIONS.overBounds) === true) { + const firstItem = itemsPositions[0]; + const lastItem = itemsPositions[itemsPositions.length - 1]; + + if (viewingPosition < firstItem.top) { + setActiveId(firstItem.id); + } + + if (viewingPosition > lastItem.bottom) { + setActiveId(lastItem.id); + } + } else { + setActiveId(activeItem?.id); + } + }; + + scrollListener(); + + window.addEventListener('resize', scrollListener); + window.addEventListener('scroll', scrollListener); + + return () => { + window.removeEventListener('resize', scrollListener); + window.removeEventListener('scroll', scrollListener); + }; + }, [items, options]); + + return activeId; +}; + +export default useScrollSpy; From dd473c039d47b49bab410470d3df792531d0c0b1 Mon Sep 17 00:00:00 2001 From: Simao Rodrigues Date: Thu, 15 Feb 2024 15:21:59 +0000 Subject: [PATCH 2/8] Add support to the static page layout to use spy scroll --- frontend/src/layouts/static-page.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/layouts/static-page.tsx b/frontend/src/layouts/static-page.tsx index dce992c3..8667da04 100644 --- a/frontend/src/layouts/static-page.tsx +++ b/frontend/src/layouts/static-page.tsx @@ -10,13 +10,15 @@ import ArrowRight from '@/styles/icons/arrow-right.svg?sprite'; type SidebarProps = { sections: { [key: string]: { + id: string; name: string; ref: MutableRefObject; }; }; + activeSection?: string; }; -const Sidebar: React.FC = ({ sections }) => { +const Sidebar: React.FC = ({ sections, activeSection }) => { if (!sections) return null; const handleClick = (key) => { @@ -28,7 +30,7 @@ const Sidebar: React.FC = ({ sections }) => { return (