Skip to content

[scheduler] Pass timestamps and plain dates to the CalendarGrid - #24007

Merged
flaviendelangle merged 13 commits into
mui:masterfrom
flaviendelangle:scheduler-timestamp-calendar-grid-bounds
Oct 27, 2026
Merged

flaviendelangle merged 13 commits into
mui:masterfrom
flaviendelangle:scheduler-timestamp-calendar-grid-bounds

Conversation

@flaviendelangle

@flaviendelangle flaviendelangle commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

It moves the bounds of the CalendarGrid parts from adapter values to timestamps and plain dates. The views pass the bounds of the time columns and of the day rows as timestamps read in the display timezone, and the days of the day cells as plain dates. The time columns and the day rows build their time axis from those timestamps, so nothing downstream reads the adapter values of the views again. The public API doesn't change: the CalendarGrid parts are internal, and the occurrences keep their shape.

Changes

  • CalendarGrid.TimeColumn and CalendarGrid.DayRow take start and end as timestamps, and CalendarGrid.DayCell takes its value as a PlainDate. The contexts of the columns and of the rows hold the timestamps and the time axis, with the display timezone.
  • The time-grid drop target, the day cell drop target, the placeholder hooks of the days and of the ranges, the time events, the current-time indicator and the placeholder selectors of the Event Calendar read those timestamps instead of calling getTime() on the adapter values. The current day of a time column compares the day of its start with nowDayNumber.
  • The views compute the bounds once from their processed days: the time-grid columns, the rows of the Month View and of the day grid, and their cells.
  • computeElementPositionInCollection() takes the time axis of [scheduler] Read the bounds of the time axis as timestamps #24006 without the adapter. The Timeline passes the axis of its preset, and addTimelinePositionsToOccurrences(), createDependencyAnchorResolver() and useEventTabNavigation() no longer take the adapter.
  • The fetch range of the agenda is a range of plain dates, which the lazy loading of the Event Calendar converts once.
  • TimeGridEvent reads the axis of its column from the context of the column instead of a column prop.
  • getDayList(), the adapter version of getDayListFromPlainDates(), and CalendarGrid.useGetDateFromPositionInColumn, which nothing called, are gone. The development check of getDayList(), which threw when the end was before the start, goes with it: the test of an inverted range now expects the empty list that getDayListFromPlainDates() returns.

Behavior changes

None. The timestamps are the ones the parts read from the adapter values: the views already built their bounds on the plain dates of their days in the display timezone, and the end of a row is still the last instant before the next day starts.

Bundle size

Measured from source with esbuild, minified, with third-party packages external and without the error minifier, gzip level 9.

Entry master This PR Difference
event-calendar 75.89 kB 75.55 kB −0.34 kB
event-calendar-premium 91.47 kB 91.17 kB −0.30 kB
event-timeline-premium 96.44 kB 96.32 kB −0.12 kB

Adapter usage

Counted like in #23943. The references go from 157 on master to 130. addMinutes, endOfDay and getMinutes are gone, so the Scheduler uses 20 methods and properties instead of 23.

Method or property master This PR Difference
getTime 42 22 −20
getTimezone 11 9 −2
addMilliseconds 5 4 −1
setHours 3 2 −1
addMinutes 1 0 −1
endOfDay 1 0 −1
getMinutes 1 0 −1

Validation

  • pnpm test:unit --project "x-scheduler*" --run: 3,936 passed, before and after the /simplify pass. The tests of the CalendarGrid, the selectors, the Timeline grid and its dependencies, and the time and month views pass in the browser (900 tests). CI runs the full browser suites.

  • The tests of the CalendarGrid parts and of the placeholder selectors pass timestamps and plain dates, and the tests of CalendarGrid.useGetDateFromPositionInColumn and getDayList() are gone with them.

  • pnpm proptypes and pnpm docs:api have nothing to change: the internals packages have no propTypes and no API pages.

  • A /simplify pass with four reviews (reuse, simplification, efficiency, altitude):

    • The time-grid drop target and the contexts of the columns and of the rows read the bounds from their axis only.
    • isCurrentDay() takes a timestamp, and useElementPositionInCollection() memoizes on the axis.
    • The current-time indicator reuses moveTimestampToPlainDate().
    • The adapter parameters that only fed the axis are gone.
  • A review round with Codex and the base-ui-review skill approved without findings.

  • Rita's review: two new tests resize an event in the Month View onto the day of its other bound, the start onto the day of the end, and the end onto the day of the start on March 8th, 2026 in America/Havana, a day without a midnight. The day cell drop target compares day numbers on every pointer move instead of plain dates, TimeGridEvent reads its axis from the context, and the docs of the axis of useElementPositionInCollection() and of the end of a time column, which isn't its last instant when the hour window is trimmed, are fixed. The plainDate field on the processed dates and a helper for the end of a plain date are left for later: the processed dates are part of the public API, so they change when the migration does.

  • TypeScript, ESLint and Prettier pass.

  • I have followed (at least) the PR section of the contributing guide.

