Vercel/src/components/AnchorSections.tsx
2025-08-28 09:23:49 -05:00

105 lines
2.4 KiB
TypeScript

import useHash from '@/utility/useHash';
import useVisibility from '@/utility/useVisibility';
import {
ReactNode,
useCallback,
useEffect,
useMemo,
useRef,
} from 'react';
import { useDebouncedCallback } from 'use-debounce';
export default function AnchorSections({
sections,
className,
classNameSection,
}: {
sections: {
id: string
content: ReactNode
}[]
className?: string
classNameSection?: string
}) {
const { hash, updateHash } = useHash();
const isAutoSelectDisabled = useRef(false);
const firstSection = useMemo(() => sections[0].id, [sections]);
// Highlight initial section
useEffect(() => {
updateHash(firstSection);
}, [updateHash, firstSection]);
// Disable auto-select for 100ms after hash
useEffect(() => {
isAutoSelectDisabled.current = true;
const timeout = setTimeout(() => {
isAutoSelectDisabled.current = false;
}, 100);
return () => clearTimeout(timeout);
}, [hash]);
// Reset section when scrolled to the top
const _onScroll = useCallback(() => {
if (window.scrollY <= 0) {
updateHash(firstSection);
}
}, [updateHash, firstSection]);
const onScroll = useDebouncedCallback(_onScroll, 100, { leading: true });
useEffect(() => {
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, [onScroll]);
const onVisible = useCallback((section: string) => {
if (!isAutoSelectDisabled.current) {
updateHash(section);
}
}, [updateHash]);
return (
<div className={className}>
{sections.map(({ id, content }) => (
<AnchorSection
key={id}
id={id}
className={classNameSection}
onVisible={onVisible}
>
{content}
</AnchorSection>
))}
</div>
);
}
function AnchorSection({
id,
children,
onVisible: _onVisible,
onHidden: _onHidden,
className,
}: {
id: string
children: ReactNode
onVisible?: (section: string, force?: boolean) => void
onHidden?: (section: string, force?: boolean) => void
className?: string
}) {
const ref = useRef<HTMLDivElement>(null);
const onVisible = useCallback(() => _onVisible?.(id), [id, _onVisible]);
const onHidden = useCallback(() => _onHidden?.(id), [id, _onHidden]);
useVisibility({ ref, onVisible, onHidden });
return (
<div ref={ref} {...{ id, className }}>
<a href={`#${id}`} />
{children}
</div>
);
}