scrolling finally works

This commit is contained in:
Michael Zhang 2023-08-23 21:11:12 -05:00
parent ba89cb6972
commit f276a6276e
4 changed files with 166 additions and 39 deletions

View file

@ -1,9 +1,10 @@
import {
addDays,
endOfMonth,
getWeek,
isSameWeek,
startOfMonth,
startOfWeek,
subDays,
} from "date-fns";
/**
@ -11,14 +12,19 @@ import {
* The last week is not returned if it contains the next month's start date
* @param date
*/
export function weekBoundsOfMonth(date: Date): [number, number] {
export function weekBoundsOfMonth(date: Date): [Date, Date] {
const firstDayOfMonth = startOfMonth(date);
const startWeek = getWeek(firstDayOfMonth);
const startWeek = startOfWeek(firstDayOfMonth);
const lastDayOfMonth = endOfMonth(date);
const firstDayOfNextMonth = addDays(lastDayOfMonth, 1);
let endWeek = getWeek(lastDayOfMonth);
if (isSameWeek(lastDayOfMonth, firstDayOfNextMonth)) endWeek -= 1;
let endWeek = startOfWeek(lastDayOfMonth);
if (isSameWeek(lastDayOfMonth, firstDayOfNextMonth))
endWeek = subDays(endWeek, 7);
return [startWeek, endWeek];
}
export function monthNameOf(date: Date): string {
return date.toLocaleDateString("default", { month: "long" });
}

View file

@ -25,6 +25,7 @@
text-align: right;
font-weight: 100;
border-bottom: 1px solid gray;
padding: 4px;
}
}

View file

@ -1,18 +1,53 @@
import { forwardRef, useCallback, useEffect, useRef, useState } from "react";
import styles from "./Calendar.module.scss";
import { addMonths, subMonths } from "date-fns";
import {
addDays,
addMonths,
differenceInWeeks,
startOfMonth,
subMonths,
} from "date-fns";
import Month from "./Month";
import { weekBoundsOfMonth } from "lib/month";
import { monthNameOf, weekBoundsOfMonth } from "lib/month";
const daysOfWeek = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const NUM_MONTHS_SHOWN = 5;
interface CalendarProps {
onDateClick?: (_: Date) => void;
}
export default function Calendar({ onDateClick }: CalendarProps) {
const now = new Date();
const [viewportHeight, setViewportHeight] = useState(0);
const [scrollyEl, setScrollyEl] = useState<HTMLDivElement>(null);
const [scrollyEl, setScrollyEl] = useState<HTMLDivElement | null>(null);
const [centerMonth, setCenterMonth] = useState(startOfMonth(now));
const [resetScrollToEl, setResetScrollToEl] = useState<
[HTMLElement, number] | null
>(null);
const range = [-2, -1, 0, 1, 2];
const [monthsShown, setMonthsShown] = useState(
range.map((x) => addMonths(centerMonth, x))
);
const [firstLoad, setFirstLoad] = useState(true);
useEffect(() => {
if (firstLoad && scrollyEl && viewportHeight > 0) {
// Identify the middle month
const el = scrollyEl.querySelector(
`[data-isodate="${centerMonth.toISOString()}"]`
);
if (el) {
console.log("first run, scrolling into view", el);
el.scrollIntoView();
setFirstLoad(false);
}
}
}, [firstLoad, scrollyEl, viewportHeight]);
const scrollyRef = useCallback((node: HTMLDivElement) => {
if (node) {
setScrollyEl(node);
@ -20,11 +55,10 @@ export default function Calendar({ onDateClick }: CalendarProps) {
}
}, []);
// Add resize observer on the viewport
useEffect(() => {
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
setViewportHeight(scrollyEl.clientHeight);
}
if (entries && scrollyEl) setViewportHeight(scrollyEl.clientHeight);
});
if (scrollyEl) observer.observe(scrollyEl);
@ -34,40 +68,90 @@ export default function Calendar({ onDateClick }: CalendarProps) {
};
}, [scrollyEl]);
// Add intersection observer to tell when we scrolled too far up or down
useEffect(() => {
if (!scrollyEl) return;
const children = [...scrollyEl.children];
if (children.length !== NUM_MONTHS_SHOWN) throw new Error("wtf");
const firstChild = children[0];
const lastChild = children[NUM_MONTHS_SHOWN - 1];
const observer = new IntersectionObserver((entries) => {
let newMonthsShown = monthsShown;
let scrollUp = false;
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);
newMonthsShown = [prevMonth, ...monthsShown];
newMonthsShown = 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);
});
console.log("observing", firstChild, lastChild);
observer.observe(firstChild);
observer.observe(lastChild);
return () => {
observer.unobserve(firstChild);
observer.unobserve(lastChild);
};
}, [monthsShown, scrollyEl]);
useEffect(() => {
if (!resetScrollToEl) return;
if (!scrollyEl) return;
console.log("current scroll top", scrollyEl.scrollTop);
console.log("params", resetScrollToEl);
const [el, height] = resetScrollToEl;
scrollyEl.scrollTop = el.offsetTop + height;
console.log("new scroll top", scrollyEl.scrollTop, el.offsetTop + height);
setResetScrollToEl(null);
}, [resetScrollToEl, scrollyEl]);
console.log("SCROLLY REF IS", scrollyRef);
const [middleMonthEl, setMiddleMonthEl] = useState<HTMLDivElement>(null);
const middleMonthRef = useCallback((node: HTMLDivElement) => {
if (node) setMiddleMonthEl(node);
}, []);
// The calendar should always show 6 rows
const dateCellHeight = viewportHeight / 6;
const now = new Date();
const [currentlyShownMonth, setCurrentlyShownMonth] = useState(now);
const monthName = currentlyShownMonth.toLocaleDateString("default", {
month: "long",
});
const monthName = monthNameOf(centerMonth);
const prevMonth = subMonths(currentlyShownMonth, 1);
const nextMonth = addMonths(currentlyShownMonth, 1);
const [monthsShown, setMonthsShown] = useState([
prevMonth,
currentlyShownMonth,
nextMonth,
]);
useEffect(() => {
middleMonthEl?.scrollIntoView();
}, [middleMonthRef]);
// useEffect(() => {
// middleMonthEl?.scrollIntoView();
// }, [middleMonthRef]);
return (
<div className={styles.calendar}>
<div className={styles.header}>
<div className={styles.title}>
<b>{monthName}</b>
{currentlyShownMonth.getFullYear()}
{centerMonth.getFullYear()}
(months shown:{" "}
{JSON.stringify(
monthsShown.map((x) =>
x.toLocaleDateString("default", { month: "long" })
)
)}
)
</div>
<div className={styles.daysOfWeek}>
@ -83,15 +167,15 @@ export default function Calendar({ onDateClick }: CalendarProps) {
{monthsShown.map((month) => {
// How many rows will this month take up?
const [startWeek, endWeek] = weekBoundsOfMonth(month);
const numWeeks = endWeek - startWeek + 1;
const numWeeks = differenceInWeeks(endWeek, startWeek);
const dateGridHeight = dateCellHeight * numWeeks;
const props = {
month,
dateGridHeight,
};
const ref =
(month === currentlyShownMonth && middleMonthRef) || undefined;
const ref = undefined;
// (month === currentlyShownMonth && middleMonthRef) || undefined;
return (
<Month
key={month.toString()}

View file

@ -2,6 +2,7 @@ import { forwardRef } from "react";
import styles from "./Month.module.scss";
import {
addDays,
differenceInWeeks,
endOfMonth,
getWeek,
isSameDay,
@ -17,12 +18,35 @@ interface MonthProps {
onDateClick?: (_: Date) => void;
}
const monthColors = [
"#AA3939",
"#FFAAAA",
"#D46A6A",
"#801515",
"#550000",
"#AA6C39",
"#FFD1AA",
"#D49A6A",
"#804515",
"#552700",
"#226666",
"#669999",
"#407F7F",
"#0D4D4D",
"#003333",
"#2D882D",
"#88CC88",
"#55AA55",
"#116611",
"#004400",
];
function Month({ month, dateGridHeight, onDateClick }: MonthProps, ref) {
const now = new Date();
const monthName = month.toLocaleString("default", { month: "long" });
const [startWeek, endWeek] = weekBoundsOfMonth(month);
const numWeeks = endWeek - startWeek + 1;
const numWeeks = differenceInWeeks(endWeek, startWeek) + 1;
const weeksArr = Array(numWeeks)
.fill(0)
.map((_, i) => addDays(startOfMonth(month), 7 * i));
@ -39,17 +63,29 @@ function Month({ month, dateGridHeight, onDateClick }: MonthProps, ref) {
className={styles.dateGrid}
style={{ height: `${dateGridHeight}px` }}
ref={ref}
data-isodate={month}
>
{datesArr.map((date) => {
const isFirst = isSameDay(date, startOfMonth(date));
const isToday = isSameDay(date, now);
return (
<div key={date.toString()} className={styles.dateCell}>
<div
key={date.toString()}
className={styles.dateCell}
data-isodate={date.toISOString()}
>
<div
className={styles.dateNumber}
onClick={() => onDateClick?.(date)}
>
<span className={classNames(isToday && styles.today)}>
<span
className={classNames(isFirst && styles.today)}
style={
isFirst
? { backgroundColor: monthColors[date.getMonth()] }
: {}
}
>
{isFirst && monthName}
{date.getDate()}
</span>