🤖 Generated with Claude Code

flaviendelangle and others added 2 commits October 26, 2026 18:49
`TimelineAxis` holds the bounds of an axis as timestamps, with the timezone
they're read in, and `getTimelineAxis()` reads them once from the adapter
values of a time-grid column or of a Timeline preset. The axis helpers, the
header iteration, the Timeline placeholder selectors and the Timeline lazy
loading read the timestamps instead of calling `getTime()` and
`getTimezone()` on the bounds.

The Timeline presets read the visible date in the display timezone, like the
views of the Event Calendar. With a visible date in UTC and the Timeline in
America/New_York, the days of the preset started at 20:00 New York time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The views pass the bounds of the CalendarGrid parts as timestamps read in the
display timezone, and the days of the day cells as plain dates. The time
columns and the day rows build their time axis from those timestamps, and the
drop targets, the placeholder hooks and the placeholder selectors read them
instead of the adapter values of the views.

`computeElementPositionInCollection()` takes the time axis, without the
adapter, and the Timeline passes the axis of its preset. The agenda fetches
the plain dates of its range, `getDayList()` is replaced by
`getDayListFromPlainDates()`, and `CalendarGrid.useGetDateFromPositionInColumn`,
which nothing called, is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@flaviendelangle flaviendelangle added type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature. scope: scheduler Changes related to the scheduler. labels Oct 26, 2026
@code-infra-dashboard

code-infra-dashboard Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Deploy preview

https://deploy-preview-24007--material-ui-x.netlify.app/
QR code for https://deploy-preview-24007--material-ui-x.netlify.app/

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.24KB(-0.29%) ▼-300B(-0.25%)
@mui/x-scheduler-premium ▼-1.09KB(-0.17%) ▼-283B(-0.16%)
@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.

The contexts of the time columns and of the day rows only expose their axis,
which holds the bounds. `isCurrentDay()` takes a timestamp,
`useElementPositionInCollection()` memoizes on the axis, and the current-time
indicator reuses `moveTimestampToPlainDate()`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
flaviendelangle and others added 2 commits October 26, 2026 19:45
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@flaviendelangle
flaviendelangle marked this pull request as ready for review October 27, 2026 06:48

@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.

PR review

Nothing is merge-blocking. Reviewed the PR's own two commits on top of #24006. The "no behavior change" claim holds: the three timezone sources the new code mixes (the store's displayTimezone, the timezone argument of the creation and drop hooks, and the label of the adapter values the parts used to receive) are all the display timezone, skipped days never get a cell, and the agenda fetch range still covers the whole last day. The full scheduler jsdom suites pass (3,937 tests), and tsc and ESLint pass on the four packages. What remains is one untested branch the PR rewrote, two docs that now say something false, and the altitude move finished one level short.

Bugs (0)

No findings.

Tests (2)

1. 🟡 Resizing a day-grid event onto the day of its other bound has no test, and the PR rewrote that branch

Location: packages/x-scheduler-internals/src/calendar-grid/day-cell/useDayCellDropTarget.ts:72

const newStart =
  order === 0 ? dayStart : moveTimestampToPlainDate(data.start, value, timezone);
...
order === 0
  ? getEndOfDayMs(dayStart, timezone)
  : moveTimestampToPlainDate(data.end, value, timezone);

dayStart is now toEpochMs(value, 0, timezone) from the PlainDate prop, where #24006 had getStartOfDayMs(adapter.getTime(value), timezone). There is no unit test for useDayCellDropTarget, and the month and week drag tests only resize to a different day with getUTCDate()).to.not.equal(...) assertions. Nothing drops the start handle on the end's day or the end handle on the start's day.

Failure scenario: A regression in the collapse branch on a day without midnight (Havana 2026-03-08) or with a repeated last hour (Santiago 2026-04-04) passes the suite.

Fix: Two cases in dragAndDrop.MonthView.test.tsx: start handle dropped on the end's day → start === toEpochMs(day, 0); end handle dropped on the start's day → end === getEndOfDayMs(...), asserted at ISO-instant level, one of them on a Havana day.

2. ℹ️ One expectation changed value, and the deleted hook took the only snap-rounding tests with it

