Keep calendar timeline stable when inspecting dates
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user