Skip to content

[scheduler] preserve multi-day event continuity across overflow#22797

Merged
rita-codes merged 28 commits into
mui:masterfrom
mustafajw07:fix/22735-scheduler-month-view-continuation-overflow
Jul 9, 2026
Merged

[scheduler] preserve multi-day event continuity across overflow#22797
rita-codes merged 28 commits into
mui:masterfrom
mustafajw07:fix/22735-scheduler-month-view-continuation-overflow

Conversation

@mustafajw07

@mustafajw07 mustafajw07 commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Closes #22735

Changelog

@code-infra-dashboard

code-infra-dashboard Bot commented Jun 12, 2026

Copy link
Copy Markdown

Deploy preview

Bundle size

Bundle Parsed size Gzip size
@mui/x-data-grid 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-pro 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-premium 0B(0.00%) 0B(0.00%)
@mui/x-charts 0B(0.00%) 0B(0.00%)
@mui/x-charts-pro 0B(0.00%) 0B(0.00%)
@mui/x-charts-premium 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers-pro 0B(0.00%) 0B(0.00%)
@mui/x-tree-view 0B(0.00%) 0B(0.00%)
@mui/x-tree-view-pro 0B(0.00%) 0B(0.00%)
@mui/x-scheduler 🔺+1.33KB(+0.37%) 🔺+397B(+0.41%)
@mui/x-scheduler-premium 🔺+1.32KB(+0.27%) 🔺+424B(+0.32%)
@mui/x-chat 0B(0.00%) 0B(0.00%)
@mui/x-license 0B(0.00%) 0B(0.00%)

Details of bundle changes


Check out the code infra dashboard for more information about this PR.

@zannager zannager added the scope: scheduler Changes related to the scheduler. label Jun 12, 2026
@mustafajw07

Copy link
Copy Markdown
Contributor Author

Hey @rita-codes , Can i get a review on this?

@rita-codes rita-codes added the type: bug It doesn't behave as expected. label Jun 16, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adjusts the Scheduler day-grid positioning logic to preserve multi-day event continuity when rows “compact” (e.g., when earlier events end and lower row indexes become available), which is a key piece of fixing Month view overflow behavior where a continuation bar could disappear mid-span.

Changes:

  • Reworked useEventOccurrencesWithDayGridPosition to allow multi-day events to “re-segment” into a lower available row by shortening the previous segment and starting a new segment.
  • Updated internal positioning tests to reflect the new segmented behavior (and added/adjusted assertions).
  • Updated WeekView all-day column-span expectation to reflect the new segmented span behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
packages/x-scheduler/src/week-view/tests/WeekView.test.tsx Updates all-day span expectation; needs comment alignment with new segmented behavior.
packages/x-scheduler-internals/src/use-event-occurrences-with-day-grid-position/useEventOccurrencesWithDayGridPosition.ts Core change: track active segments and split/compact multi-day bars when lower rows open up.
packages/x-scheduler-internals/src/use-event-occurrences-with-day-grid-position/useEventOccurrencesWithDayGridPosition.test.ts Test expectations updated; some descriptions/comments need to be brought in sync with new behavior.
packages/x-scheduler-internals/src/calendar-grid/use-placeholder-in-day/useCalendarGridPlaceholderInDay.ts Placeholder index selection updated to avoid always using row 1.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +175 to +176
// Should span 4 columns (4 days)
expect(gridColumnSpan).to.equal('4');
expect(gridColumnSpan).to.equal('3');
Comment on lines 60 to 64

expect(result.maxIndex).to.equal(2);
expect(result.days[1].withPosition[1].id).to.equal('B');
expect(result.days[1].withPosition[1].position).to.deep.equal({ index: 2, daySpan: 2 });
expect(result.days[1].withPosition[1].position).to.deep.equal({ index: 2, daySpan: 1 });
expect(result.days[2].withPosition[0].id).to.equal('B');
Comment on lines 78 to 82
// Event A is not present on day 3, so event C should use index 1 on that day instead of using index 3 below Event B
expect(result.maxIndex).to.equal(2);
expect(result.days[2].withPosition[0].id).to.equal('C');
expect(result.days[2].withPosition[0].id).to.equal('B');
expect(result.days[2].withPosition[0].position).to.deep.equal({ index: 1, daySpan: 1 });
});
@rita-codes

