Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="flex w-full basis-full flex-col items-center gap-2 p-0">
Expand All @@ -52,7 +59,7 @@ export function RecurrenceSection({

<EndsOnDate
until={until}
minDate={scheduleEndDate}
minDate={recurrenceMinDate}
setUntil={setUntil}
/>
</>
Expand Down