103 lines
2.9 KiB
TypeScript
103 lines
2.9 KiB
TypeScript
import {
|
|
ForwardedRef,
|
|
forwardRef,
|
|
useCallback,
|
|
useEffect,
|
|
useState,
|
|
} from "react";
|
|
import styles from "./ScrollContainer.module.scss";
|
|
import { subMonths } from "date-fns";
|
|
|
|
interface ScrollContainerProps<T> {
|
|
direction: "horizontal" | "vertical";
|
|
|
|
/** A list of things managed */
|
|
list: T[];
|
|
|
|
generatePreviousObject: () => void;
|
|
generateNextObject: () => void;
|
|
}
|
|
/**
|
|
* Generic infinite scrolling container
|
|
*/
|
|
function ScrollContainer<T>(
|
|
{ direction, list }: ScrollContainerProps<T>,
|
|
ref: ForwardedRef<HTMLDivElement>
|
|
) {
|
|
const [resetScrollToEl, setResetScrollToEl] = useState<
|
|
[HTMLElement, number] | null
|
|
>(null);
|
|
|
|
// Viewport state
|
|
const [viewportSize, setViewportSize] = useState<[number, number]>([0, 0]);
|
|
const [viewportWidth, viewportHeight] = viewportSize;
|
|
const viewportMetric =
|
|
direction === "horizontal" ? viewportWidth : viewportHeight;
|
|
|
|
// Scroll element state
|
|
const [scrollyEl, setScrollyEl] = useState<HTMLDivElement | null>(null);
|
|
const scrollyRef = useCallback((node: HTMLDivElement) => {
|
|
if (node) {
|
|
setScrollyEl(node);
|
|
setViewportSize([node.clientWidth, node.clientHeight]);
|
|
}
|
|
}, []);
|
|
|
|
// 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 (!scrollyEl) return;
|
|
|
|
const children: HTMLElement[] = [...scrollyEl.children];
|
|
const firstChild = children[0];
|
|
const lastChild = children[children.length - 1];
|
|
|
|
const observer = new IntersectionObserver((entries) => {
|
|
if (viewportMetric === 0) return;
|
|
let newList = [...list];
|
|
|
|
for (const entry of entries) {
|
|
if (!entry.isIntersecting) continue;
|
|
|
|
if (entry.target === firstChild) {
|
|
console.log("intersected");
|
|
const firstChildDate = new Date(firstChild.dataset.isodate!);
|
|
const prevMonth = subMonths(firstChildDate, 1);
|
|
newList = [prevMonth, ...monthsShown];
|
|
newList = newMonthsShown.slice(0, NUM_MONTHS_SHOWN);
|
|
|
|
setResetScrollToEl([
|
|
firstChild,
|
|
scrollyEl.scrollTop - firstChild.offsetTop,
|
|
]);
|
|
} else if (entry.target === lastChild) {
|
|
const lastChildDate = new Date(lastChild.dataset.isodate!);
|
|
const nextMonth = addMonths(lastChildDate, 1);
|
|
newMonthsShown = [...monthsShown, nextMonth];
|
|
newMonthsShown = newMonthsShown.slice(-NUM_MONTHS_SHOWN);
|
|
}
|
|
}
|
|
|
|
setMonthsShown(newMonthsShown);
|
|
});
|
|
});
|
|
|
|
return <div className={styles.scrollyEl} ref={scrollyRef}></div>;
|
|
}
|
|
|
|
export default forwardRef(ScrollContainer);
|