mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: calendar click/double-click created events near current time instead of the clicked slot
This commit is contained in:
@@ -1664,11 +1664,16 @@ export function CalendarApp({ linkSegments }: CalendarAppProps = {}) {
|
||||
const d = new Date(date);
|
||||
if (typeof hour === "number") {
|
||||
d.setHours(hour, 0, 0, 0);
|
||||
// Explicit end so the modal keeps the clicked hour instead of
|
||||
// applying its next-hour fallback for date-only defaults (#435).
|
||||
const end = new Date(date);
|
||||
end.setHours(hour + 1, 0, 0, 0);
|
||||
openCreateModal(d, end);
|
||||
} else {
|
||||
const now = new Date();
|
||||
d.setHours(now.getHours() + 1, 0, 0, 0);
|
||||
openCreateModal(d);
|
||||
}
|
||||
openCreateModal(d);
|
||||
}}
|
||||
onNewAllDayEvent={() => {
|
||||
const d = new Date(date);
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useTimeGridInteractions } from "@/hooks/use-time-grid-interactions";
|
||||
|
||||
const errorMessages = { resize: "resize", move: "move", created: "created", error: "error" };
|
||||
|
||||
function renderInteractions(onCreateRange = vi.fn()) {
|
||||
const { result } = renderHook(() =>
|
||||
useTimeGridInteractions({ hourHeight: 60, calendars: [], onCreateRange, errorMessages }),
|
||||
);
|
||||
return { result, onCreateRange };
|
||||
}
|
||||
|
||||
describe("useTimeGridInteractions slot clicks (#435)", () => {
|
||||
beforeEach(() => {
|
||||
// Freeze "now" far from the clicked slot so a regression to the modal's
|
||||
// next-hour-from-now fallback would produce a different time than asserted.
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(2026, 7, 21, 12, 2, 0));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("single click creates a range at the clicked hour, not near the current time", () => {
|
||||
const { result, onCreateRange } = renderInteractions();
|
||||
const day = new Date(2026, 8, 3);
|
||||
|
||||
act(() => result.current.handleSlotClick(day, 16));
|
||||
act(() => vi.advanceTimersByTime(300));
|
||||
|
||||
expect(onCreateRange).toHaveBeenCalledTimes(1);
|
||||
const [start, end] = onCreateRange.mock.calls[0];
|
||||
expect(start).toEqual(new Date(2026, 8, 3, 16, 0, 0, 0));
|
||||
expect(end).toEqual(new Date(2026, 8, 3, 17, 0, 0, 0));
|
||||
});
|
||||
|
||||
it("double click creates a range at the clicked hour immediately", () => {
|
||||
const { result, onCreateRange } = renderInteractions();
|
||||
const day = new Date(2026, 8, 3);
|
||||
|
||||
act(() => result.current.handleSlotClick(day, 9));
|
||||
act(() => result.current.handleSlotDoubleClick(day, 9));
|
||||
|
||||
expect(onCreateRange).toHaveBeenCalledTimes(1);
|
||||
const [start, end] = onCreateRange.mock.calls[0];
|
||||
expect(start).toEqual(new Date(2026, 8, 3, 9, 0, 0, 0));
|
||||
expect(end).toEqual(new Date(2026, 8, 3, 10, 0, 0, 0));
|
||||
|
||||
// The pending single-click timer must have been cancelled by the double click.
|
||||
act(() => vi.advanceTimersByTime(500));
|
||||
expect(onCreateRange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("a 23:00 click ends at midnight of the next day", () => {
|
||||
const { result, onCreateRange } = renderInteractions();
|
||||
const day = new Date(2026, 8, 3);
|
||||
|
||||
act(() => result.current.handleSlotDoubleClick(day, 23));
|
||||
|
||||
const [start, end] = onCreateRange.mock.calls[0];
|
||||
expect(start).toEqual(new Date(2026, 8, 3, 23, 0, 0, 0));
|
||||
expect(end).toEqual(new Date(2026, 8, 4, 0, 0, 0, 0));
|
||||
});
|
||||
});
|
||||
@@ -256,6 +256,18 @@ export function useTimeGridInteractions({
|
||||
const clickTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [quickCreate, setQuickCreate] = useState<QuickCreateState | null>(null);
|
||||
|
||||
// Pass an explicit end alongside the clicked slot: the event modal only
|
||||
// honors a timed `defaultDate` when `defaultEndDate` is present — without it
|
||||
// the modal falls back to "next hour from now" (meant for date-only month
|
||||
// clicks) and discards the clicked time (#435).
|
||||
const slotToRange = useCallback((day: Date, hour: number): [Date, Date] => {
|
||||
const start = new Date(day);
|
||||
start.setHours(hour, 0, 0, 0);
|
||||
const end = new Date(day);
|
||||
end.setHours(hour + 1, 0, 0, 0);
|
||||
return [start, end];
|
||||
}, []);
|
||||
|
||||
const handleSlotClick = useCallback((day: Date, hour: number) => {
|
||||
if (wasDragging.current) return;
|
||||
if (clickTimerRef.current) {
|
||||
@@ -265,11 +277,9 @@ export function useTimeGridInteractions({
|
||||
}
|
||||
clickTimerRef.current = setTimeout(() => {
|
||||
clickTimerRef.current = null;
|
||||
const d = new Date(day);
|
||||
d.setHours(hour, 0, 0, 0);
|
||||
onCreateRange(d);
|
||||
onCreateRange(...slotToRange(day, hour));
|
||||
}, 250);
|
||||
}, [onCreateRange]);
|
||||
}, [onCreateRange, slotToRange]);
|
||||
|
||||
const handleSlotDoubleClick = useCallback((day: Date, hour: number) => {
|
||||
if (wasDragging.current) return;
|
||||
@@ -277,10 +287,8 @@ export function useTimeGridInteractions({
|
||||
clearTimeout(clickTimerRef.current);
|
||||
clickTimerRef.current = null;
|
||||
}
|
||||
const d = new Date(day);
|
||||
d.setHours(hour, 0, 0, 0);
|
||||
onCreateRange(d);
|
||||
}, [onCreateRange]);
|
||||
onCreateRange(...slotToRange(day, hour));
|
||||
}, [onCreateRange, slotToRange]);
|
||||
|
||||
const handleQuickCreateSubmit = useCallback(async (title: string) => {
|
||||
if (!quickCreate) return;
|
||||
|
||||
Reference in New Issue
Block a user