Location: packages/x-scheduler-internals/src/get-day-list/getDayList.test.ts:21

The inverted-range case went from to.throw(/must be a day after/) to to.deep.equal([]): the dev-mode guard lived in the deleted adapter getDayList(), which had no callers at #24006, so this is dead-code removal, but it is the one expectation in the diff that is not a shape conversion and the PR body does not mention it. Separately, useCalendarGridDateFromPositionInTimeColumn.test.tsx held the only unit-level "floor to snap minutes" cases; the live equivalents, roundToDragPrecision and floorToWallClockMinutes, have no test file of their own (pre-existing gap).

Simplifications (3)

1. 🟡 TimeGridEvent still receives the column axis as a prop after the layer moved to context

Location: packages/x-scheduler/src/internals/components/event/time-grid-event/TimeGridEvent.types.ts:13

column: TimelineAxis;

The PR replaced the column prop of ColumnInteractiveLayer with useCalendarGridTimeColumnContext().axis, then passes it on through TimeGridEventProps.column into useTimeGridEvent(occurrence, column), whose only uses are collection: column and column.dayEndMinute - column.dayStartMinute. TimeGridEvent is rendered only from that layer, and useTimeGridEvent has no other caller.

Fix: useTimeGridEvent reads useCalendarGridTimeColumnContext().axis itself (the hook is now exported for exactly this), and column drops from the props type, both variants and the two JSX sites. Same object from the same provider, so behavior-neutral.

2. 🟡 PlainDate.compare on every pointer move where a day-number subtraction does the job

Location: packages/x-scheduler-internals/src/calendar-grid/day-cell/useDayCellDropTarget.ts:65

const order = PlainDate.compare(value, toPlainDateTime(data.end, timezone).date);

PlainDate.compare is compareOrder on the two day numbers, so value.dayNumber - toLocalDayNumber(data.end, timezone) gives the same sign without allocating a PlainDate and computing msInDay per move. The PR already uses that shape two blocks above (value.dayNumber - grabbedPlainDate.dayNumber), and the PlainDate import becomes type-only.

Fix: Replace both PlainDate.compare(...) calls (lines 65 and 77) with the day-number difference.

3. ℹ️ Start-of-day → end-of-day round trips, and the plain date re-derived from the processed day

Location: packages/x-scheduler-internals/src/calendar-grid/day-cell/CalendarGridDayCell.tsx:61

toEpochMs(value, 0, tz) then getEndOfDayMs(start, tz) appears here and in the drop target; the lazy-loading plugin converts range.end to a start-of-day instant only for toWholeDays to redo getStartOfDayMs/getEndOfDayMs on it; and TimeGridColumn.tsx:97 branches on endTime === 24 where toEpochMs(plainDate, 24 * HOUR_MS) - 1 is the same instant. This is the third PR in the series that would use a getEndOfPlainDateMs(plainDate, tz) in timeZone.ts. Relatedly, DayGridCell.tsx:95, MonthViewCell.tsx and the agenda fetchRange all run toPlainDateTime(day.timestamp, displayTimezone).date on a processed day that buildProcessedDate already computed and threw away; a plainDate field on SchedulerProcessedDate would remove all of them, probably as a follow-up.

Docs (3)

1. 🟡 collection JSDoc still names viewConfig and presetConfig

Location: packages/x-scheduler-internals/src/internals/utils/useElementPositionInCollection.ts:73

/**
 * The displayed range and daily hour window the element is positioned in: a
 * time-grid column (`viewConfig`) or the timeline's axis (`presetConfig`).
 */
collection: TimelineAxis;

The PR changed the type on the next line but kept the text. viewConfig is the user's per-view hour range, not an axis, and the callers now pass useCalendarGridTimeColumnContext().axis, useCalendarGridDayRowContext().axis or config.axis.

Fix: "The axis the element is positioned in: the axis of a CalendarGrid time column or day row, or the timeline preset's config.axis."

2. 🟡 "The last instant of the column" is false with a trimmed hour window

Location: packages/x-scheduler-internals/src/calendar-grid/time-column/CalendarGridTimeColumn.tsx:188

/**
 * The last instant of the column, as a timestamp read in the display timezone.
 */
end: number;

The view builds end as endTime === 24 ? getEndOfDayMs(...) : toEpochMs(plainDate, endTime * HOUR_MS, displayTimezone), so with endTime: 20 it is 20:00:00.000, the exclusive bound of the window, and the code treats it as exclusive (noon >= end, placeholder.start >= end). The last instant would be 19:59:59.999. Same wording on useCalendarGridPlaceholderInRange.Parameters.end. The row doc is fine because rows always end at getEndOfDayMs.

