diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection.test.tsx b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection.test.tsx index 2302d2eb8..99352ac34 100644 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection.test.tsx +++ b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection.test.tsx @@ -34,6 +34,17 @@ const baseDraft = () => timeZone: "UTC", }); +// A timed event that runs past local midnight: it starts on Aug 3 but its end +// lands on Aug 4. The "Ends on" floor is anchored to the start date, so the +// event's own date (Aug 3) stays selectable. +const pastMidnightDraft = () => + createGridEventDraft({ + kind: "timed", + start: new Date("2026-08-03T23:00:00.000Z"), + end: new Date("2026-08-04T00:30:00.000Z"), + timeZone: "UTC", + }); + const recurringDraft = () => { const source = createMockEvent({ schedule: SCHEDULE, @@ -127,6 +138,26 @@ describe("RecurrenceSection", () => { ]); }); + // Regression: the "Ends on" floor used to be the event's *end*, so an event + // running past local midnight (end on the next calendar day) disabled its own + // start date - the date the user most naturally picks. react-datepicker + // compares day cells by calendar day, so the fix is to anchor the floor to + // the start date. See RecurrenceSectionView's recurrenceMinDate. + it("keeps the event's own date selectable when the event ends after midnight", async () => { + const user = userEvent.setup(); + renderRecurrenceSection({ initialDraft: pastMidnightDraft() }); + + // Enable recurrence to reveal the "Ends on" picker, then open it via its + // input and assert the event's own (start) date is offered, not blocked. + await user.click(screen.getByRole("button", { name: /edit recurrence/i })); + await user.click(await screen.findByRole("textbox")); + + const ownDate = await screen.findByLabelText(/Monday, August 3rd, 2026/); + expect(ownDate.getAttribute("aria-label")).toMatch(/^Choose /); + expect(ownDate).not.toHaveClass("react-datepicker__day--disabled"); + expect(ownDate.getAttribute("aria-disabled")).not.toBe("true"); + }); + it("turning off Repeat on an existing recurring event clears the controls", async () => { // Guards against the toggle being a no-op on an edit draft: clearing // recurrence used to resolve to "preserve", which read the source diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSectionView.tsx b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSectionView.tsx index b5c814efb..6ded81e64 100644 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSectionView.tsx +++ b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSectionView.tsx @@ -25,10 +25,17 @@ export function RecurrenceSection({ const { setInterval, setFreq, setWeekDays, setUntil } = recurrenceHook; const { weekDays, interval, freq, until, toggleRecurrence } = recurrenceHook; const { hasRecurrence } = recurrenceHook; - const scheduleEndDate = - draft.values.schedule.kind === "allDay" - ? dayjs(draft.values.schedule.end).toYearMonthDayString() - : dayjs(draft.values.schedule.end).format(); + // Lower bound for the "Ends on" picker: the recurrence can't stop before the + // event's own occurrence, so the floor is the event's *start* date. Anchoring + // it to the *end* used to disable the event's own date whenever the event ran + // past local midnight (e.g. 23:00-00:30, or anything ending at 00:00 the next + // day): the end lands on the following calendar day, and react-datepicker + // compares day cells by calendar day, so the start date - the date the user + // sees and most naturally reaches for - rendered disabled. A bare + // YYYY-MM-DD keeps the bound date-only for both event kinds. + const recurrenceMinDate = dayjs( + draft.values.schedule.start, + ).toYearMonthDayString(); return (
@@ -52,7 +59,7 @@ export function RecurrenceSection({