Skip to content

[temporal adapter date-fns] Fix date-only string parsing and setTimezone duck typing#4561

Open
rita-codes wants to merge 2 commits intomui:masterfrom
rita-codes:fix/temporal-adapter-date-only-utc
Open

[temporal adapter date-fns] Fix date-only string parsing and setTimezone duck typing#4561
rita-codes wants to merge 2 commits intomui:masterfrom
rita-codes:fix/temporal-adapter-date-only-utc

Conversation

@rita-codes
Copy link
Copy Markdown
Member

@rita-codes rita-codes commented Apr 8, 2026

Fix two issues in TemporalAdapterDateFns:

Date-only string parsing

Date-only strings (e.g. "2026-04-06") are parsed as UTC midnight by the JS spec. The adapter was using local getters (getDate(), getHours(), etc.) to extract the components, which causes the day to roll back by one in timezones behind UTC (e.g. America/Sao_Paulo).

The fix uses UTC getters for date-only strings (no T in the input) and keeps local getters for datetime strings (which JS parses as local time).

setTimezone duck typing

setTimezone used instanceof TZDate to detect timezone-capable dates. This fails when multiple copies of @date-fns/tz exist in the bundle (e.g. different versions resolved by the package manager), because the object is a TZDate but from a different constructor.

The fix uses duck typing (typeof value.withTimeZone === 'function') instead of instanceof.

Tests added

  • Date-only string in negative UTC offset (system timezone)
  • Date-only string in a named timezone
  • Datetime strings still use local getters
  • setTimezone with a non-TZDate object that supports withTimeZone

Note

These fixes were already applied and validated in the internal copy of this adapter used by @mui/x-scheduler.

@mui-bot
Copy link
Copy Markdown

mui-bot commented Apr 8, 2026

Bundle size report

Bundle Parsed size Gzip size
@base-ui/react 0B(0.00%) 0B(0.00%)

Details of bundle changes


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

@rita-codes rita-codes self-assigned this Apr 8, 2026
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 8, 2026

commit: c83da88

@rita-codes rita-codes added the internal Behind-the-scenes enhancement. Formerly called “core”. label Apr 8, 2026
@netlify
Copy link
Copy Markdown

netlify bot commented Apr 8, 2026

Deploy Preview for base-ui ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit 63b299e
🔍 Latest deploy log https://app.netlify.com/projects/base-ui/deploys/69d6780231bece00080d1952
😎 Deploy Preview https://deploy-preview-4561--base-ui.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link
Copy Markdown

netlify bot commented Apr 8, 2026

Deploy Preview for base-ui ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit c83da88
🔍 Latest deploy log https://app.netlify.com/projects/base-ui/deploys/69d67888df739a0007bde814
😎 Deploy Preview https://deploy-preview-4561--base-ui.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Copy Markdown
Member

@LukasTy LukasTy left a comment

Choose a reason for hiding this comment

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

LGTM, but I'd suggest waiting for a second pair of eyes from Flavien.

Claude Opus 4.6 review

Author: @rita-codes | Files changed: 2 (+78 / -15) | CI: All 23 checks passing


Summary

Two targeted bug-fixes in TemporalAdapterDateFns:

  1. Date-only string parsing"2026-04-06" is parsed by JS as UTC midnight, but the adapter previously extracted components with local getters (getDate(), etc.), causing the date to roll back a day in negative-UTC timezones. The fix detects date-only strings (no T in the input) and switches to UTC getters (getUTCDate(), etc.).

  2. setTimezone duck typing — Replaces instanceof TZDate with typeof value.withTimeZone === 'function', which is resilient to duplicate copies of @date-fns/tz in the bundle.


Detailed Analysis

Fix 1: Date-only UTC parsing

Aspect Assessment
Correctness The heuristic !value.includes('T') accurately targets ECMA-262 date-only forms (YYYY-MM-DD), which are indeed parsed as UTC. Datetime strings with T are parsed as local time. This is correct per spec.
Edge cases — date-only with timezone suffix A string like "2026-04-06Z" or "2026-04-06+05:30" has no T, would be classified as "date-only", and UTC getters would be used. This is actually correct since those are also UTC midnight per spec (date-only + zone offset).
Edge cases — non-ISO strings Strings like "April 6, 2026" have no T and would be treated as date-only, using UTC getters. The behavior of new Date("April 6, 2026") is implementation-defined (not ISO 8601). However, the date() method is documented to accept ISO strings, so this is acceptable.
system/default timezone path For date-only strings, a new Date(year, month, date) is constructed, which creates a local-time midnight — logically correct: the user said "April 6th", they get April 6th at local midnight.
Named timezone path UTC getters are used to construct TZDate(y, m, d, h, min, s, ms, tz) — correct: extracts the intended face-value date, then anchors it in the target timezone.