Failure scenario: Someone adds inclusive <= end logic for an event ending at 20:00 believing end is the last visible instant.

Fix: "The end of the column as a timestamp: the last instant of its day when the whole day is displayed, otherwise the exclusive end of its hour window, read in the display timezone."

3. ℹ️ An orphaned comment, and two removed symbols live in a published package

Location: packages/x-scheduler-internals/src/calendar-grid/day-cell/useDayCellDropTarget.ts:44

"Days are compared as plain dates, and each date moves on the wall clock of the display timezone." used to introduce the valuePlainDate line the PR removed; it now hangs above a blank line. Drop the blank line so it heads the "Move…" block. Separately, @mui/x-scheduler-internals is published to npm with ./calendar-grid and ./get-day-list subpaths, so CalendarGrid.useGetDateFromPositionInColumn and getDayList() were technically reachable; the repo treats that package as internal (no changelog section, no docs imports), so this only matters if the team considers those subpaths semver surface.

Verdict

Approve after nits — no regression, but the rewritten resize-collapse branch is untested and two new JSDoc lines state something false.


🤖 Review generated with Claude Code

flaviendelangle and others added 5 commits October 27, 2026 12:09
…ezone

- The docs of `displayTimezone` and of the Timeline navigation say that the visible range starts on the day of the visible date in the display timezone, and that the next and previous navigation give dates in that timezone.
- The next and previous navigation of the Timeline share one helper, like the ones of the Event Calendar.
- `timelineAxisOffsetToTimestamp()` adds the days to the day number of the start of the axis, without creating plain dates on every pointer move.
- The axis helpers say that the dates placed on the axis are read in its timezone.
- New tests: the timezone of the navigated visible date and of `onVisibleDateChange`, the start of the `dayAndWeek`, `monthAndYear` and `year` presets, the timezone of the axis, and the four days of the `dayAndHour` header.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ther bound

- Two new tests resize an event in the Month View onto the day of its other bound, one on a day without a midnight.
- The day cell drop target compares day numbers instead of plain dates on every pointer move.
- `TimeGridEvent` reads the axis of its column from the context of the column instead of a `column` prop.
- The docs of the axis of `useElementPositionInCollection()` and of the end of a time column, which isn't its last instant when the hour window is trimmed, are fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The header of the Timeline only renders the days that fit in the browser, so the test checks the first day and that the span stops before July 4th.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@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.

Follow-up verified: the two cross-day resize tests (the Havana one included), the day-number subtraction in the drop target, TimeGridEvent reading the axis from the column context, the fixed axis/column-end docs and the relocated comment. PR description now mentions the removed inverted-range guard. Full scheduler suite green locally. Thanks!

@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Oct 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

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

…estamp-calendar-grid-bounds

# Conflicts:
#	packages/x-scheduler-internals-premium/src/timeline-grid/current-time-indicator/TimelineGridCurrentTimeIndicator.tsx
#	packages/x-scheduler-internals/src/calendar-grid/current-time-indicator/CalendarGridCurrentTimeIndicator.tsx
#	packages/x-scheduler-premium/src/event-timeline-premium/content/timeline-dependencies/dependencyArrowGeometry.test.ts
@github-actions github-actions Bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Oct 27, 2026
…mp-calendar-grid-bounds

# Conflicts:
#	packages/x-scheduler-internals/src/calendar-grid/time-column/useTimeDropTarget.ts
#	packages/x-scheduler-internals/src/internals/utils/timeline-axis.bench.ts
#	packages/x-scheduler-internals/src/internals/utils/timeline-axis.ts
#	packages/x-scheduler-internals/src/internals/utils/useElementPositionInCollection.ts
#	packages/x-scheduler-premium/src/event-timeline-premium/tests/dependencyGeometryTestUtils.ts
#	packages/x-scheduler/src/internals/components/day-time-grid/TimeGridColumn.tsx
#	packages/x-scheduler/src/internals/components/event/time-grid-event/TimeGridEvent.types.ts
#	packages/x-scheduler/src/internals/components/event/time-grid-event/useTimeGridEvent.ts
@flaviendelangle flaviendelangle added scope: scheduler Changes related to the scheduler. and removed scope: scheduler Changes related to the scheduler. labels Oct 27, 2026
@flaviendelangle
flaviendelangle merged commit f6611c8 into mui:master Oct 27, 2026
27 checks passed
@flaviendelangle
flaviendelangle deleted the scheduler-timestamp-calendar-grid-bounds branch October 27, 2026 14:18
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: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants