Skip to content

[scheduler] Add all day events #18940

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
Aug 6, 2025
Merged

Conversation

noraleonte
Copy link
Contributor

@noraleonte noraleonte commented Jul 28, 2025

Preview link

Closes #17713
image

@noraleonte noraleonte added type: new feature Introduces a new piece of functionality or capability. scope: scheduler Changes or issues related to the scheduler product labels Jul 28, 2025
@mui-bot
Copy link

mui-bot commented Jul 28, 2025

Deploy preview: https://deploy-preview-18940--material-ui-x.netlify.app/

Bundle size report

Bundle Parsed Size Gzip Size
@mui/x-data-grid 0B(0.00%) 0B(0.00%)
@mui/x-data-grid/DataGrid 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-pro 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-pro/DataGridPro 0B(0.00%) ▼-1B(0.00%)
@mui/x-data-grid-premium 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-premium/DataGridPremium 0B(0.00%) 0B(0.00%)
@mui/x-charts 0B(0.00%) 0B(0.00%)
@mui/x-charts-pro 0B(0.00%) 🔺+1B(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%)

Details of bundle changes

Generated by 🚫 dangerJS against 16dee84

Comment on lines 166 to 175
<div className="AllDayGridCells">
{days.map((day) => (
<div
key={day.day.toString()}
className="DayTimeGridAllDayEventsCell"
aria-labelledby={`DayTimeGridHeaderCell-${day.day.toString()}`}
role="gridcell"
data-weekend={isWeekend(adapter, day) ? '' : undefined}
/>
))}
Copy link
Contributor Author

@noraleonte noraleonte Jul 28, 2025

Choose a reason for hiding this comment

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

This is a bit of a weird solution but I needed a way to render the grid (and colored backgrounds) behind the events. All day events can span across multiple columns, so they can't exactly be placed inside a specific cell. 🙈 If we agree on this solution (or a similar one) I can refactor the month view as well to respect the same structure.

Copy link
Member

@flaviendelangle flaviendelangle Jul 29, 2025

Choose a reason for hiding this comment

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

This would require some adjustment on the day grid drag and drop
We can start with this approach and see if it's problematic, but on Bryntum the events are rendered in the 1st cell with position: absolute from what I saw

Copy link
Member

Choose a reason for hiding this comment

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

I'm checking Google Calendar too, they take a similar approach: the event is placed inside the first cell with a specific width set (screenshot 1). Also when looking at the month view, I was wondering how they account for space from previous-day events, and it seems like there's a hidden element involved.
Screenshot 2025-07-29 at 10 36 01
Screenshot 2025-07-29 at 10 32 18
This seems far from a simple implementation 🙃, especially if we want to support drag and drop properly, so we should think it through carefully 🤔

Copy link
Member

@flaviendelangle flaviendelangle Jul 29, 2025

Choose a reason for hiding this comment

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

OK so if I understand correctly, to render an event that spans across multiple day, they render a visible event in the 1st cell that has position: absolute and a width to overlap on the next cell(s).
And on the other cells, they render a hidden event.

I think that's a good approach.
There might be some complexity to optimize the layout, but we can keep that for later (if I have 3 events, on from on day 1, one on day 2 and one on day 1 to 2, then I want ot render the 1st and 2nd event on the same line and the 3rd event on its own line).

Copy link
Member

Choose a reason for hiding this comment

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

Yes, that’s exactly right, we’ve been pairing and trying things out, and we’ve explored this approach. It actually looks quite promising!

@noraleonte worked her magic and it’s almost there 👏 This strategy allows us to rely on our existing primitives and also makes it much easier to handle both single-day and multi-day events in the month view.

As you said, there are some complex edge cases, especially when events overlap, but we believe they’re solvable 🙌

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Small update: I refactored this piece. The events are now rendered within the cell. When an all day event spans across multiple columns, the first column is the one that renders the event with a larger width (css black magic there 🙈 ), and the following ones render an invisible version of the event - could even be a div maybe. That part is there to be able to avoid overlapping and make sure the event are rendered in the right row.
I also made some changes to the selector @flaviendelangle. I need the information of which row the events start rendering on on each column to avoid the situation where the invisible event is rendered on the wrong row if there are no other event rendered above it.
image
I'll let you guys check out this approach and if we think it's good I'll refine this PR and implement the logic on the month view as well 👌

Copy link
Member

Choose a reason for hiding this comment

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

I left a couple of small suggestions, in general looks amazing! IMO the approach looks correct 💯

Comment on lines 81 to 87
allDayEvents = events.filter(
(event) =>
event.allDay &&
(!event.resource || !visibleResources.get(event.resource)) &&
adapter.startOfDay(event.start) <= adapter.startOfDay(day) &&
adapter.startOfDay(event.end) >= adapter.startOfDay(day),
);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm trying to cover an edge case here. If we are on a week view for instance, and have events that started last week, but end this week, they should be rendered starting from the first column (first displayed day of the week)
Super open to suggestions on this one

