feat: add useSettingsSections hook with scroll-spy and smooth scrolling
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
import { useRef, useCallback, useLayoutEffect } from 'react';
|
||||
import { useSettingsSectionsContext, type SettingsSection } from '../components/modals/SettingsSectionsContext';
|
||||
|
||||
export function useSettingsSections(sections: SettingsSection[]) {
|
||||
const ctx = useSettingsSectionsContext();
|
||||
// scrollContainerRef is owned by the context provider, not by this hook.
|
||||
// UserSettings attaches it to the scroll container div, and this hook reads it.
|
||||
const scrollContainerRef = ctx?.scrollContainerRef ?? null;
|
||||
const sectionElementsRef = useRef(new Map<string, HTMLElement>());
|
||||
const sectionRefCallbacksRef = useRef(new Map<string, (el: HTMLElement | null) => void>());
|
||||
const suppressObserverRef = useRef(false);
|
||||
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||
|
||||
// Register sections into context synchronously (useLayoutEffect prevents flicker)
|
||||
useLayoutEffect(() => {
|
||||
if (ctx) {
|
||||
ctx.setSections(sections);
|
||||
}
|
||||
return () => {
|
||||
if (ctx) {
|
||||
ctx.setSections([]);
|
||||
ctx.setActiveSection('');
|
||||
}
|
||||
};
|
||||
}, [sections, ctx]);
|
||||
|
||||
// scrollToSection implementation
|
||||
const scrollToSection = useCallback((id: string) => {
|
||||
const el = sectionElementsRef.current.get(id);
|
||||
if (!el) return;
|
||||
|
||||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
suppressObserverRef.current = true;
|
||||
ctx?.setActiveSection(id);
|
||||
|
||||
el.scrollIntoView({
|
||||
behavior: prefersReducedMotion ? 'auto' : 'smooth',
|
||||
block: 'start',
|
||||
});
|
||||
|
||||
const container = scrollContainerRef?.current;
|
||||
if (container && 'onscrollend' in container) {
|
||||
container.addEventListener('scrollend', () => {
|
||||
suppressObserverRef.current = false;
|
||||
}, { once: true });
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
suppressObserverRef.current = false;
|
||||
}, prefersReducedMotion ? 50 : 500);
|
||||
}
|
||||
}, [ctx, scrollContainerRef]);
|
||||
|
||||
// Register scrollToSection into context
|
||||
useLayoutEffect(() => {
|
||||
if (ctx) {
|
||||
ctx.setScrollToSection(scrollToSection);
|
||||
}
|
||||
}, [scrollToSection, ctx]);
|
||||
|
||||
// Set up IntersectionObserver
|
||||
useLayoutEffect(() => {
|
||||
const container = scrollContainerRef?.current;
|
||||
if (!container || sections.length === 0) return;
|
||||
|
||||
observerRef.current = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (suppressObserverRef.current) return;
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
const id = entry.target.getAttribute('data-section-id');
|
||||
if (id && ctx) {
|
||||
ctx.setActiveSection(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
root: container,
|
||||
rootMargin: '-20% 0px -70% 0px',
|
||||
}
|
||||
);
|
||||
|
||||
// Observe all registered section elements
|
||||
sectionElementsRef.current.forEach((el) => {
|
||||
observerRef.current?.observe(el);
|
||||
});
|
||||
|
||||
return () => {
|
||||
observerRef.current?.disconnect();
|
||||
observerRef.current = null;
|
||||
};
|
||||
}, [sections, ctx, scrollContainerRef]);
|
||||
|
||||
// Stable callback ref factory (cached per id to avoid re-attach)
|
||||
const sectionRef = useCallback((id: string) => {
|
||||
let cb = sectionRefCallbacksRef.current.get(id);
|
||||
if (!cb) {
|
||||
cb = (el: HTMLElement | null) => {
|
||||
if (el) {
|
||||
el.setAttribute('data-section-id', id);
|
||||
sectionElementsRef.current.set(id, el);
|
||||
observerRef.current?.observe(el);
|
||||
} else {
|
||||
const prev = sectionElementsRef.current.get(id);
|
||||
if (prev) observerRef.current?.unobserve(prev);
|
||||
sectionElementsRef.current.delete(id);
|
||||
}
|
||||
};
|
||||
sectionRefCallbacksRef.current.set(id, cb);
|
||||
}
|
||||
return cb;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
sectionRef,
|
||||
scrollToSection,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user