Files
supabase/packages/common/hooks/useAnchorObserver.ts
Francesco Sansalvadore d24df14aab Feat/toc component (#33964)
* test fuma toc

* make toc work

* ok toc

* ui-patterns toc

* fix types

* cleanup

* fix overflow issues

* cleanup

* fix loading w

* Update apps/docs/components/GuidesTableOfContents.tsx

* fix prettier

* format

* install deps

* target ES2018

* tweak regex

* update docs test snapshots

* fix useSubscribeTocRerender
2025-03-05 23:10:02 +01:00

65 lines
1.9 KiB
TypeScript

import { useEffect, useState } from 'react'
/**
* Find the active heading of page
*
* It selects the top heading by default, and the last item when reached the bottom of page.
*
* @param watch - An array of element ids to watch
* @param single - only one active item at most
* @returns Active anchor
*/
export function useAnchorObserver(watch: string[], single: boolean): string[] {
const [activeAnchor, setActiveAnchor] = useState<string[]>([])
useEffect(() => {
let visible: string[] = []
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting && !visible.includes(entry.target.id)) {
visible = [...visible, entry.target.id]
} else if (!entry.isIntersecting && visible.includes(entry.target.id)) {
visible = visible.filter((v) => v !== entry.target.id)
}
}
if (visible.length > 0) setActiveAnchor(visible)
},
{
rootMargin: single ? '-80px 0% -70% 0%' : `-20px 0% -40% 0%`,
threshold: 1,
}
)
function onScroll(): void {
const element = document.scrollingElement
if (!element) return
if (element.scrollTop === 0 && single) setActiveAnchor(watch.slice(0, 1))
else if (element.scrollTop + element.clientHeight >= element.scrollHeight - 6) {
setActiveAnchor((active) => {
return active.length > 0 && !single
? watch.slice(watch.indexOf(active[0]))
: watch.slice(-1)
})
}
}
for (const heading of watch) {
const element = document.getElementById(heading)
if (element) observer.observe(element)
}
onScroll()
window.addEventListener('scroll', onScroll)
return () => {
window.removeEventListener('scroll', onScroll)
observer.disconnect()
}
}, [single, watch])
return single ? activeAnchor.slice(0, 1) : activeAnchor
}