Skip to content

[scheduler] Fix the navigation of the Timeline presets across DST transitions - #23966

Merged
flaviendelangle merged 13 commits into
mui:masterfrom
flaviendelangle:scheduler-plain-date-timeline-presets
Oct 26, 2026
Merged

flaviendelangle merged 13 commits into
mui:masterfrom
flaviendelangle:scheduler-plain-date-timeline-presets

Conversation

@flaviendelangle

@flaviendelangle flaviendelangle commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

This PR moves the ranges and the navigation of the Timeline presets to the plain dates of BaseTemporal, after the recurring events in #23943. The presets compute the first and last instants of the visible range, the number of grid cells of some of them, and the visible date of the next and previous buttons. It's independent of #23961, #23962 and #23963. The public API doesn't change.

Changes

  • getStartDate(), getEndDate() and navigate() of the 5 presets come from one helper, createPlainDateRange(), given the unit of the preset and the first day of its period:
    • The range starts on the first instant of that day, read on the plain date of the visible date: the day itself, or its first day of the month or the year with PlainDate.with(). dayAndWeek still starts on getStartOfWeek().
    • It ends on the last instant before the period that follows the last one, adding the unit count to the plain date of the start with PlainDate.add(). dayAndWeek no longer calls getEndOfWeek().
    • navigate() moves the visible date by days, weeks, months or years on the wall clock with addToAdapterDate(), keeping its time of day. A time that a DST transition skips moves forward by the length of the gap, and a repeated one resolves to its first instant, like the wall times of [scheduler] Fix recurring events across timezones and DST transitions #23943.
  • getCssUnitCount() of dayAndWeek and monthAndYear subtracts day numbers instead of calling differenceInDays().
  • plain-date-utils gets addToAdapterDate(), and addDaysOnWallClock() of the drag and drop, from [scheduler] Fix the drag and drop of the day grid across DST transitions and speed up its preview #23964, calls it, so that one function moves dates on the wall clock. The internals export the PlainDateDuration type.
  • Once [scheduler] Speed up the day grid and fix day lists across DST transitions #23961 or [scheduler] Fix the time axis across DST transitions and speed it up #23962 lands, the first and last instants of the ranges can come from their createStartOfPlainDate() and createEndOfPlainDate() instead of the offsetMs of createDateFromPlainDate().

Behavior changes

The old code depended on how TZDate resolves the times that DST transitions skip or repeat, which changes with the timezone of the system. An exhaustive scan of the navigation, below, found no change of the visible range in common timezones. The changes are in the visible date the buttons set, which onVisibleDateChange receives, and in the bounds of some ranges in rarer timezones.

  1. On systems with their own DST transitions, navigating could move the time of day of the visible date by an hour, with every unit. On a Paris system with the Timeline in Europe/London, the next button of dayAndHour from October 24th, 2026 at 02:00 went to October 25th at 01:00. It goes to 02:00 now, and so do dayAndWeek from October 18th, monthAndYear from September 25th and year from October 25th, 2025. On a London system with the Timeline in Europe/Paris, the next day of March 27th, 2027 at 02:30 was March 28th at 01:30, before the skipped hour. It's 03:30 now, after it. On a Lord Howe system, the 30-minute transitions moved some times by 30 minutes.
  2. A visible date moved to a time that a fall-back transition repeats resolved to its second instant on some systems, like a UTC or New York system for Europe/Paris. It resolves to the first instant on every system now.
  3. Some ranges started or ended on the wrong instant:
    • On a system in Australia/Lord_Howe, a dayAndHour or dayAndMonth range starting on April 5th, 2026 in America/Santiago started on April 4th at 23:30.
    • On a system whose DST transitions are at midnight, a range starting on the day of a transition of the system could start an hour late in other timezones. On a Havana system, a monthAndYear range starting on April 1st, 2012 in New York started at 01:00.
    • On a Paris or London system, the ranges of America/Nuuk, which changes its offset at the same instant, could end early. A monthAndYear range of October 2021 ended on October 30th at 22:59:59.999, 25 hours before the end of the month, and a dayAndWeek week ending on Saturday, October 30th ended an hour early.
    • On a system in Pacific/Apia, the ranges of other timezones skipped December 30th, 2011: a range starting that day started on December 31st, and a range across it ended one day late. A dayAndWeek week from Saturday, December 24th in New York ended on Saturday the 31st instead of Friday the 30th.
    • In a timezone that skips a day, a range ending after it ended one day late, on every system. Pacific/Kiritimati skipped December 31st, 1994, so a monthAndYear or year range ending with 1994 ended on January 1st, 1995 at 23:59:59.999, and monthAndYear counted 32 days in December 1994. The range ends just before 1995 starts now, and December 1994 has 30 days.
    • A midnight that a DST transition repeats, like November 1st, 2026 in Havana, now starts a range at its first instant on every system, where a Los Angeles system started it at the second one. A range ending on a day whose last hour a DST transition repeats ends on the later 23:59:59.999, on every system, like the all-day events of [scheduler] Fix recurring events across timezones and DST transitions #23943.
  4. An invalid visibleDate now throws in all the presets, where master rendered an empty Timeline. The Event Calendar already throws on master.
  5. The year preset throws for a visibleDate from 9970, because the end of its 30 years is computed from January 1st, 10000, after the last year of PlainDate. The next button gets there from 9940. Master rendered these ranges, up to one ending in 10028. The Scheduler doesn't support dates after 9999.

getStartOfWeek() also depends on the system on these days, which #23961 fixes. Until then, on a Lord Howe system, dayAndWeek in Santiago can still start on April 4th, 2026 at 23:30. Its 16 weeks then end on July 24th, where they ended on July 18th before. On a Paris system, a week of America/Nuuk from Sunday, October 31st, 2021 still starts on October 30th at 23:00.

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.17 kB 75.19 kB +0.02 kB
event-calendar-premium 90.99 kB 91.03 kB +0.04 kB
event-timeline-premium 96.14 kB 96.18 kB +0.04 kB

The Timeline already bundles PlainDate. The difference is the helpers of preset-utils.tsx and addToAdapterDate(), which the Event Calendar now calls from addDaysOnWallClock().

Adapter usage

Counted like in #23943. The references go from 493 to 474, and endOfYear is gone, so the Scheduler uses 50 methods and properties instead of 51.

Method or property master This PR Difference
addDays 28 24 −4
addMonths, addWeeks, addYears 15 9 −6
differenceInDays 10 8 −2
startOfDay 35 33 −2
endOfDay 21 19 −2
startOfMonth, startOfYear 13 11 −2
endOfMonth, endOfYear 4 2 −2
getTimezone 17 18 +1

Validation

  • pnpm test:unit --project "x-scheduler*" --run: 3,856 passed, and pnpm test:browser on x-scheduler-internals, x-scheduler-internals-premium and x-scheduler-premium: 3,146 passed. With master's preset-utils.tsx, the 18 tests of getStartDate(), getEndDate() and navigate() fail. They cover the navigation of the 4 units, the starts of dayAndHour and monthAndYear, and the ends of dayAndHour, dayAndWeek, monthAndYear and year.

  • A randomized harness, not committed, compared the presets with master's on 5,000 random cases under each of 8 system timezones (UTC, New York, Paris, Lord Howe, Havana, Kolkata, Apia and Santiago). It covered 10 display timezones around their DST transitions: the start, end, cell count and navigation of the 5 presets, with random unit counts and week starts. Every difference falls into one of the behavior changes above. After the last /simplify pass, it compared the presets with their previous version on 3,000 random cases under 6 system timezones. The only differences were the ends of dayAndWeek on the Apia and Lord Howe systems, described above.

  • After Rita's review, a search compared the start, end and cell count of monthAndYear and year with master's for every month from 1990 to 2030, under 12 system timezones and in 16 display timezones, then for year under 19 system timezones with unusual transitions. Every difference falls into the behavior changes above, which now list the Havana system, Nuuk and Kiritimati ones, and the start of a year never changed. With the bounds the presets return, differenceInDays(end, start) + 1 gives the same count as the day numbers under 14 system timezones and in 14 display timezones. They only differ when getStartOfWeek() returns a start that isn't a midnight, like the Nuuk week above.

  • An exhaustive scan ran every quarter of an hour of 3 days around 8 DST transitions of 2026, in 9 display timezones (UTC, New York, Los Angeles, São Paulo, London, Paris, Tokyo, Sydney and the system one). It went through the navigation of the 5 presets by one step and by their unit count, under 6 system timezones (UTC, New York, Los Angeles, London, Paris and Sydney). The new visible dates never changed the visible range.

  • Six review rounds with Codex and the base-ui-review skill, in three passes, approved without findings. Two /simplify passes shared one plain-date range between the presets, then built their start, end and navigation from one helper.

  • TypeScript, ESLint and Prettier pass.

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

🤖 Generated with Claude Code

The presets of the Timeline read the first and last days of their range and
move the visible date with the PlainDate API, instead of the adapter's
calendar math. Navigating keeps the wall-clock time of the visible date,
whatever the timezone of the system.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@flaviendelangle flaviendelangle added type: bug It doesn't behave as expected. scope: scheduler Changes related to the scheduler. labels Oct 22, 2026
@code-infra-dashboard

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

Copy link
Copy Markdown

Deploy preview

https://deploy-preview-23966--material-ui-x.netlify.app/
QR code for https://deploy-preview-23966--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 🔺+26B(+0.01%) 🔺+16B(+0.01%)
@mui/x-scheduler-premium ▼-171B(-0.03%) 🔺+65B(+0.04%)
@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.

flaviendelangle and others added 6 commits October 22, 2026 20:03
…transitions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The `dayAndHour`, `dayAndMonth`, `monthAndYear` and `year` presets build their
range and navigation with `createPlainDateRange()`, from the start of their
period and its duration. The end of a range is the last instant before the
period that follows it, so it no longer subtracts a day it then adds back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `createPlainDateRange()` builds the start, the end and the navigation of the day, week, month and year presets from one unit, and `getEndDate()` adds the units to the plain date of the start.
- `addToAdapterDate()` moves an adapter date by a plain-date duration on its wall clock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and repeated times

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Oct 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Oct 23, 2026
@rita-codes

Copy link
Copy Markdown
Member

A few things from my review, all checked by running the presets on this branch and on master. No bugs in the normal date range, and the new tests all fail on master:

🟡 addToAdapterDate() duplicates addDaysOnWallClock() (adapter.ts#L147). drag-utils.ts, which landed with #23964, has the same body with { days: dayCount }, next to moveToPlainDate(). One could call the other, so there's a single wall-clock mover.

🟡 The weeks, months and years units aren't pinned. Every new navigate test uses dayAndHour, and the bound tests only cover dayAndHour and the end of dayAndWeek. Putting master's code back in these places keeps the whole suite green:

  • navigate of dayAndWeek, monthAndYear and year with adapter.addWeeks/addMonths/addYears(), so behavior changes 1 and 2 aren't pinned for them.
  • getStartDate / getEndDate of monthAndYear and year with startOfMonth/endOfMonth/startOfYear/endOfYear.
  • getDayCount() with differenceInDays(end, start) + 1.

The Havana repeated midnight of change 3 isn't tested either.

ℹ️ Ranges past year 9999 now throw. The year preset adds 30 years with PlainDate.add(), so any visibleDate from 9970 throws during render, and the next button gets there from 9940. Master renders up to 10028. It's absurd in practice, but it isn't listed with the behavior changes, and #23961 avoided similar throws with day-number arithmetic.

ℹ️ An invalid visibleDate now throws in all the presets, where master rendered an empty Timeline. The Event Calendar already throws on master, so it's consistent, just not mentioned.

ℹ️ createDateFromPlainDate(..., 0) and (..., -1) are the start and end of a plain date, like createStartOfPlainDate() / createEndOfPlainDate() from #23961 and #23962. The offsetMs parameter only exists for the -1, and its JSDoc says "first instant" for both. Once those land, this could use them.

flaviendelangle and others added 3 commits October 26, 2026 10:19
…pterDate()

addDaysOnWallClock() had the same body as addToAdapterDate() with a duration in days. It now
calls it and keeps returning the date unchanged for 0 days. The JSDoc of
createDateFromPlainDate() also names what an `offsetMs` of -1 returns.

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

The navigation tests run on the days, weeks, months and years of the presets, and new tests
cover the start of a month at a midnight that Havana repeats on a Los Angeles system, the end of
a month in Nuuk on a Paris system, and the end of a year that Kiritimati cut short. They all fail
with master's presets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Oct 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

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

# Conflicts:
#	packages/x-scheduler-internals/src/internals/utils/plain-date-utils/adapter.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 26, 2026
# Conflicts:
#	packages/x-scheduler-internals-premium/src/internals/utils/preset-utils.tsx
@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Oct 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

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 Oct 26, 2026
@flaviendelangle
flaviendelangle merged commit 09fe99b into mui:master Oct 26, 2026
22 checks passed
@flaviendelangle
flaviendelangle deleted the scheduler-plain-date-timeline-presets branch October 26, 2026 10:55
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.

2 participants