Repository navigation
[scheduler] Pass timestamps and plain dates to the CalendarGrid - #24007
flaviendelangle merged 13 commits into
Conversation
`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>
Deploy previewBundle size
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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rita-codes
left a comment
There was a problem hiding this comment.
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
…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
left a comment
There was a problem hiding this comment.
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!
…mp-timeline-bounds
|
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
…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
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.TimeColumnandCalendarGrid.DayRowtakestartandendas timestamps, andCalendarGrid.DayCelltakes itsvalueas aPlainDate. The contexts of the columns and of the rows hold the timestamps and the time axis, with the display timezone.getTime()on the adapter values. The current day of a time column compares the day of its start withnowDayNumber.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, andaddTimelinePositionsToOccurrences(),createDependencyAnchorResolver()anduseEventTabNavigation()no longer take the adapter.TimeGridEventreads the axis of its column from the context of the column instead of acolumnprop.getDayList(), the adapter version ofgetDayListFromPlainDates(), andCalendarGrid.useGetDateFromPositionInColumn, which nothing called, are gone. The development check ofgetDayList(), which threw when the end was before the start, goes with it: the test of an inverted range now expects the empty list thatgetDayListFromPlainDates()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.
event-calendarevent-calendar-premiumevent-timeline-premiumAdapter usage
Counted like in #23943. The references go from 157 on master to 130.
addMinutes,endOfDayandgetMinutesare gone, so the Scheduler uses 20 methods and properties instead of 23.getTimegetTimezoneaddMillisecondssetHoursaddMinutesendOfDaygetMinutesValidation
pnpm test:unit --project "x-scheduler*" --run: 3,936 passed, before and after the/simplifypass. 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.useGetDateFromPositionInColumnandgetDayList()are gone with them.pnpm proptypesandpnpm docs:apihave nothing to change: the internals packages have no propTypes and no API pages.A
/simplifypass with four reviews (reuse, simplification, efficiency, altitude):isCurrentDay()takes a timestamp, anduseElementPositionInCollection()memoizes on the axis.moveTimestampToPlainDate().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,
TimeGridEventreads its axis from the context, and the docs of the axis ofuseElementPositionInCollection()and of the end of a time column, which isn't its last instant when the hour window is trimmed, are fixed. TheplainDatefield 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