Skip to content

Commit d394e22

Browse files
committed
feat(material-temporal-adapter): add Temporal API date adapter for Angular Material
Adds a new @angular/material-temporal-adapter package that provides DateAdapter implementations using the JavaScript Temporal API (Stage 3 proposal). Features: Unified Adapter (TemporalDateAdapter): - Configurable mode: date (PlainDate), datetime (PlainDateTime), zoned (ZonedDateTime) - Full calendar support: iso8601, hebrew, islamic, japanese, chinese, persian, buddhist - Separate input/output calendar configuration - Overflow handling: reject (strict) or constrain (lenient) - Timezone support with DST disambiguation options - Rounding configuration for time precision Split Adapters: - PlainTemporalAdapter: Configurable date/datetime mode - ZonedDateTimeAdapter: Full timezone-aware datetime - PlainDateAdapter: Date-only (internal) - PlainDateTimeAdapter: DateTime without timezone (internal) Technical Implementation: - Immutable Temporal objects with proper clone() semantics - 0-indexed month interface (matching Angular Material DateAdapter contract) - Sentinel objects for invalid date representation - Duck-typing type guards for Temporal type detection - ngDevMode-guarded development warnings Security and Robustness: - Epoch millisecond validation (range check, NaN/Infinity checks) - parseTime input length validation (DoS prevention) - setTime parameter validation (finite numbers, valid ranges) - Try/catch wrappers on Temporal API calls Test Coverage: - 279 unit tests covering all adapters - Edge case tests for security validations - Calendar support tests (Hebrew 13-month, Islamic) - DST transition and timezone tests - Overflow constrain/reject behavior tests
1 parent 71cdb84 commit d394e22

31 files changed

+8112
-16
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ node_modules
1919
/packages
2020
/pubspec.lock
2121
/src/.pub
22+
PR_DESCRIPTION.md
2223
/src/.packages
2324
/src/packages
2425
/src/pubspec.lock

.ng-dev/commit-message.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export const commitMessage: CommitMessageConfig = {
9595
'material-moment-adapter',
9696
'material-date-fns-adapter',
9797
'material-luxon-adapter',
98+
'material-temporal-adapter',
9899
'youtube-player',
99100
'material-angular-io',
100101
],

.vscode/mcp.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"servers": {
3+
"io.github.ChromeDevTools/chrome-devtools-mcp": {
4+
"command": "npx",
5+
"args": ["-y", "chrome-devtools-mcp"],
6+
"env": {},
7+
"type": "stdio"
8+
}
9+
},
10+
"inputs": []
11+
}

goldens/BUILD.bazel

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,8 @@ api_golden_test_npm_package(
4848
golden_dir = "goldens/aria",
4949
npm_package = "src/aria/npm_package",
5050
)
51+
52+
# Note: material-temporal-adapter API golden tests are not included because
53+
# api-extractor cannot resolve the global Temporal namespace (similar to how
54+
# material-moment-adapter, material-luxon-adapter, and material-date-fns-adapter
55+
# also don't have API golden tests).

packages.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ ANGULAR_COMPONENTS_SCOPED_PACKAGES = ["@angular/%s" % p for p in [
88
"material-luxon-adapter",
99
"material-moment-adapter",
1010
"material-date-fns-adapter",
11+
"material-temporal-adapter",
1112
]]
1213

1314
PKG_GROUP_REPLACEMENTS = {

pnpm-lock.yaml

Lines changed: 41 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
load("//tools:defaults.bzl", "ng_package", "ng_project", "ng_web_test_suite")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
ng_project(
6+
name = "material-temporal-adapter",
7+
srcs = glob(
8+
["**/*.ts"],
9+
exclude = ["**/*.spec.ts"],
10+
),
11+
deps = [
12+
"//:node_modules/@angular/core",
13+
"//src:dev_mode_types",
14+
"//src/material/core",
15+
],
16+
)
17+
18+
ng_project(
19+
name = "unit_test_sources",
20+
testonly = True,
21+
srcs = glob(
22+
["**/*.spec.ts"],
23+
exclude = ["**/*.e2e.spec.ts"],
24+
),
25+
deps = [
26+
":material-temporal-adapter",
27+
"//:node_modules/@angular/core",
28+
"//src/material/core",
29+
],
30+
)
31+
32+
ng_web_test_suite(
33+
name = "unit_tests",
34+
deps = [
35+
":unit_test_sources",
36+
],
37+
)
38+
39+
ng_package(
40+
name = "npm_package",
41+
package_name = "@angular/material-temporal-adapter",
42+
srcs = ["package.json"],
43+
nested_packages = ["//src/material-temporal-adapter/schematics:npm_package"],
44+
tags = ["release-package"],
45+
visibility = [
46+
"//:__pkg__",
47+
"//goldens:__pkg__",
48+
"//integration:__subpackages__",
49+
],
50+
deps = [":material-temporal-adapter"],
51+
)
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {NgModule, Provider} from '@angular/core';
10+
import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core';
11+
import {
12+
MAT_TEMPORAL_DATE_ADAPTER_OPTIONS,
13+
MatTemporalDateAdapterOptions,
14+
TemporalDateAdapter,
15+
} from './temporal-date-adapter';
16+
import {MAT_TEMPORAL_DATE_FORMATS} from './temporal-date-formats';
17+
18+
export * from './temporal-date-adapter';
19+
export * from './temporal-date-formats';
20+
21+
/**
22+
* Module providing the Temporal date adapter.
23+
* @deprecated Use `provideTemporalDateAdapter` instead.
24+
*/
25+
@NgModule({
26+
providers: [
27+
{
28+
provide: DateAdapter,
29+
useClass: TemporalDateAdapter,
30+
},
31+
],
32+
})
33+
export class TemporalModule {}
34+
35+
/**
36+
* Module providing the Temporal date adapter with default formats.
37+
*
38+
* @example
39+
* ```typescript
40+
* import { MatTemporalModule } from '@angular/material-temporal-adapter';
41+
*
42+
* @NgModule({
43+
* imports: [MatTemporalModule],
44+
* })
45+
* export class AppModule {}
46+
* ```
47+
*/
48+
@NgModule({
49+
providers: [provideTemporalDateAdapter()],
50+
})
51+
export class MatTemporalModule {}
52+
53+
/**
54+
* Provider function for the Temporal date adapter.
55+
*
56+
* @param formats Custom date formats to use. Defaults to MAT_TEMPORAL_DATE_FORMATS.
57+
* @param options Configuration options for the adapter.
58+
* @returns Array of providers for the Temporal date adapter.
59+
*
60+
* @example
61+
* ```typescript
62+
* import { provideTemporalDateAdapter } from '@angular/material-temporal-adapter';
63+
*
64+
* bootstrapApplication(AppComponent, {
65+
* providers: [
66+
* provideTemporalDateAdapter(),
67+
* ],
68+
* });
69+
* ```
70+
*
71+
* @example
72+
* With custom options:
73+
* ```typescript
74+
* import {
75+
* provideTemporalDateAdapter,
76+
* MAT_TEMPORAL_DATETIME_FORMATS
77+
* } from '@angular/material-temporal-adapter';
78+
*
79+
* bootstrapApplication(AppComponent, {
80+
* providers: [
81+
* provideTemporalDateAdapter(MAT_TEMPORAL_DATETIME_FORMATS, {
82+
* calendar: 'islamic',
83+
* mode: 'datetime',
84+
* }),
85+
* ],
86+
* });
87+
* ```
88+
*/
89+
export function provideTemporalDateAdapter(
90+
formats: MatDateFormats = MAT_TEMPORAL_DATE_FORMATS,
91+
options?: Partial<MatTemporalDateAdapterOptions>,
92+
): Provider[] {
93+
const providers: Provider[] = [
94+
{
95+
provide: DateAdapter,
96+
useClass: TemporalDateAdapter,
97+
},
98+
{provide: MAT_DATE_FORMATS, useValue: formats},
99+
];
100+
101+
if (options) {
102+
const zonedOptions = options.mode === 'zoned' ? options : null;
103+
providers.push({
104+
provide: MAT_TEMPORAL_DATE_ADAPTER_OPTIONS,
105+
useValue: {
106+
calendar: options.calendar ?? 'iso8601',
107+
outputCalendar: options.outputCalendar,
108+
mode: options.mode ?? 'date',
109+
firstDayOfWeek: options.firstDayOfWeek,
110+
overflow: options.overflow,
111+
...(zonedOptions
112+
? {
113+
timezone: zonedOptions.timezone,
114+
disambiguation: zonedOptions.disambiguation,
115+
offset: zonedOptions.offset,
116+
rounding: zonedOptions.rounding,
117+
}
118+
: {}),
119+
},
120+
});
121+
}
122+
123+
return providers;
124+
}

0 commit comments

Comments
 (0)