Verdict: Sound fix. The heuristic is aligned with the JS specification.

Fix 2: setTimezone duck typing

Aspect Assessment
Correctness typeof value?.withTimeZone === 'function' is a reasonable duck-type check for TZDate objects. It avoids the classic cross-realm / duplicate-package instanceof failure.
Behavioral change The old code had a special path: if value instanceof TZDate && isSystemTimezone, it called this.toJsDate(value). If !(value instanceof TZDate) && isSystemTimezone, it returned value as-is. The new code always calls this.toJsDate(value) when isSystemTimezone, even for plain Date objects. toJsDate on a plain Date simply returns it (if (value instanceof TZDate) ... else return value), so this is functionally equivalent.
Risk of false positives Only objects with a .withTimeZone function will trigger the duck-type path. This is a very specific method name; false positives are negligible in practice.

Verdict: Clean simplification with correct behavior preservation.

Tests

  • 4 new test cases covering: date-only in negative UTC offset, date-only in named timezone, datetime strings still using local getters, and setTimezone duck typing with a fake TZDate-like object.
  • Tests manipulate process.env.TZ with proper afterEach cleanup.
  • Coverage is adequate for the changes.

Comment typo fix

The comment in date() was corrected from "whereas we want to create that represents" → "whereas we want to create a date that represents". Note: the same typo still exists in the parse() method in the base-ui PR (line visible in the diff context). This could be flagged as a nit.


Cross-reference with MUI X local copy

The internal copy at packages/x-scheduler-headless/.../TemporalAdapterDateFns.ts already contains both fixes (date-only UTC parsing and duck-typing setTimezone). The PR is upstreaming these proven changes back to base-ui. This is a positive signal — the fixes have been battle-tested in the scheduler package.

One minor discrepancy: the local copy's parse() method at line 177 still has the uncorrected typo "whereas we want to create that represents" — same as the base-ui PR, which only fixes the typo in date() but not in parse().


Potential Concerns

  1. Minor: The isDateOnly heuristic relies on value being a string, which is guaranteed by the method signature (value: T extends string | null), so no issue. However, if someone passed a numeric timestamp coerced to string (e.g. "1680739200000"), it has no T, would be treated as date-only, and UTC getters would be used. This is an extremely unlikely edge case and the adapter documents ISO string input, so this is acceptable.

  2. Minor nit: The typo in the parse() method comment ("whereas we want to create that represents") could be fixed in the same PR for consistency, since the date() method comment was already corrected.

  3. No breaking changes — The public API surface is unchanged. Both fixes are purely behavioral corrections.


Merge Readiness Score

Category Score
Correctness 9/10 — Both fixes are spec-aligned and logically sound
Test coverage 8/10 — Good coverage; could add a positive-offset timezone test for completeness
Code quality 9/10 — Clean, well-commented, minimal diff
Risk 9/10 — Low risk; fixes proven in x-scheduler first
Documentation 8/10 — Excellent PR description; minor typo in parse() comment left unfixed

Overall: 8.5/10 — Ready to merge

The PR fixes two real bugs with correct, minimal, well-tested changes that have already been validated in the MUI X scheduler. The only actionable suggestion is to fix the remaining comment typo in parse() while in the neighborhood.

@flaviendelangle
Copy link
Copy Markdown
Member

Minor: The isDateOnly heuristic relies on value being a string, which is guaranteed by the method signature (value: T extends string | null), so no issue. However, if someone passed a numeric timestamp coerced to string (e.g. "1680739200000"), it has no T, would be treated as date-only, and UTC getters would be used. This is an extremely unlikely edge case and the adapter documents ISO string input, so this is acceptable.

I think we can disreguard that one, because adapter.date is never used in public API except for really advance use cases where we can expect people to know what they are doing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

internal Behind-the-scenes enhancement. Formerly called “core”.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants