Keep calendar timeline stable when inspecting dates

This commit is contained in:
syntaxbullet
2026-09-05 08:10:51 +02:00
parent 1f017caee5
commit 2f67b775bf
2 changed files with 62 additions and 6 deletions

View File

@@ -44,11 +44,18 @@ export function CalendarHeatmap({
const latestDate =
allDays.findLast((day) => day.state !== "future")?.date ??
allDays.at(-1)?.date;
// A date selected in the editor must stay visible even outside the current window.
const anchor =
controlledDate && allDays.some((day) => day.date === controlledDate)
? controlledDate
: latestDate;
// Inspecting a square changes selection, not the window. Only an external
// date outside the visible preset needs a new anchor to bring it into view.
const [windowAnchor, setWindowAnchor] = useState({ latestDate, date: latestDate });
let anchor = windowAnchor.latestDate === latestDate ? windowAnchor.date : latestDate;
if (months !== "custom" && anchor && controlledDate &&
allDays.some(day => day.date === controlledDate) &&
!visibleCalendarDays(allDays, months, anchor).some(day => day.date === controlledDate)) {
anchor = controlledDate;
}
if (windowAnchor.latestDate !== latestDate || windowAnchor.date !== anchor) {
setWindowAnchor({ latestDate, date: anchor });
}
const [localSelectedDate, setLocalSelectedDate] = useState(anchor);
const selectedDate = controlledDate ?? localSelectedDate;
function setSelectedDate(date: string) {

View File

@@ -66,7 +66,7 @@ test("calendar associates exact keyboard guidance and preserves focus when selec
expect(selected.tabIndex).toBe(0);
});
test("calendar focuses the accepted date after a controlled month window changes", async () => {
test("calendar focuses the accepted date when selection crosses a month boundary", async () => {
function Calendar() {
const [date, setDate] = useState("2026-09-01");
return <CalendarHeatmap days={demoCalendar(3, 8)} unit="glasses" label="Water history" selectedDate={date} onSelectDate={setDate} />;
@@ -105,6 +105,55 @@ test("rejected calendar clicks and repeated arrows do not leave stale focus requ
expect(rejected.getAttribute("aria-pressed")).toBe("true");
});
test("selecting tracked, empty, unscheduled and future dates never changes the displayed timeline", async () => {
const days = demoCalendar(3, 8).map((day, index) => ({ ...day,
state: day.state === "future" ? day.state : index % 2 === 0 ? "not-due" as const : "due" as const,
value: index % 3 === 0 ? 0 : day.value,
}));
function Calendar() {
const [date, setDate] = useState<string>();
return <CalendarHeatmap days={days} unit="pages" label="Reading history" selectedDate={date} onSelectDate={setDate} />;
}
await act(async () => root.render(<Calendar />));
const timeline = () => ({
dates: [...container.querySelectorAll(".ds-day")].map(day => day.getAttribute("aria-label")?.slice(0, 10)),
months: [...container.querySelectorAll(".ds-calendar-month-label")].map(month => month.textContent),
range: container.querySelector(".ds-calendar-range-label")?.textContent,
weeks: (container.querySelector(".ds-calendar") as HTMLElement).style.getPropertyValue("--calendar-weeks"),
});
for (const preset of [12, 6, 4, 3, "Custom"]) {
await act(async () => button(typeof preset === "number" ? `${preset}m` : preset).click());
const before = timeline();
for (const index of [1, 0, 8, 7, before.dates.length - 1]) {
const date = before.dates[index]!;
await act(async () => container.querySelector<HTMLButtonElement>(`.ds-day[aria-label^="${date}:"]`)!.click());
expect(timeline()).toEqual(before);
expect(container.querySelector('.ds-day[aria-pressed="true"]')?.getAttribute("aria-label")).toStartWith(`${date}:`);
}
for (const key of ["ArrowLeft", "Home", "End"]) {
const selected = container.querySelector<HTMLButtonElement>('.ds-day[aria-pressed="true"]')!;
await act(async () => selected.dispatchEvent(new dom.KeyboardEvent("keydown", { key, bubbles: true }) as unknown as KeyboardEvent));
expect(timeline()).toEqual(before);
}
}
});
test("an external editor date outside the window remains reachable without moving focus", async () => {
let setDate!: (date: string) => void;
function Calendar() {
const [date, update] = useState("2026-09-04");
setDate = update;
return <><input aria-label="Editor date" /><CalendarHeatmap days={demoCalendar(3, 8)} unit="pages" label="Reading history" selectedDate={date} onSelectDate={update} /></>;
}
await act(async () => root.render(<Calendar />));
await act(async () => button("3m").click());
const editor = container.querySelector<HTMLInputElement>('input[aria-label="Editor date"]')!;
editor.focus();
await act(async () => setDate("2026-03-04"));
expect(container.querySelector('.ds-day[aria-pressed="true"]')?.getAttribute("aria-label")).toStartWith("2026-03-04:");
expect(document.activeElement).toBe(editor);
});
test("editor controls expose their explanatory hints", async () => {
await act(async () => root.render(<EditingWorkbench />));
expect(description(container.querySelector("input[readonly]")!)).toContain("Method is fixed");