image image

Copy link
Member

Choose a reason for hiding this comment

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

I think this selector is becoming too crowded 😆
I tried reworking it so that it returns the list of events that should be rendered in a given day.
Feel free to modify it if it doesn't suit your needs.

To handle all day events on the month view, you will have to set shouldOnlyRenderEventInOneCell to true and handle the spanning correctly.

On the agenda view it now renders the events in all of its days.

And we can clean the function that splits all day and non all day events (based on yesterday's discussion) depending on how we want to handle non-all-day but multi-days events.
For now I kept it in the DayTimeGrid.tsx file for simplicity.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

@flaviendelangle flaviendelangle Jul 29, 2025

Choose a reason for hiding this comment

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

Damn, with this approach we don't support #18828
To support it, we would need to do the split all day / non all day at the selector level.
I'll make a 2nd commit to allow a method for shouldOnlyRenderEventInOneCell so that the selector can apply an event to all days when it will be rendered in the time grid but only to a single day when it will be rendered in the day grid.

It's not blocking though

@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged label Jul 28, 2025
Copy link

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged label Jul 28, 2025
Copy link

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged label Jul 29, 2025
@github-actions github-actions bot added PR: out-of-date The pull request has merge conflicts and can't be merged and removed PR: out-of-date The pull request has merge conflicts and can't be merged labels Jul 29, 2025
Copy link

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@noraleonte
Copy link
Contributor Author

It might make sense to implement the all day events on the month view in a follow-up PR. This is becoming a bit bloated and on the month view we might bump into more edge cases 🤔
Marking it as ready for review 👌

@noraleonte noraleonte marked this pull request as ready for review July 31, 2025 12:11
@flaviendelangle
Copy link
Member

No problem to split it in two yeah 👌

@@ -6,6 +6,46 @@ export const defaultVisibleDate = DateTime.fromISO('2025-07-01T00:00:00');
const START_OF_FIRST_WEEK = defaultVisibleDate.startOf('week');

export const initialEvents = [
{
id: 'allday-0',
Copy link
Member

@flaviendelangle flaviendelangle Jul 31, 2025

Choose a reason for hiding this comment

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

Nitpick: I would stick with "All day event A1" etc... type of name for consistency.

And more important: could you add an all day event that goes from the 30th to the 1st? To see if it correctly goes below the two "Conferences day" without creating a new line?

Copy link
Contributor Author

@noraleonte noraleonte Aug 1, 2025

Choose a reason for hiding this comment

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

I optimized the calculation of the row indexes because your suggestion helped me uncover some edge cases.
Here's what it looks like now:
image

Short answer: No, A2 does not go below B1. It renders the invisible cells in order. I think there are improvements to be made here:

  • A2 should more efficiently use the space that is created below
  • More importantly, C1, D1, D2, D3, and E1 could use the empty rows above. But there are things we would need to debate. For instance, which event takes precedence over the other? D1, D2 or D3? C1 move up? Does E1 go to the first row, or sticks after C1?
    I'll explore all of these points in a follow-up.

))}
</div>
</div>
<DayGrid.Root
Copy link
Member

Choose a reason for hiding this comment

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

Screencast.2025-07-31.14.35.37.mp4

Is the scroll expected here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

damn, no, not expected. I'll look into it

@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged label Jul 31, 2025
Copy link

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged label Aug 4, 2025
Comment on lines 119 to 134
ref={allDayHeaderWrapperRef}
className={clsx('DayTimeGridGridRow', 'DayTimeGridAllDayEventsGrid')}
role="row"
data-weekend={lastIsWeekend ? '' : undefined}
>
<div
className="DayTimeGridAllDayEventsCell DayTimeGridAllDayEventsHeaderCell"
role="columnheader"
>
{translations.allDay}
</div>
<DayGrid.Row
className="DayTimeGridAllDayEventsRow"
role="row"
style={{ '--column-count': days.length } as React.CSSProperties}
>
Copy link
Member

@rita-codes rita-codes Aug 5, 2025

Choose a reason for hiding this comment

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

We can't contain one row inside another row, we have role=row on line 121 and a nested one in 132 🤔
Screenshot 2025-08-05 at 16 38 40
I think a good solution would be to remove the outer role="row", and then we can associate the cells with both headers using aria-labelledby. That way, each cell can reference the "All day" row header and the day column header, even if the row header is rendered outside. This should keep the structure accessible and avoid nested row roles, which aren't valid, wdyt?

Comment on lines 167 to 183
const previousEventRowPosition = getEventWithLargestRowIndexForDay(dayKey, daysMap);

