import { Fragment, Key, useCallback, useContext, useEffect, useState, } from "react"; import styles from "./ScrollContainer.module.scss"; import classNames from "classnames"; import { getWeek } from "date-fns"; import { CalendarContext } from "src/calendar/Calendar"; import { monthNameOf, weekString } from "./date"; interface ScrollContainerProps { direction: "horizontal" | "vertical"; className?: string; /** A list of things managed */ list: T[]; renderItem: (_: T) => JSX.Element; keyOf: (_: T) => Key; generatePreviousObject: (_: HTMLElement) => void; generateNextObject: (_: HTMLElement) => void; } interface OldScrollState { el: HTMLElement; offsetLeft: number; offsetTop: number; } /** * Generic infinite scrolling container */ function ScrollContainer({ direction, className, list, renderItem, generateNextObject, generatePreviousObject, keyOf, }: ScrollContainerProps) { const [resetScrollToEl, setResetScrollToEl] = useState( null ); // Viewport state const { viewportWidth, viewportHeight, setViewportSize } = useContext(CalendarContext); const viewportMetric = direction === "horizontal" ? viewportWidth : viewportHeight; // Scroll element state const [scrollyEl, setScrollyEl] = useState(null); const scrollyRef = useCallback((node: HTMLDivElement) => { if (node) { setScrollyEl(node); setViewportSize([node.clientWidth, node.clientHeight]); } }, []); // Scroll to middle on first load const [firstLoad, setFirstLoad] = useState(true); // Add resize observer on the viewport useEffect(() => { const observer = new ResizeObserver((entries) => { if (entries && scrollyEl) { setViewportSize([scrollyEl.clientWidth, scrollyEl.clientHeight]); } }); if (scrollyEl) observer.observe(scrollyEl); return () => { if (scrollyEl) observer.unobserve(scrollyEl); }; }, [scrollyEl]); // Add intersection observer to tell when we scrolled too far useEffect(() => { if (viewportWidth === 0 || viewportHeight === 0) return; if (!scrollyEl) return; if (firstLoad) { const scrollLen = scrollyEl.children.length; const middleIdx = (scrollLen + 1) / 2 - 1; console.log("middle idx", middleIdx); // Identify the middle month const el = scrollyEl.children[middleIdx]; if (el) { // const date = new Date(el.dataset.isodate!); // console.log("first run, scrolling into view", date.getMonth()); el.scrollIntoView(); setFirstLoad(false); } return; } const children = [...scrollyEl.children] as HTMLElement[]; const firstChild = children[0]; const lastChild = children[children.length - 1]; const observer = new IntersectionObserver((entries) => { if (viewportMetric === 0) return; for (const entry of entries) { if (!entry.isIntersecting) continue; // const intersectionArea = // entry.intersectionRect.width * entry.intersectionRect.height; // if (intersectionArea === 0) continue; // const date = new Date(entry.target.dataset.isodate!); // console.log("intersected with", weekString(date), entry); if (entry.target === firstChild) { const newResetScrollToEl = { el: firstChild, offsetLeft: scrollyEl.scrollLeft - firstChild.offsetLeft, offsetTop: scrollyEl.scrollTop - firstChild.offsetTop, }; setResetScrollToEl(newResetScrollToEl); generatePreviousObject(firstChild); } if (entry.target === lastChild) { console.log("child offset left", lastChild.offsetLeft); const newResetScrollToEl = { el: lastChild, offsetLeft: scrollyEl.scrollLeft - lastChild.offsetLeft, offsetTop: scrollyEl.scrollTop - lastChild.offsetTop, }; setResetScrollToEl(newResetScrollToEl); generateNextObject(lastChild); } } }); // const firstDate = new Date(firstChild.dataset.isodate!); // const lastDate = new Date(lastChild.dataset.isodate!); // console.log("observing", weekString(firstDate), weekString(lastDate)); observer.observe(firstChild); observer.observe(lastChild); return () => { observer.unobserve(firstChild); observer.unobserve(lastChild); }; }, [firstLoad, list, scrollyEl, viewportHeight]); useEffect(() => { if (!resetScrollToEl) return; if (!scrollyEl) return; const { el, offsetLeft, offsetTop } = resetScrollToEl; if (direction === "horizontal") scrollyEl.scrollLeft = el.offsetLeft + offsetLeft; else if (direction === "vertical") scrollyEl.scrollTop = el.offsetTop + offsetTop; setResetScrollToEl(null); }, [resetScrollToEl, scrollyEl]); return ( <> {JSON.stringify([firstLoad, viewportWidth, viewportHeight])}
{list.map((x) => ( {renderItem(x)} ))}
); } export default ScrollContainer;