rita-codes commented Jun 17, 2026

Copy link
Copy Markdown
Member

Thanks for tackling this, @mustafajw07! I caught a regression in the Argos diff we should sort out before this goes in.

In the week view, Event 5 gets split into two disjoint pieces — a chip on Tue (bottom row) and a separate full-width bar starting Wed (top row) — even though there's no "+N more" overflow in any of those cells. On master the same event stays in a single row and renders as one continuous bar. So we're splitting events unconditionally whenever a lower row frees up, not only when there's actual overflow.

The compaction in useEventOccurrencesWithDayGridPosition.ts (activeSegments) reopens the bar at the smallest available index as soon as a row frees up, without checking whether the event was actually hidden. I think the rule should be:

  • No "+N more" in the next cell → keep the event in its original row, rendered continuously (same index), like master does today.
  • Split + continuation arrow only when the event was hidden inside the "+N more" section on its starting day(s) and resurfaces on a later day where it now fits.

This ties into the two changes we agreed on in yesterday's sync:

  1. Render multi-day events first (breaking strict start-date ordering) so long events get the top rows and land in the "+N more" overflow less often.
  2. When an event is hidden in "+N more" on its starting day(s), render it on later days with an arrow indicating it continues from a previous (hidden) day — including showing that arrow inside the "+N more" popover, which we don't do today.

Also please make sure drag & drop stays accurate with whatever new ordering/index logic you land on.

What do you think? If you have any doubts or would prefer we take it over, just let me know 🙌

Screenshot 2026-06-16 at 18 01 52 Screenshot 2026-06-16 at 17 56 34

@mustafajw07

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review and for catching the regression.

You're right — the current activeSegments compaction logic reassigns a lower index whenever one becomes available, without distinguishing between normal compaction and an event that was previously hidden by overflow. That explains the week-view regression where the bar gets split even though there is no "+N more" involved.

I'll rework the logic so that multi-day events keep their original row and render continuously unless they were actually hidden in overflow. I'll also revisit the ordering to prioritize multi-day events, add the continuation-arrow behavior for resurfacing events (including in the "+N more" popover), and verify drag & drop behavior against the updated positioning logic.

I'll push an update once I've worked through those changes. Thanks again for the clear explanation and screenshots!

@mustafajw07

Copy link
Copy Markdown
Contributor Author

Hi @rita-codes ,
I've pushed an update addressing the feedback around the multi-day event regression. The latest changes preserve row continuity for multi-day events and avoid splitting events when there is no actual overflow.

I've also revisited the positioning logic and updated the related tests accordingly.

Could you please take another look when you have a chance? Thanks again for the detailed review and guidance.

@mustafajw07

Copy link
Copy Markdown
Contributor Author

Hey @rita-codes , can you review this PR?

@rita-codes

rita-codes commented Jun 29, 2026

Copy link
Copy Markdown
Member

Hi @mustafajw07, thanks for taking this on! I pulled the branch and ran it locally. The direction is right, but as it stands the fix doesn’t take effect yet, and while testing I found a second positioning bug. Here’s everything, with repros.

1. The issue still reproduces on this branch

