This work was implemented by AI tools (Claude Opus 4.5 and GPT-5.2-Codex) under my orchestration.
View Interactive Storybook & Matrix Test Suite Comprehensive playground demonstrating:
- Integration with Material Datepicker/Timepicker
- 10+ Calendar systems (Japanese, Hebrew, Chinese, etc.)
- Automated Matrix Tests verifying all adapter operations
Add a Temporal-based DateAdapter for Angular Material datepicker, supporting date-only, date+time, and timezone-aware date+time use cases.
Fixes #25753
Temporal is a Stage 3 TC39 proposal providing immutable date/time primitives with explicit calendar and timezone semantics. This adapter enables Material datepicker to work with Temporal (native or polyfilled) without relying on JS Date as the internal model. The implementation follows patterns from existing adapters in this repo, especially NativeDateAdapter.
- Unified adapter:
TemporalDateAdapter(The primary 3-in-1 adapter recommended for general use). Supports switching modes via configuration:date→Temporal.PlainDatedatetime→Temporal.PlainDateTimezoned→Temporal.ZonedDateTime
- Demonstration / Split adapters: A set of 4 additional adapters illustrating different architectural splittings and strict type handling. These serve as architectural examples:
PlainDateAdapter: Strictly forTemporal.PlainDate.PlainDateTimeAdapter: Strictly forTemporal.PlainDateTime.ZonedDateTimeAdapter: Strictly forTemporal.ZonedDateTime.PlainTemporalAdapter: A hybrid handling bothPlainDateandPlainDateTime.
- Formatting uses
Intl.DateTimeFormatOptionswith Temporal’s locale formatting. - No runtime dependency is added; consumers supply a Temporal implementation (native or polyfill).
Defaults
calendar:iso8601outputCalendar: same ascalendarmode:dateoverflow:rejecttimezone: system timezone (only whenmode: 'zoned')disambiguation,offset,rounding: Temporal defaults (only whenmode: 'zoned')
Options
calendar: calendar system to use for calculations.outputCalendar: calendar system to use for output/formatting when different from calculations (MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/withCalendar, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/withCalendar).mode:date | datetime | zoned.timezone(zoned only): IANA ID likeEurope/WarsaworUTC.disambiguation(zoned only):'compatible' | 'earlier' | 'later' | 'reject'for DST gaps/overlaps (MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/toZonedDateTime, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/from).offset(zoned only):'use' | 'ignore' | 'reject' | 'prefer'for offset ambiguity on parse (MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/from).rounding(zoned only):{smallestUnit, roundingIncrement?, roundingMode?}applied to zoned output (MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/round).firstDayOfWeek: overrides the locale-derived week start.overflow:rejectthrows on invalid dates,constrainclamps to the nearest valid date.
Notes
outputCalendaruseswithCalendarfor display fields only; it does not re-resolve the instant, sodisambiguation/offsetare not applied during output conversion.- If
disambiguation,offset, orroundingaren’t provided, the adapter relies on Temporal’s defaults. overflowdefaults torejectto avoid silent clamping and better match strict validation expectations.
PlainTemporalAdapterOptions (for PlainTemporalAdapter)
mode(default:datetime):date→PlainDate,datetime→PlainDateTime.calendar(default:iso8601): calendar system for calculations.outputCalendar(default: same ascalendar): calendar system for output/formatting.firstDayOfWeek(default: locale-derived): overrides the locale-derived week start.overflow(default:reject):rejectthrows on invalid dates,constrainclamps.
ZonedDateTimeAdapterOptions (for ZonedDateTimeAdapter)
calendar(default:iso8601): calendar system for calculations.outputCalendar(default: same ascalendar): calendar system for output/formatting (MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/withCalendar).timezone(default: system timezone): IANA ID likeEurope/WarsaworUTC.disambiguation:'compatible' | 'earlier' | 'later' | 'reject'for DST gaps/overlaps (MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime/toZonedDateTime, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/from).offset:'use' | 'ignore' | 'reject' | 'prefer'for offset ambiguity on parse (MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/from).rounding:{smallestUnit, roundingIncrement?, roundingMode?}applied to zoned output (MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/round).firstDayOfWeek(default: locale-derived): overrides the locale-derived week start.overflow(default:reject):rejectthrows on invalid dates,constrainclamps.
calendar→ display and calculations use the Japanese calendar.provideTemporalDateAdapter({mode: 'date', calendar: 'japanese'}); const value = Temporal.PlainDate.from('2024-01-15').withCalendar('japanese'); // Output example: adapter.format(value, {year: 'numeric'}) -> (Japanese era year, locale-dependent)
outputCalendar→ calculations use ISO, output uses the Japanese calendar.provideTemporalDateAdapter({mode: 'date', calendar: 'iso8601', outputCalendar: 'japanese'}); const value = Temporal.PlainDate.from('2024-01-15'); // Output example: adapter.format(value, {year: 'numeric'}) -> (Japanese era year, locale-dependent)
mode→ values areprovideTemporalDateAdapter({mode: 'datetime'}); const value = Temporal.PlainDateTime.from('2024-01-15T12:30'); // Output examples: // mode: 'date' -> adapter.toIso8601(value) -> 2024-01-15 // mode: 'datetime'-> adapter.toIso8601(value) -> 2024-01-15 // mode: 'zoned' -> adapter.toIso8601(value) -> 2024-01-15T12:30:00+00:00[UTC]
Temporal.PlainDateTime.timezone→ zoned values in UTC.provideTemporalDateAdapter({mode: 'zoned', timezone: 'UTC'}); const value = Temporal.ZonedDateTime.from('2024-01-15T12:30[UTC]'); // Output example: adapter.toIso8601(value) -> 2024-01-15T12:30:00+00:00[UTC]
disambiguation→ ambiguous times choose the later instant.provideTemporalDateAdapter({mode: 'zoned', timezone: 'America/New_York', disambiguation: 'later'}); const value = Temporal.PlainDateTime.from('2024-11-03T01:05').toZonedDateTime('America/New_York', { disambiguation: 'later', }); // Output examples (same local time): // compatible -> adapter.toIso8601(value) -> 2024-11-03T01:05:00-04:00[America/New_York] // earlier -> adapter.toIso8601(value) -> 2024-11-03T01:05:00-04:00[America/New_York] // later -> adapter.toIso8601(value) -> 2024-11-03T01:05:00-05:00[America/New_York] // reject -> adapter.setTime(...) throws for ambiguous/nonexistent time
offset→ keep local time when offset conflicts with zone on parse.provideTemporalDateAdapter({mode: 'zoned', offset: 'ignore'}); const value = Temporal.ZonedDateTime.from('2019-12-23T12:00:00-02:00[America/Sao_Paulo]', { offset: 'ignore', }); // Output examples (same input string): // use -> adapter.toIso8601(value) -> 2019-12-23T11:00:00-03:00[America/Sao_Paulo] // ignore -> adapter.toIso8601(value) -> 2019-12-23T12:00:00-03:00[America/Sao_Paulo] // prefer -> adapter.toIso8601(value) -> 2019-12-23T12:00:00-03:00[America/Sao_Paulo] // reject -> adapter.deserialize(...) -> invalid
rounding→ outputs rounded to 5-minute steps.provideTemporalDateAdapter({mode: 'zoned', rounding: {smallestUnit: 'minute', roundingIncrement: 5}}); const value = Temporal.ZonedDateTime.from('2024-01-15T12:34:56[UTC]'); // Output examples (roundingMode): // halfExpand -> adapter.toIso8601(value) -> 2024-01-15T12:35:00+00:00[UTC] // floor -> adapter.toIso8601(value) -> 2024-01-15T12:30:00+00:00[UTC] // ceil -> adapter.toIso8601(value) -> 2024-01-15T12:35:00+00:00[UTC] // trunc -> adapter.toIso8601(value) -> 2024-01-15T12:30:00+00:00[UTC]
firstDayOfWeek→ weeks start on Monday.provideTemporalDateAdapter({firstDayOfWeek: 1}); // Output example: adapter.getFirstDayOfWeek() -> 1
overflow→ invalid dates throw; invalid dates clamp.provideTemporalDateAdapter({overflow: 'reject'}); // Output example (reject): adapter.createDate(2024, 1, 31) -> throws provideTemporalDateAdapter({overflow: 'constrain'}); // Output example (constrain): adapter.createDate(2024, 1, 31) -> 2024-02-29
Form control example (overflow)
provideTemporalDateAdapter({mode: 'date', overflow: 'constrain'});
const control = new FormControl(Temporal.PlainDate.from({year: 2024, month: 2, day: 31}));Effect: the value is clamped to the last valid day of the month (e.g. 2024-02-29) instead of throwing.
providePlainTemporalAdapter({mode: 'date', calendar: 'iso8601', outputCalendar: 'japanese'});
provideZonedDateTimeAdapter({timezone: 'UTC', disambiguation: 'reject', rounding: {smallestUnit: 'minute'}});import {provideTemporalDateAdapter} from '@angular/material-temporal-adapter';
provideTemporalDateAdapter({mode: 'date'});
provideTemporalDateAdapter({mode: 'datetime'});
provideTemporalDateAdapter({mode: 'zoned'}); // default: system timezone
provideTemporalDateAdapter({mode: 'zoned', timezone: 'UTC'});import {
providePlainTemporalAdapter,
provideZonedDateTimeAdapter,
} from '@angular/material-temporal-adapter/adapter/split';
providePlainTemporalAdapter({mode: 'date'});
providePlainTemporalAdapter({mode: 'datetime'});
provideZonedDateTimeAdapter({timezone: 'Europe/Warsaw'});- From
NativeDateAdapter(Date): switch tomode: 'date', replaceDatewithTemporal.PlainDate. If you rely on timezone/time-of-day behavior, usemode: 'zoned'with the system timezone instead. - From
MomentDateAdapter/LuxonDateAdapter(date+time): usemode: 'datetime', replace values withTemporal.PlainDateTime. - From timezone-aware usage: use
mode: 'zoned'+timezone, replace values withTemporal.ZonedDateTime. - From Luxon
defaultOutputCalendar: keepcalendarfor calculations, setoutputCalendarfor display. - From apps relying on DST/offset rules or rounding: configure
disambiguation,offset, androundinginzonedmode. - Incremental/explicit type choice: use split adapters (
PlainTemporalAdapterorZonedDateTimeAdapter). - If a Temporal polyfill is needed, load it before bootstrapping the app.
- If you provide custom
MAT_DATE_FORMATS, ensure they areIntl.DateTimeFormatOptions(Temporal ignores parse formats and parses ISO strings only).
Custom display formats
import {MatDateFormats} from '@angular/material/core';
import {provideTemporalDateAdapter, MAT_TEMPORAL_DATE_FORMATS} from '@angular/material-temporal-adapter';
const MY_TEMPORAL_FORMATS = {
...MAT_TEMPORAL_DATE_FORMATS,
display: {
...MAT_TEMPORAL_DATE_FORMATS.display,
dateInput: {year: 'numeric', month: 'long', day: 'numeric'},
},
} satisfies MatDateFormats;
bootstrapApplication(AppComponent, {
providers: [provideTemporalDateAdapter(MY_TEMPORAL_FORMATS, {mode: 'date'})],
});MAT token notes
MAT_DATE_LOCALE: locale string used for formatting and week info.MAT_DATE_FORMATS: must beIntl.DateTimeFormatOptions(parse formats ignored).
- Follows the same
DateAdaptercontract asNativeDateAdapter,MomentDateAdapter,LuxonDateAdapter, andDateFnsAdapter. - Uses
Intl.DateTimeFormatOptionsfor display formatting (likeNativeDateAdapter), but the backing values are Temporal types instead ofDate.
Option mapping (approximate)
| Existing adapter | Option | Temporal equivalent | Notes |
|---|---|---|---|
| NativeDateAdapter | n/a | mode: 'date' |
Temporal PlainDate is the closest analogue to Date for date-only use. |
| MomentDateAdapter | useUtc: true |
mode: 'zoned', timezone: 'UTC' |
Use zoned+UTC for UTC-centric workflows. |
| MomentDateAdapter | strict: true |
overflow: 'reject' |
Temporal parsing is ISO-only; overflow controls invalid date creation. |
| LuxonDateAdapter | useUtc: true |
mode: 'zoned', timezone: 'UTC' |
Similar UTC behavior. |
| LuxonDateAdapter | firstDayOfWeek |
firstDayOfWeek |
Same semantics. |
| LuxonDateAdapter | defaultOutputCalendar |
outputCalendar |
Calendar used for output/formatting (calculations use calendar). |
| DateFnsAdapter | n/a | MAT_DATE_LOCALE + MAT_DATE_FORMATS |
Temporal adapter also uses locale + formats. |
TypeScript does not currently ship lib.esnext.temporal in the version used here, so this PR includes local declarations to type the adapter until the repo upgrades to a TS version that includes Temporal libs.
Reference: microsoft/TypeScript#62628
API Extractor had trouble resolving the global Temporal namespace in this setup. Additionally, existing adapter packages (moment/luxon/date-fns) don’t have API goldens. For consistency and to avoid introducing a blocked/flaky step, this PR does not add API golden coverage for this adapter package.
Intl.DateTimeFormat is used for localization (same overall approach as other adapters). Zoned values are formatted using the configured timezone.
- Unit tests cover all
modevariants and timezone behavior, plus creation/parsing/formatting/time operations. - Latest run:
pnpm test src/material-temporal-adapter --no-watchon Chromium (local).- Result: 141 passed, 2 skipped (Islamic calendar suite skipped because
islamiccalendar is not supported by the current Temporal implementation).
- Result: 141 passed, 2 skipped (Islamic calendar suite skipped because
- Note: split adapters (
PlainTemporalAdapter,ZonedDateTimeAdapter) are not explicitly covered by a dedicated spec yet.
- Calendar type: keep
calendarasstring(max flexibility) vs trying to constrain it (risk of being incomplete/locale-dependent). - Positioning: should this adapter be documented as “ready” like other adapters, or called out as “early” given current ecosystem support for Temporal?
- Should
modebe required for the unified adapter (no implicit default), or keep the defaultdatemode? - Should
overflowdefault toreject(strict) or align with Temporal’sconstraindefault? - Should we add migration examples to the public docs (datepicker/timepicker) instead of only in the PR description?