| import { useState, useEffect, useRef, useCallback } from 'react'; |
|
|
| interface ScrollTrackingItem { |
| id: string; |
| } |
|
|
| interface UseScrollTrackingOptions<T extends ScrollTrackingItem> { |
| |
| items: T[]; |
| |
| filterFn?: (item: T) => boolean; |
| |
| initialSection?: string; |
| |
| scrollOffset?: number; |
| } |
|
|
| |
| |
| |
| |
| |
| export function useScrollTracking<T extends ScrollTrackingItem>({ |
| items, |
| filterFn = () => true, |
| initialSection, |
| scrollOffset = 24, |
| }: UseScrollTrackingOptions<T>) { |
| const [activeSection, setActiveSection] = useState(initialSection || items[0]?.id || ''); |
| const scrollContainerRef = useRef<HTMLDivElement>(null); |
|
|
| |
| useEffect(() => { |
| const container = scrollContainerRef.current; |
| if (!container) return; |
|
|
| const handleScroll = () => { |
| const sections = items |
| .filter(filterFn) |
| .map((item) => ({ |
| id: item.id, |
| element: document.getElementById(item.id), |
| })) |
| .filter((s) => s.element); |
|
|
| const containerRect = container.getBoundingClientRect(); |
| const scrollTop = container.scrollTop; |
| const scrollHeight = container.scrollHeight; |
| const clientHeight = container.clientHeight; |
|
|
| |
| const isAtBottom = scrollTop + clientHeight >= scrollHeight - 50; |
|
|
| if (isAtBottom && sections.length > 0) { |
| |
| setActiveSection(sections[sections.length - 1].id); |
| return; |
| } |
|
|
| for (let i = sections.length - 1; i >= 0; i--) { |
| const section = sections[i]; |
| if (section.element) { |
| const rect = section.element.getBoundingClientRect(); |
| const relativeTop = rect.top - containerRect.top + scrollTop; |
| if (scrollTop >= relativeTop - 100) { |
| setActiveSection(section.id); |
| break; |
| } |
| } |
| } |
| }; |
|
|
| container.addEventListener('scroll', handleScroll); |
| return () => container.removeEventListener('scroll', handleScroll); |
| }, [items, filterFn]); |
|
|
| |
| const scrollToSection = useCallback( |
| (sectionId: string) => { |
| const element = document.getElementById(sectionId); |
| if (element && scrollContainerRef.current) { |
| const container = scrollContainerRef.current; |
| const containerRect = container.getBoundingClientRect(); |
| const elementRect = element.getBoundingClientRect(); |
| const relativeTop = elementRect.top - containerRect.top + container.scrollTop; |
|
|
| container.scrollTo({ |
| top: relativeTop - scrollOffset, |
| behavior: 'smooth', |
| }); |
| } |
| }, |
| [scrollOffset] |
| ); |
|
|
| return { |
| activeSection, |
| scrollToSection, |
| scrollContainerRef, |
| }; |
| } |
|
|