With this exact example (straight from #22735), a multi-day event pushed into overflow stays stuck on its overflow row instead of collapsing once it runs alone — identical to master:

import * as React from 'react';
import { SchedulerEvent } from '@mui/x-scheduler/models';
import { StandaloneMonthView } from '@mui/x-scheduler/month-view';

const initialEvents: SchedulerEvent[] = [
  { id: 'A', title: 'A', start: '2025-07-01T09:00:00', end: '2025-07-07T17:00:00' },
  { id: 'B', title: 'B', start: '2025-07-02T09:00:00', end: '2025-07-07T17:00:00' },
  { id: 'C', title: 'C', start: '2025-07-03T09:00:00', end: '2025-07-07T17:00:00' },
  { id: 'D', title: 'D', start: '2025-07-04T09:00:00', end: '2025-07-07T17:00:00' },
  { id: 'T', title: 'Long trip', start: '2025-07-07T08:00:00', end: '2025-07-20T17:00:00' },
];

export default function Repro() {
  const [events, setEvents] = React.useState(initialEvents);
  return (
    <div style={{ height: 460 }}>
      <StandaloneMonthView
        events={events}
        defaultVisibleDate={new Date('2025-07-01T00:00:00')}
        onEventsChange={setEvents}
      />
    </div>
  );
}
Screenshot 2026-06-29 at 18 42 23

The reason: the new maxEvents parameter never reaches the hook. Both production call sites omit it — MonthViewWeekRow.tsx (useEventOccurrencesWithDayGridPosition({ days, occurrencesMap })) and DayTimeGrid.tsx. With maxEvents undefined, every new branch collapses to the old behavior, so the algorithm change is effectively dead code (the unit test doesn’t pass maxEvents either, so nothing exercises it). Passing maxEvents through from MonthViewWeekRow is what makes the fix actually run.

2. Once maxEvents is wired, a second positioning bug appears

With rows kept full so the overflow event can’t collapse, it disappears on its continuation days (no bar, and it drops out of the +N more count) instead of staying in overflow on every cell it crosses:

const initialEvents = [
  { id: 'trip', title: 'Long trip', start: '2025-07-07T08:00:00', end: '2025-07-20T17:00:00' },
  { id: 'filler', title: 'Filler', start: '2025-07-08T08:00:00', end: '2025-07-20T17:00:00' },
  { id: 'X', title: 'X overflow', start: '2025-07-08T09:00:00', end: '2025-07-12T17:00:00' }, // timed multi-day
];
// rows 1-2 are full Jul 8-20, so X should show "+more" on Jul 8, 9, 10, 11, 12 — but it vanishes after Jul 8

Root cause: the new sort processes the resurfacing (largest-span) event before the still-visible events that retain their row, but smallestAvailableIndex is computed from a usedIndexes set that isn’t populated yet. So the overflow event thinks row 1/2 is free and resurfaces onto a row another event is about to keep — overlapping it, and removing itself from the overflow count. The direction that fixed it locally: before the assignment loop, reserve the rows that already-visible active events will retain, so resurfacing/new events only pick genuinely free rows.

3. isContinuation is set but never rendered

The new isContinuation flag is written in the hook and documented as “the renderer should show a left-pointing arrow,” but nothing reads it — so the continuation segment renders as a plain bar with no indication it started earlier. DayGridEvent already has the arrow-point clip-path for data-starting-before-edge; the continuation case needs the same treatment, because the hidden day is in the same row/week so the existing edge logic doesn’t trigger.

4. The +N more popover should match

When a continuing event appears in the overflow popover, it currently renders as a square card. For consistency it should use the same arrow-point clip on the side it continues (left if it started before this day, right if it ends after) — both can be derived from occurrence.displayTimezone.start/end vs the popover’s day in EventItem, reusing the same clip constants as the grid bars rather than duplicating them.

5. Tests

testHook never passes maxEvents, so none of the new logic is covered. Worth adding regression tests for: an overflow event collapsing once a row frees up (continuation), and an overflow event that can’t collapse staying counted in overflow across every day it crosses (the bug in §2).

After all the changes it should look like this:
Screenshot 2026-06-29 at 18 58 35

I know this is a lot, and I’ve got all of this working locally, including the +N more arrows. If you’d like, I can push these changes to your branch, or if you’d rather try it yourself, just say the word 🙌 . Thanks again for pushing this forward! 🙌

@mustafajw07

Copy link
Copy Markdown
Contributor Author

Thanks! I'd like to try implementing it myself first so I can better understand the changes. That said, if this is blocking any issue or is urgent, I'm totally okay with you pushing the changes to my branch. 🙌

…in occurrence positioning and add tests for continuation logic
@mustafajw07

Copy link
Copy Markdown
Contributor Author

Hey @rita-codes , I have update a few changes can you review those?

@rita-codes

rita-codes commented Jul 3, 2026

Copy link
Copy Markdown
Member

We are closer! 🙌

One thing still open: the continuation cue in the popover isn't rendering yet. It's keyed on position.isContinuation, but that's almost never true for what the popover shows — an event in overflow on its start day isn't a continuation yet, and once it resurfaces into a visible row it's no longer in the popover, so the cards stay square.

Playing with it, I think isContinuation can actually go away rather than be extended: nothing reads it for layout (the hook only writes it), and it only models the left side. The cue is fully derivable from the event's real range vs the day it renders in — point left if it starts earlier, right if it ends later. Same rule for the grid bar and the popover card, both sides. Reusing the clip-path constants already in DayGridEvent keeps it DRY, and the popover stops touching position altogether.

I tested this approach locally and it works nicely — both the grid continuity and the popover arrows come out right.

Two smaller things while you're in there:

  • The new span sort also runs for DayTimeGrid (week & day all-day rows), which don't pass maxEvents, so it reorders all-day events there too — worth gating behind maxEvents or confirming it's intended and covering it.
  • A render test for the arrow (grid + popover) would lock it in.

You're on the right track, this is going to be a great fix 🙌

@mustafajw07

Copy link
Copy Markdown
Contributor Author

Hey @rita-codes , I have update a few changes can you review those?

@rita-codes

Copy link
Copy Markdown
Member

Really nice catch spotting the "+N more" button getting covered — and good instinct adding a pass for it 🙌 Testing it though, the current version trades that bug for another: the spanning bar now vanishes for the rest of the week after the overflow day.

Repro — two all-day events fill rows 1–2 across the month; a single event on Jul 9 makes that cell overflow. The "+N more" is no longer covered (👍), but All-day B shows up to Jul 9 and then disappears (nothing on Jul 10–12), even though it keeps spanning them:

import * as React from 'react';
import { SchedulerEvent } from '@mui/x-scheduler/models';
import { StandaloneMonthView } from '@mui/x-scheduler/month-view';

const initialEvents: SchedulerEvent[] = [
  { id: 'A', title: 'All-day A', start: '2025-07-01T00:00:00', end: '2025-07-31T23:59:59', allDay: true },
  { id: 'B', title: 'All-day B', start: '2025-07-01T00:00:00', end: '2025-07-31T23:59:59', allDay: true },
  { id: 'C', title: 'New on Jul 9', start: '2025-07-09T09:00:00', end: '2025-07-09T10:00:00' },
];

export default function Repro() {
  const [events, setEvents] = React.useState(initialEvents);
  return (
    <div style={{ height: 460 }}>
      <StandaloneMonthView
        events={events}
        defaultVisibleDate={new Date('2025-07-01T00:00:00')}
        onEventsChange={setEvents}
      />
    </div>
  );
}

The root cause is that the hook and MonthViewCell disagree on what "visible" means: the hook treats row maxEvents as visible, but on an overflow day that row is taken by the "+N more" button (the cell only renders maxEvents - 1 events). So an event on that row is actually hidden that day — yet the hook keeps it as a continuous invisible bar and the post-pass only truncates it, so it never resumes.

The direction that worked for me: make the hook aware the button reserves a row on overflow days (effective capacity maxEvents - 1), and model the visible→hidden→visible transition so the bar splits — ends before the overflow day and resurfaces after it, reusing the same continuation mechanism (the ◄). That replaces the truncate-only pass. I prototyped it locally and it holds up (button stays visible and the bar resurfaces on Jul 10–12), with a hook test pinning it.

Happy to share the diff if it helps, or leave it to you 🙌

@mustafajw07

Copy link
Copy Markdown
Contributor Author

Yes, that would be really helpful. If you don't mind, could you share the diff?

@rita-codes

rita-codes commented Jul 6, 2026

Copy link
Copy Markdown
Member

Actually, I just pushed it straight to the branch as a commit instead — easier to read and test than a pasted diff, and fully revertible if you'd rather take your own route (just drop the commit).

Key idea: a per-day effectiveMax (maxEvents - 1 on overflow days, since the "+N more" button eats a row) plus a hidden flag on activeSegments, so a bar that gets bumped closes its segment and resurfaces on the next day it fits — instead of the truncate-only pass. Existing tests stay green, plus a new one pinning the split-and-resurface case.

Take it or leave it — whatever's easiest for you 🙌

Track a per-day effective capacity (maxEvents - 1 on overflow days, where the
"+N more" button takes a row) plus a `hidden` flag on the active segments, so a
multi-day bar bumped by the button ends before the overflow day and resurfaces
after it — instead of only being truncated, which made it vanish for the rest
of the week.
@mustafajw07

Copy link
Copy Markdown
Contributor Author

Thanks, I appreciate it! I'll take a look at the commit and go through the implementation to understand the approach. If everything looks good, I'll keep it.

@mustafajw07

Copy link
Copy Markdown
Contributor Author

@rita-codes I took a look at the commit and the changes look good. I'll keep them and go through the implementation to better understand the approach. Thanks for putting this together! 🙌

@mustafajw07

Copy link
Copy Markdown
Contributor Author

@rita-codes , any further changes required on this pr?

@rita-codes rita-codes left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice fix 🙌 the resurfacing logic and the overflow accounting in MonthViewCell hold up, and the new hook tests cover the tricky cases well. Two cleanups before merge:

  1. Dead code in useCalendarGridPlaceholderInDay.ts (lines 103–120). The creation and external-drag branches already route through findAvailableIndex(), and this branch's return now uses findAvailableIndex(rawPlaceholder.occurrenceKey) too — so the inline positionIndex computation here (the targetDay lookup, the while loop, and the maxEvents clamp) is that same logic duplicated, and its result is never read. It was live on master (index: positionIndex), but extracting findAvailableIndex and re-pointing the return at it orphaned this block. Could you drop lines 103–120 so the branch relies solely on findAvailableIndex? Leaving the duplicate risks the two copies drifting apart.

  2. Misleading sort comment in useEventOccurrencesWithDayGridPosition.ts (line 48). The comment says "multi-day events first (so they claim top rows), then by start date", but sortEventOccurrences sorts by start date ascending (then end date descending) — the span-based ordering only comes from the maxEvents != null secondary sort right below. As written, the first line contradicts the code the reader hits next; could you reword it so the base sort and the overflow-only span sort aren't conflated?

@mustafajw07

Copy link
Copy Markdown
Contributor Author

Thanks for the review! I've addressed both points:

  • Removed the dead code in useCalendarGridPlaceholderInDay.ts so the branch now relies solely on findAvailableIndex().
  • Updated the comment in useEventOccurrencesWithDayGridPosition.ts to accurately describe the base sort and the overflow-only span sort.

The PR has been updated. Could you take another look when you have a chance? Thanks! 🙌

@rita-codes

Copy link
Copy Markdown
Member

Thanks so much for tackling this, @mustafajw07 — this was an important fix and you nailed it. The resurfacing logic came out clean and well-tested, and the overflow accounting reads really well. Great work! 🎉

Heads-up: I pushed 3 small experiment demos to your branch (MonthViewOverflowContinuation, MonthViewOverflowPersist, MonthViewOverflowButtonOverlap), plus an entry in experiments.md.

Why: anything under docs/data/**/[A-Z]*.js is automatically screenshotted by our Argos visual regression suite, so these three become permanent guards for exactly the scenarios this PR fixes — the continuation bar collapsing to a free row, an overflow event that can never collapse staying counted in "+N more", and the "+N more" button staying visible behind a spanning bar. The unit tests cover the positioning logic, but they can't catch the visual half (the continuation arrow, a bar painting over the button) — these demos close that gap and will flag any future regression with a screenshot diff.

Thanks again! 🙌

@mustafajw07

Copy link
Copy Markdown
Contributor Author

Thank you so much for all the detailed reviews and guidance throughout this PR! I really appreciate the time you took to explain the reasoning behind the feedback—it helped me understand the problem much better. I also appreciate you pushing the final changes for the edge case; it was really helpful to learn from. Thanks again for all your support throughout this PR! 🙌

Guard the mui#22735 fix with three experiment demos picked up by Argos:
continuation after overflow, overflow persisting when no row frees up,
and the "+N more" button behind spanning bars.
@rita-codes
rita-codes force-pushed the fix/22735-scheduler-month-view-continuation-overflow branch from 074a516 to 50fd2bc Compare July 9, 2026 10:05
@rita-codes
rita-codes merged commit 6c59c31 into mui:master Jul 9, 2026
21 checks passed
@mustafajw07
mustafajw07 deleted the fix/22735-scheduler-month-view-continuation-overflow branch July 9, 2026 10:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: scheduler Changes related to the scheduler. type: bug It doesn't behave as expected.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[scheduler] Month view: multi-day event's continuation bar breaks when bumped into "+N more"

4 participants