Drag calendar events to another day in the week grid (#400)

A timed event in the week view now moves sideways across the columns
as well as up and down, landing on the new day at the hour it was
dragged to. All-day chips above the grid drag between days the way
month chips do. Both drags count from the day the event was picked up
on, so a multi-day event grabbed on its last day moves by the distance
dragged, not by its length.
This commit is contained in:
jcoffey
2026-09-18 21:19:58 -07:00
committed by GitHub
parent c118184975
commit 091782ae3a
3 changed files with 130 additions and 43 deletions
@@ -10,6 +10,8 @@ import {
snap,
movePatch,
moveByDaysPatch,
moveAcrossPatch,
columnsMoved,
dayDelta,
resizePatch,
SNAP_MINUTES,
@@ -180,10 +182,22 @@ describe("the patch a drag sends, computed in the event's own frame", () => {
expect(resizePatch(3600, -600)).toEqual({ duration: "PT15M" });
});
it("moves by days and minutes together, as a week-grid drag does", () => {
expect(moveAcrossPatch("2026-09-04T14:00:00", 2, 90)).toEqual({ start: "2026-09-06T15:30:00" });
expect(moveAcrossPatch("2026-09-04T14:00:00", -1, 0)).toEqual({ start: "2026-09-03T14:00:00" });
expect(moveAcrossPatch("2026-09-04T14:00:00", 0, -30)).toEqual({ start: "2026-09-04T13:30:00" });
});
it("adds the days as days, so a clock change does not move the hour", () => {
// US clocks go back on 1 November 2026; 14:00 stays 14:00 across it.
expect(moveAcrossPatch("2026-10-31T14:00:00", 2, 0)).toEqual({ start: "2026-11-02T14:00:00" });
});
it("says nothing at all about a start it cannot read", () => {
expect(movePatch("not a date", 30)).toEqual({});
expect(moveByDaysPatch("", 3)).toEqual({});
expect(moveByDaysPatch("2026-09-04T14:00:00", Number.NaN)).toEqual({});
expect(moveAcrossPatch("not a date", 1, 30)).toEqual({});
});
});
@@ -228,3 +242,23 @@ describe("pixelsToMinutes", () => {
expect(SNAP_MINUTES).toBe(15);
});
});
describe("columnsMoved", () => {
it("counts whole columns, to the nearest", () => {
expect(columnsMoved(100, 100, 2, 7)).toBe(1);
expect(columnsMoved(140, 100, 2, 7)).toBe(1);
expect(columnsMoved(160, 100, 2, 7)).toBe(2);
expect(columnsMoved(-40, 100, 2, 7)).toBe(0);
expect(columnsMoved(-160, 100, 3, 7)).toBe(-2);
});
it("stops at the edges of the week instead of wrapping", () => {
expect(columnsMoved(-900, 100, 2, 7)).toBe(-2);
expect(columnsMoved(900, 100, 2, 7)).toBe(4);
});
it("never moves sideways in a one-day grid or before it is measured", () => {
expect(columnsMoved(500, 100, 0, 1)).toBe(0);
expect(columnsMoved(500, 0, 0, 7)).toBe(0);
});
});
+29
View File
@@ -142,6 +142,35 @@ export function moveByDaysPatch(storedStart: string, days: number): DragPatch {
return { start: formatStored(moved) };
}
/**
* Moved by whole days and by minutes at once -- the week grid, where a drag
* goes sideways to another day and up or down to another hour in the same
* gesture.
*
* The days go first and as days, for the reason moveByDaysPatch gives: a day
* added to a wall clock keeps its time of day across a clock change, where
* 1440 minutes would not.
*/
export function moveAcrossPatch(storedStart: string, days: number, deltaMinutes: number): DragPatch {
const byDays = days ? moveByDaysPatch(storedStart, days).start : storedStart;
if (!byDays) return {};
return snap(deltaMinutes) ? movePatch(byDays, deltaMinutes) : { start: byDays };
}
/**
* How many columns sideways the pointer has gone, kept inside the grid.
*
* Counted from the column the drag began in, so an event that crosses
* midnight moves by the same amount whichever of its two halves was picked
* up. Past the first or last column it stops at the edge rather than
* wrapping: the week on screen is the only week a drag can reach.
*/
export function columnsMoved(deltaPixels: number, columnWidth: number, fromIndex: number, columnCount: number): number {
if (!columnWidth || columnCount < 2) return 0;
const moved = Math.round(deltaPixels / columnWidth) || 0; // never -0
return Math.max(-fromIndex, Math.min(columnCount - 1 - fromIndex, moved));
}
/** Whole days between two local dates, ignoring the time of day on each. */
export function dayDelta(from: Date, to: Date): number {
const a = new Date(from.getFullYear(), from.getMonth(), from.getDate()).getTime();