plane/apps/docs/src/components/SectionProvider.jsx

116 lines
3.2 KiB
React
Raw Normal View History

2022-12-21 19:03:32 +00:00
import {
createContext,
useContext,
useEffect,
useLayoutEffect,
useState,
} from 'react'
import { createStore, useStore } from 'zustand'
import { remToPx } from '@/lib/remToPx'
function createSectionStore(sections) {
return createStore((set) => ({
sections,
visibleSections: [],
setVisibleSections: (visibleSections) =>
set((state) =>
state.visibleSections.join() === visibleSections.join()
? {}
: { visibleSections }
),
registerHeading: ({ id, ref, offsetRem }) =>
2023-01-29 06:56:36 +00:00
set((state) => ({
sections: state.sections.map((section) => {
if (section.id === id) {
return {
...section,
headingRef: ref,
offsetRem,
2022-12-21 19:03:32 +00:00
}
2023-01-29 06:56:36 +00:00
}
return section
}),
})),
2022-12-21 19:03:32 +00:00
}))
}
function useVisibleSections(sectionStore) {
2023-01-29 06:56:36 +00:00
const setVisibleSections = useStore(sectionStore, (s) => s.setVisibleSections)
const sections = useStore(sectionStore, (s) => s.sections)
2022-12-21 19:03:32 +00:00
useEffect(() => {
function checkVisibleSections() {
2023-01-29 06:56:36 +00:00
const { innerHeight, scrollY } = window
const newVisibleSections = []
2022-12-21 19:03:32 +00:00
for (
let sectionIndex = 0;
sectionIndex < sections.length;
sectionIndex++
) {
2023-01-29 06:56:36 +00:00
const { id, headingRef, offsetRem } = sections[sectionIndex]
const offset = remToPx(offsetRem)
const top = headingRef.current.getBoundingClientRect().top + scrollY
2022-12-21 19:03:32 +00:00
if (sectionIndex === 0 && top - offset > scrollY) {
newVisibleSections.push('_top')
}
2023-01-29 06:56:36 +00:00
const nextSection = sections[sectionIndex + 1]
const bottom =
2022-12-21 19:03:32 +00:00
(nextSection?.headingRef.current.getBoundingClientRect().top ??
Infinity) +
scrollY -
remToPx(nextSection?.offsetRem ?? 0)
if (
(top > scrollY && top < scrollY + innerHeight) ||
(bottom > scrollY && bottom < scrollY + innerHeight) ||
(top <= scrollY && bottom >= scrollY + innerHeight)
) {
newVisibleSections.push(id)
}
}
setVisibleSections(newVisibleSections)
}
2023-01-29 06:56:36 +00:00
const raf = window.requestAnimationFrame(() => checkVisibleSections())
2022-12-21 19:03:32 +00:00
window.addEventListener('scroll', checkVisibleSections, { passive: true })
window.addEventListener('resize', checkVisibleSections)
return () => {
window.cancelAnimationFrame(raf)
window.removeEventListener('scroll', checkVisibleSections)
window.removeEventListener('resize', checkVisibleSections)
}
}, [setVisibleSections, sections])
}
const SectionStoreContext = createContext()
const useIsomorphicLayoutEffect =
typeof window === 'undefined' ? useEffect : useLayoutEffect
export function SectionProvider({ sections, children }) {
2023-01-29 06:56:36 +00:00
const [sectionStore] = useState(() => createSectionStore(sections))
2022-12-21 19:03:32 +00:00
useVisibleSections(sectionStore)
useIsomorphicLayoutEffect(() => {
sectionStore.setState({ sections })
}, [sectionStore, sections])
return (
<SectionStoreContext.Provider value={sectionStore}>
{children}
</SectionStoreContext.Provider>
)
}
export function useSectionStore(selector) {
2023-01-29 06:56:36 +00:00
const store = useContext(SectionStoreContext)
2022-12-21 19:03:32 +00:00
return useStore(store, selector)
}