if (previousEventRowPosition + 1 > eventIndex + 1) {
for (let i = 1; i < previousEventRowPosition + 1; i += 1) {
if (
daysMap
.get(dayKey)!
.allDayEvents?.findIndex((eventInMap) => eventInMap.eventRowIndex === i) ===
-1
) {
eventRowIndex = i;
break;
}
}
} else {
eventRowIndex = previousEventRowPosition + 1;
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const previousEventRowPosition = getEventWithLargestRowIndexForDay(dayKey, daysMap);
if (previousEventRowPosition + 1 > eventIndex + 1) {
for (let i = 1; i < previousEventRowPosition + 1; i += 1) {
if (
daysMap
.get(dayKey)!
.allDayEvents?.findIndex((eventInMap) => eventInMap.eventRowIndex === i) ===
-1
) {
eventRowIndex = i;
break;
}
}
} else {
eventRowIndex = previousEventRowPosition + 1;
}
const allDayEvents = daysMap.get(dayKey)!.allDayEvents;
const usedIndexes = new Set(allDayEvents.map((event) => event.eventRowIndex));
eventRowIndex = 1;
while (usedIndexes.has(eventRowIndex)) {
eventRowIndex += 1;
}

I was testing this locally and I think we could simplify and slightly optimize it by using a Set to track used indexes, instead of looping and calling .findIndex() each time.
It avoids the need for getEventWithLargestRowIndexForDay and makes the logic a bit easier to follow. Let me know what you think because I might be missing something 🙌

Copy link
Member

Choose a reason for hiding this comment

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

Bonus track! After that we could also extract that section into a function like

function getEventRowIndex(
  event: CalendarEvent,
  day: SchedulerValidDate,
  days: SchedulerValidDate[],
  daysMap: Map<string, { allDayEvents: CalendarEventWithPosition[] }>,
  adapter: Adapter,
): number {
  const dayKey = adapter.format(day, 'keyboardDate');
  const eventFirstDay = adapter.startOfDay(event.start);

  // If the event starts before the current day, we need to find the row index of the first day of the event
  const isBeforeVisibleRange =
    adapter.isBefore(eventFirstDay, day) && !adapter.isSameDay(days[0], day);
  if (isBeforeVisibleRange) {
    const firstDayKey = adapter.format(
      adapter.isBefore(eventFirstDay, days[0]) ? days[0] : eventFirstDay,
      'keyboardDate',
    );

    // Try to find the row index from the original event placement on the first visible day
    const existingRowIndex = daysMap
      .get(firstDayKey)
      ?.allDayEvents.find((item) => item.id === event.id)?.eventRowIndex;

    return existingRowIndex ?? 1;
  }

  // Otherwise, we just render the event on the first available row in the column
  const usedIndexes = new Set(
    daysMap.get(dayKey)?.allDayEvents.map((item) => item.eventRowIndex) ?? [],
  );
  let i = 1;
  while (usedIndexes.has(i)) {
    i += 1;
  }
  return i;
}

So that part could look less crowded, like this =>
Screenshot 2025-08-05 at 19 06 29

Comment on lines 127 to 138
let eventDays: SchedulerValidDate[];
if (shouldOnlyRenderEventInOneCell) {
if (adapter.isBefore(eventFirstDay, days[0])) {
eventDays = [days[0]];
} else {
eventDays = [eventFirstDay];
}
} else {
eventDays = days.filter((day) =>
isDayWithinRange(day, eventFirstDay, eventLastDay, adapter),
);
}
Copy link
Member

Choose a reason for hiding this comment

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

It would be nice to extract this too, to a function called getEventDays or similar

Copy link
Member

@rita-codes rita-codes left a comment

Choose a reason for hiding this comment

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

Super nice job on this! 💙 You’ve handled a pretty complex piece of logic with care and it’s looking really solid, seriously, amazing work. I’ve suggested a small improvement around how the eventRowIndex is computed and left a few optional ideas on extracting logic to make it more readable, but not a blocker!

One thought that might help with readability long-term: since the logic is quite dense, maybe it could be worth adding some high-level comments like // Step 1: Sort events, // Step 2: Blabla. It might make it easier for the next person to follow the flow at a glance.

Again, awesome job, you clearly put a lot of care into this! 🙇‍♀️ 💙

@noraleonte noraleonte requested a review from rita-codes August 6, 2025 13:35
@@ -99,7 +86,7 @@ export const selectors = {
const dayKey = adapter.format(day, 'keyboardDate');
daysMap.set(dayKey, { events: [], allDayEvents: [] });
}

// STEP 1: Sort events by start date
Copy link
Member

Choose a reason for hiding this comment

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

💙 This helps a lot!

Copy link
Member

@rita-codes rita-codes left a comment

Choose a reason for hiding this comment

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

Looks amazing to me!! ✨ So much cleaner, again, thanks for taking care of this and good job 🥇

@noraleonte noraleonte merged commit d23c23e into mui:master Aug 6, 2025
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
scope: scheduler Changes or issues related to the scheduler product type: new feature Introduces a new piece of functionality or capability.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[scheduler][styled] Add support for "all-day" events
4 participants