Skip to content

Commit 2646e08

Browse files
committedOct 4, 2024
feat(material/timepicker): add timepicker component
Addresses a long-time feature request by adding a component that allows users to select a time. The new component uses a combination of an `input` and a dropdown to allow users to either type a time or select it from a pre-defined list. Example usage: ```html <mat-form-field> <mat-label>Pick a time</mat-label> <input matInput [matTimepicker]="picker"/> <mat-timepicker #picker/> <mat-timepicker-toggle [for]="picker"/> </mat-form-field> ``` Features of the new component include: * Automatically parses the typed-in value to a date object using the current `DateAdapter`. Existing date adapters have been updated to add support for parsing times. * Time values can be generated either using the `interval` input (e.g. `interval="45min"`) or provided directly through the `options` input. * Integrated into `@angular/forms` by providing itself as a `ControlValueAccessor` and `Validator`. * Offers built-in validation for minimum, maximum and time formatting. * Offers keyboard navigation support. * Accessibility implemented using the combobox + listbox pattern. * Can be used either with `mat-form-field` or on its own. * Can be combined with `mat-datepicker` (docs to follow, see the dev app for now). * Includes test harnesses for all directives. * Works with Material's theming system. * Can be configured globally through an injection token. * Can be used either as an `NgModule` or by importing the standalone directives. One of the main reasons why we hadn't provided a timepicker component until now was that there's no universally-accepted design for what a timepicker should look like. Material Design has had a [specification for a timepicker](https://m3.material.io/components/time-pickers/overview) for years, but we didn't want to implement it because: 1. This design is primarily geared towards mobile users on Android. It would look out of place in the desktop-focused enterprise UIs that a lot of Angular developers build. 2. The time dial UI is complicated and can be overwhelming, especially in the 24h variant. 3. The accessibility pattern is unclear, users may have to fall back to using the inputs. 4. It's unclear how the time selection would work on non-Westernized locales whose time formatting isn't some variation of `HH:MM`. 5. The time dial requires very precise movements if the user wants to select a specific time between others (e.g. 6:52). This can be unusable for users with some disabilities. 6. The non-dial functionality (inputs in a dropdown) don't add much to the user experience. There are [community implementations](https://dhutaryan.github.io/ngx-mat-timepicker) of the dial design that you can install if you want it for your app. Some libraries like [Kendo UI](https://www.telerik.com/kendo-angular-ui/components/dateinputs/timepicker), [Ignite UI](https://www.infragistics.com/products/ignite-ui-angular/angular/components/time-picker) or [MUI](https://mui.com/x/react-date-pickers/time-picker/), as well as Chrome's implementation of `<input type="time"/>` appear to have settled on a multi-column design for the dropdown. We didn't want to do something similar because: 1. The selected state is only shown using one sensory characteristic (color) which is problematic for accessibility. While we could either add a second one (e.g. a checkbox) or adjust the design somehow, we felt that this would make it look sub-optimal. 2. The UI only looks good on smaller sizes and when each column has roughly the same amount of text. Changing either for a particular column can throw off the whole UI's appearance. 3. It requires the user to tab through several controls within the dialog. 4. It's unclear how the time selection would work on non-Westernized locales whose time formatting isn't some variation of `HH:MM`. 5. Each column requires a lot of filler whitespace in order to be able to align the selected states to each other which can look off on some selections. We chose the current design, because: 1. Users are familiar with it, e.g. Google Calendar uses something similar for their time selection. 2. It reuses the design from existing Material Design components. 3. It uses an established accessibility pattern (combobox + listbox) and it doesn't have the same concerns as the multi-column design around indicating the selected state. 4. It allows us to support a wide range of locales. 5. It's compact, allowing us to do some sort of unified control with `mat-datepicker` in the future. Fixes #5648.
1 parent a8c41b9 commit 2646e08

20 files changed

+2776
-38
lines changed
 

‎src/dev-app/timepicker/BUILD.bazel

+5
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ ng_module(
1212
deps = [
1313
"//src/material/button",
1414
"//src/material/card",
15+
"//src/material/core",
16+
"//src/material/datepicker",
17+
"//src/material/form-field",
1518
"//src/material/icon",
19+
"//src/material/input",
20+
"//src/material/select",
1621
"//src/material/timepicker",
1722
],
1823
)
+99-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,99 @@
1-
<mat-timepicker/>
1+
<div class="demo-row">
2+
<div>
3+
<div>
4+
<h2>Basic timepicker</h2>
5+
<mat-form-field>
6+
<mat-label>Pick a time</mat-label>
7+
<input
8+
matInput
9+
[matTimepicker]="basicPicker"
10+
[matTimepickerMin]="minControl.value"
11+
[matTimepickerMax]="maxControl.value"
12+
[formControl]="control">
13+
<mat-timepicker [interval]="intervalControl.value" #basicPicker/>
14+
<mat-timepicker-toggle [for]="basicPicker" matSuffix/>
15+
</mat-form-field>
16+
17+
<p>Value: {{control.value}}</p>
18+
<p>Dirty: {{control.dirty}}</p>
19+
<p>Touched: {{control.touched}}</p>
20+
<p>Errors: {{control.errors | json}}</p>
21+
<button mat-button (click)="randomizeValue()">Assign a random value</button>
22+
</div>
23+
24+
<div>
25+
<h2>Timepicker and datepicker</h2>
26+
<mat-form-field>
27+
<mat-label>Pick a date</mat-label>
28+
<input
29+
matInput
30+
[matDatepicker]="combinedDatepicker"
31+
[(ngModel)]="combinedValue">
32+
<mat-datepicker #combinedDatepicker/>
33+
<mat-datepicker-toggle [for]="combinedDatepicker" matSuffix/>
34+
</mat-form-field>
35+
36+
<div>
37+
<mat-form-field>
38+
<mat-label>Pick a time</mat-label>
39+
<input
40+
matInput
41+
[matTimepicker]="combinedTimepicker"
42+
[matTimepickerMin]="minControl.value"
43+
[matTimepickerMax]="maxControl.value"
44+
[(ngModel)]="combinedValue"
45+
[ngModelOptions]="{updateOn: 'blur'}">
46+
<mat-timepicker [interval]="intervalControl.value" #combinedTimepicker/>
47+
<mat-timepicker-toggle [for]="combinedTimepicker" matSuffix/>
48+
</mat-form-field>
49+
</div>
50+
51+
<p>Value: {{combinedValue}}</p>
52+
</div>
53+
54+
<div>
55+
<h2>Timepicker without form field</h2>
56+
<input [matTimepicker]="nonFormFieldPicker">
57+
<mat-timepicker aria-label="Standalone timepicker" #nonFormFieldPicker/>
58+
</div>
59+
</div>
60+
61+
<mat-card appearance="outlined" class="demo-card">
62+
<mat-card-header>
63+
<mat-card-title>State</mat-card-title>
64+
</mat-card-header>
65+
66+
<mat-card-content>
67+
<div class="demo-form-fields">
68+
<mat-form-field>
69+
<mat-label>Locale</mat-label>
70+
<mat-select [formControl]="localeControl">
71+
@for (locale of locales; track $index) {
72+
<mat-option [value]="locale">{{locale}}</mat-option>
73+
}
74+
</mat-select>
75+
</mat-form-field>
76+
77+
<mat-form-field>
78+
<mat-label>Interval</mat-label>
79+
<input matInput [formControl]="intervalControl"/>
80+
</mat-form-field>
81+
82+
<mat-form-field>
83+
<mat-label>Min time</mat-label>
84+
<input matInput [matTimepicker]="minPicker" [formControl]="minControl">
85+
<mat-timepicker #minPicker/>
86+
<mat-timepicker-toggle [for]="minPicker" matSuffix/>
87+
</mat-form-field>
88+
89+
<mat-form-field>
90+
<mat-label>Max time</mat-label>
91+
<input matInput [matTimepicker]="maxPicker" [formControl]="maxControl">
92+
<mat-timepicker #maxPicker/>
93+
<mat-timepicker-toggle [for]="maxPicker" matSuffix/>
94+
</mat-form-field>
95+
</div>
96+
</mat-card-content>
97+
</mat-card>
98+
</div>
99+
+22-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,22 @@
1-
// TODO
1+
.demo-row {
2+
display: flex;
3+
align-items: flex-start;
4+
gap: 100px;
5+
}
6+
7+
.demo-card {
8+
width: 600px;
9+
max-width: 100%;
10+
flex-shrink: 0;
11+
}
12+
13+
.demo-form-fields {
14+
display: flex;
15+
flex-wrap: wrap;
16+
gap: 0 2%;
17+
margin-top: 16px;
18+
19+
mat-form-field {
20+
flex-basis: 49%;
21+
}
22+
}

‎src/dev-app/timepicker/timepicker-demo.ts

+57-4
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,68 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {ChangeDetectionStrategy, Component} from '@angular/core';
10-
import {MatTimepicker} from '@angular/material/timepicker';
9+
import {ChangeDetectionStrategy, Component, inject, OnDestroy} from '@angular/core';
10+
import {DateAdapter} from '@angular/material/core';
11+
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
12+
import {MatTimepickerModule} from '@angular/material/timepicker';
13+
import {MatFormFieldModule} from '@angular/material/form-field';
14+
import {MatInputModule} from '@angular/material/input';
15+
import {JsonPipe} from '@angular/common';
16+
import {MatButtonModule} from '@angular/material/button';
17+
import {MatSelectModule} from '@angular/material/select';
18+
import {Subscription} from 'rxjs';
19+
import {MatCardModule} from '@angular/material/card';
20+
import {MatDatepickerModule} from '@angular/material/datepicker';
1121

1222
@Component({
1323
selector: 'timepicker-demo',
1424
templateUrl: 'timepicker-demo.html',
1525
styleUrl: 'timepicker-demo.css',
1626
standalone: true,
1727
changeDetection: ChangeDetectionStrategy.OnPush,
18-
imports: [MatTimepicker],
28+
imports: [
29+
MatTimepickerModule,
30+
MatDatepickerModule,
31+
MatFormFieldModule,
32+
MatInputModule,
33+
ReactiveFormsModule,
34+
FormsModule,
35+
JsonPipe,
36+
MatButtonModule,
37+
MatSelectModule,
38+
MatCardModule,
39+
],
1940
})
20-
export class TimepickerDemo {}
41+
export class TimepickerDemo implements OnDestroy {
42+
private _dateAdapter = inject(DateAdapter);
43+
private _localeSubscription: Subscription;
44+
locales = ['en-US', 'da-DK', 'bg-BG', 'zh-TW'];
45+
control: FormControl<Date | null>;
46+
localeControl = new FormControl('en-US', {nonNullable: true});
47+
intervalControl = new FormControl('1h', {nonNullable: true});
48+
minControl = new FormControl<Date | null>(null);
49+
maxControl = new FormControl<Date | null>(null);
50+
combinedValue: Date | null = null;
51+
52+
constructor() {
53+
const value = new Date();
54+
value.setHours(15, 0, 0);
55+
this.control = new FormControl(value);
56+
57+
this._localeSubscription = this.localeControl.valueChanges.subscribe(locale => {
58+
if (locale) {
59+
this._dateAdapter.setLocale(locale);
60+
}
61+
});
62+
}
63+
64+
randomizeValue() {
65+
const value = new Date();
66+
value.setHours(Math.floor(Math.random() * 23), Math.floor(Math.random() * 59), 0);
67+
this.control.setValue(value);
68+
}
69+
70+
ngOnDestroy(): void {
71+
this._localeSubscription.unsubscribe();
72+
}
73+
}

‎src/material/core/tokens/_density.scss

+1-2
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ $_density-tokens: (
138138
(mat, slider): (),
139139
(mat, snack-bar): (),
140140
(mat, sort): (),
141+
(mat, timepicker): (),
141142
(mat, standard-button-toggle): (
142143
height: (40px, 40px, 40px, 36px, 24px),
143144
),
@@ -157,8 +158,6 @@ $_density-tokens: (
157158
(mat, tree): (
158159
node-min-height: (48px, 44px, 40px, 36px, 28px),
159160
),
160-
// TODO: timepicker
161-
(mat, timepicker): (),
162161
);
163162

164163
/// Gets the value for the given density scale from the given set of density values.

‎src/material/core/tokens/m2/mat/_timepicker.scss

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,30 @@
11
@use '../../token-definition';
2+
@use '../../../theming/inspection';
23
@use '../../../style/sass-utils';
4+
@use '../../../style/elevation';
35

46
// The prefix used to generate the fully qualified name for tokens in this file.
57
$prefix: (mat, timepicker);
68

79
// Tokens that can't be configured through Angular Material's current theming API,
810
// but may be in a future version of the theming API.
911
@function get-unthemable-tokens() {
10-
@return ();
12+
@return (
13+
container-shape: 4px,
14+
container-elevation-shadow: elevation.get-box-shadow(8),
15+
);
1116
}
1217

1318
// Tokens that can be configured through Angular Material's color theming API.
1419
@function get-color-tokens($theme) {
1520
@return (
16-
enabled-trigger-text-color: hotpink,
21+
container-background-color: inspection.get-theme-color($theme, background, card)
1722
);
1823
}
1924

2025
// Tokens that can be configured through Angular Material's typography theming API.
2126
@function get-typography-tokens($theme) {
22-
@return (
23-
trigger-text-font: fantasy,
24-
);
27+
@return ();
2528
}
2629

2730
// Tokens that can be configured through Angular Material's density theming API.

‎src/material/core/tokens/m3/mat/_timepicker.scss

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@use 'sass:map';
2+
@use '../../../style/elevation';
13
@use '../../token-definition';
24

35
// The prefix used to generate the fully qualified name for tokens in this file.
@@ -10,8 +12,10 @@ $prefix: (mat, timepicker);
1012
/// @return {Map} A set of custom tokens for the mat-timepicker
1113
@function get-tokens($systems, $exclude-hardcoded, $token-slots) {
1214
$tokens: (
13-
enabled-trigger-text-color: hotpink,
14-
trigger-text-font: fantasy,
15+
container-background-color: map.get($systems, md-sys-color, surface-container),
16+
container-shape: map.get($systems, md-sys-shape, corner-extra-small),
17+
container-elevation-shadow:
18+
token-definition.hardcode(elevation.get-box-shadow(2), $exclude-hardcoded),
1519
);
1620

1721
@return token-definition.namespace-tokens($prefix, $tokens, $token-slots);

‎src/material/timepicker/BUILD.bazel

+14-2
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,16 @@ ng_module(
2020
deps = [
2121
"//src:dev_mode_types",
2222
"//src/cdk/bidi",
23-
"//src/cdk/coercion",
23+
"//src/cdk/keycodes",
24+
"//src/cdk/overlay",
25+
"//src/cdk/platform",
26+
"//src/cdk/portal",
27+
"//src/cdk/scrolling",
28+
"//src/material/button",
2429
"//src/material/core",
30+
"//src/material/input",
2531
"@npm//@angular/core",
32+
"@npm//@angular/forms",
2633
],
2734
)
2835

@@ -45,7 +52,12 @@ ng_test_library(
4552
),
4653
deps = [
4754
":timepicker",
48-
"//src/cdk/bidi",
55+
"//src/cdk/keycodes",
56+
"//src/cdk/testing/private",
57+
"//src/material/core",
58+
"//src/material/form-field",
59+
"//src/material/input",
60+
"@npm//@angular/forms",
4961
"@npm//@angular/platform-browser",
5062
],
5163
)

‎src/material/timepicker/_timepicker-theme.scss

+22-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
@use '../core/tokens/m2/mat/timepicker' as tokens-mat-timepicker;
88
@use '../core/tokens/token-utils';
99

10+
/// Outputs base theme styles (styles not dependent on the color, typography, or density settings)
11+
/// for the mat-timepicker.
12+
/// @param {Map} $theme The theme to generate base styles for.
1013
@mixin base($theme) {
1114
@if inspection.get-theme-version($theme) == 1 {
1215
@include _theme-from-tokens(inspection.get-theme-tokens($theme, base));
@@ -19,7 +22,12 @@
1922
}
2023
}
2124

22-
@mixin color($theme) {
25+
/// Outputs color theme styles for the mat-timepicker.
26+
/// @param {Map} $theme The theme to generate color styles for.
27+
/// @param {ArgList} Additional optional arguments (only supported for M3 themes):
28+
/// $color-variant: The color variant to use for the main selection: primary, secondary, tertiary,
29+
/// or error (If not specified, default primary color will be used).
30+
@mixin color($theme, $options...) {
2331
@if inspection.get-theme-version($theme) == 1 {
2432
@include _theme-from-tokens(inspection.get-theme-tokens($theme, color), $options...);
2533
}
@@ -31,9 +39,11 @@
3139
}
3240
}
3341

42+
/// Outputs typography theme styles for the mat-timepicker.
43+
/// @param {Map} $theme The theme to generate typography styles for.
3444
@mixin typography($theme) {
3545
@if inspection.get-theme-version($theme) == 1 {
36-
@include _theme-from-tokens(inspection.get-theme-tokens($theme, typography), $options...);
46+
@include _theme-from-tokens(inspection.get-theme-tokens($theme, typography));
3747
}
3848
@else {
3949
@include sass-utils.current-selector-or-root() {
@@ -43,9 +53,11 @@
4353
}
4454
}
4555

56+
/// Outputs density theme styles for the mat-timepicker.
57+
/// @param {Map} $theme The theme to generate density styles for.
4658
@mixin density($theme) {
4759
@if inspection.get-theme-version($theme) == 1 {
48-
@include _theme-from-tokens(inspection.get-theme-tokens($theme, density), $options...);
60+
@include _theme-from-tokens(inspection.get-theme-tokens($theme, density));
4961
}
5062
@else {
5163
@include sass-utils.current-selector-or-root() {
@@ -55,13 +67,20 @@
5567
}
5668
}
5769

70+
/// Outputs the CSS variable values for the given tokens.
71+
/// @param {Map} $tokens The token values to emit.
5872
@mixin overrides($tokens: ()) {
5973
@include token-utils.batch-create-token-values(
6074
$tokens,
6175
(prefix: tokens-mat-timepicker.$prefix, tokens: tokens-mat-timepicker.get-token-slots()),
6276
);
6377
}
6478

79+
/// Outputs all (base, color, typography, and density) theme styles for the mat-timepicker.
80+
/// @param {Map} $theme The theme to generate styles for.
81+
/// @param {ArgList} Additional optional arguments (only supported for M3 themes):
82+
/// $color-variant: The color variant to use for the main selection: primary, secondary, tertiary,
83+
/// or error (If not specified, default primary color will be used).
6584
@mixin theme($theme) {
6685
@include theming.private-check-duplicate-theme-styles($theme, 'mat-timepicker') {
6786
@if inspection.get-theme-version($theme) == 1 {

‎src/material/timepicker/public-api.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,8 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
export * from './timepicker-module';
109
export * from './timepicker';
10+
export * from './timepicker-input';
11+
export * from './timepicker-toggle';
12+
export * from './timepicker-module';
13+
export {MatTimepickerOption, MAT_TIMEPICKER_CONFIG, MatTimepickerConfig} from './util';

‎src/material/timepicker/timepicker-input.ts

+415
Large diffs are not rendered by default.

‎src/material/timepicker/timepicker-module.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77
*/
88

99
import {NgModule} from '@angular/core';
10+
import {CdkScrollableModule} from '@angular/cdk/scrolling';
1011
import {MatTimepicker} from './timepicker';
12+
import {MatTimepickerInput} from './timepicker-input';
13+
import {MatTimepickerToggle} from './timepicker-toggle';
1114

1215
@NgModule({
13-
imports: [MatTimepicker],
14-
exports: [MatTimepicker],
16+
imports: [MatTimepicker, MatTimepickerInput, MatTimepickerToggle],
17+
exports: [CdkScrollableModule, MatTimepicker, MatTimepickerInput, MatTimepickerToggle],
1518
})
1619
export class MatTimepickerModule {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<button
2+
mat-icon-button
3+
type="button"
4+
aria-haspopup="listbox"
5+
[attr.aria-label]="ariaLabel()"
6+
[attr.aria-expanded]="timepicker().isOpen()"
7+
[attr.tabindex]="disabled() ? -1 : tabIndex()"
8+
[disabled]="disabled()"
9+
[disableRipple]="disableRipple()">
10+
11+
<ng-content select="[matTimepickerToggleIcon]">
12+
<svg
13+
class="mat-timepicker-toggle-default-icon"
14+
height="24px"
15+
width="24px"
16+
viewBox="0 -960 960 960"
17+
fill="currentColor">
18+
<path d="m612-292 56-56-148-148v-184h-80v216l172 172ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-400Zm0 320q133 0 226.5-93.5T800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 133 93.5 226.5T480-160Z"/>
19+
</svg>
20+
</ng-content>
21+
</button>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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 {
10+
booleanAttribute,
11+
ChangeDetectionStrategy,
12+
Component,
13+
HostAttributeToken,
14+
inject,
15+
input,
16+
InputSignal,
17+
InputSignalWithTransform,
18+
ViewEncapsulation,
19+
} from '@angular/core';
20+
import {MatIconButton} from '@angular/material/button';
21+
import {MAT_TIMEPICKER_CONFIG} from './util';
22+
import type {MatTimepicker} from './timepicker';
23+
24+
/** Button that can be used to open a `mat-timepicker`. */
25+
@Component({
26+
selector: 'mat-timepicker-toggle',
27+
templateUrl: 'timepicker-toggle.html',
28+
host: {
29+
'class': 'mat-timepicker-toggle',
30+
'[attr.tabindex]': 'null',
31+
// Bind the `click` on the host, rather than the inner `button`, so that we can call
32+
// `stopPropagation` on it without affecting the user's `click` handlers. We need to stop
33+
// it so that the input doesn't get focused automatically by the form field (See #21836).
34+
'(click)': '_open($event)',
35+
},
36+
exportAs: 'matTimepickerToggle',
37+
encapsulation: ViewEncapsulation.None,
38+
changeDetection: ChangeDetectionStrategy.OnPush,
39+
standalone: true,
40+
imports: [MatIconButton],
41+
})
42+
export class MatTimepickerToggle<D> {
43+
private _defaultConfig = inject(MAT_TIMEPICKER_CONFIG, {optional: true});
44+
private _defaultTabIndex = (() => {
45+
const value = inject(new HostAttributeToken('tabindex'), {optional: true});
46+
const parsed = Number(value);
47+
return isNaN(parsed) ? null : parsed;
48+
})();
49+
50+
/** Timepicker instance that the button will toggle. */
51+
readonly timepicker: InputSignal<MatTimepicker<D>> = input.required<MatTimepicker<D>>({
52+
alias: 'for',
53+
});
54+
55+
/** Screen-reader label for the button. */
56+
readonly ariaLabel = input<string | undefined>(undefined, {
57+
alias: 'aria-label',
58+
});
59+
60+
/** Whether the toggle button is disabled. */
61+
readonly disabled: InputSignalWithTransform<boolean, unknown> = input(false, {
62+
transform: booleanAttribute,
63+
alias: 'disabled',
64+
});
65+
66+
/** Tabindex for the toggle. */
67+
readonly tabIndex: InputSignal<number | null> = input(this._defaultTabIndex);
68+
69+
/** Whether ripples on the toggle should be disabled. */
70+
readonly disableRipple: InputSignalWithTransform<boolean, unknown> = input(
71+
this._defaultConfig?.disableRipple ?? false,
72+
{transform: booleanAttribute},
73+
);
74+
75+
/** Opens the connected timepicker. */
76+
protected _open(event: Event): void {
77+
if (this.timepicker() && !this.disabled()) {
78+
this.timepicker().open();
79+
event.stopPropagation();
80+
}
81+
}
82+
}
+15-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,15 @@
1-
Hello
1+
<ng-template #panelTemplate>
2+
<div
3+
role="listbox"
4+
class="mat-timepicker-panel"
5+
[attr.aria-label]="ariaLabel() || null"
6+
[attr.aria-labelledby]="_getAriaLabelledby()"
7+
[id]="panelId"
8+
@panel>
9+
@for (option of _timeOptions; track option.value) {
10+
<mat-option
11+
[value]="option.value"
12+
(onSelectionChange)="_selectValue(option.value)">{{option.label}}</mat-option>
13+
}
14+
</div>
15+
</ng-template>
+46-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,53 @@
1+
@use '@angular/cdk';
12
@use '../core/tokens/token-utils';
23
@use '../core/tokens/m2/mat/timepicker' as tokens-mat-timepicker;
34

4-
.mat-timepicker {
5+
mat-timepicker {
6+
display: none;
7+
}
8+
9+
.mat-timepicker-panel {
10+
width: 100%;
11+
max-height: 256px;
12+
transform-origin: center top;
13+
overflow: auto;
14+
padding: 8px 0;
15+
box-sizing: border-box;
16+
517
@include token-utils.use-tokens(
618
tokens-mat-timepicker.$prefix, tokens-mat-timepicker.get-token-slots()) {
7-
@include token-utils.create-token-slot(color, enabled-trigger-text-color);
8-
@include token-utils.create-token-slot(font-family, trigger-text-font);
19+
@include token-utils.create-token-slot(border-bottom-left-radius, container-shape);
20+
@include token-utils.create-token-slot(border-bottom-right-radius, container-shape);
21+
@include token-utils.create-token-slot(box-shadow, container-elevation-shadow);
22+
@include token-utils.create-token-slot(background-color, container-background-color);
23+
}
24+
25+
@include cdk.high-contrast {
26+
outline: solid 1px;
27+
}
28+
29+
.mat-timepicker-above & {
30+
border-bottom-left-radius: 0;
31+
border-bottom-right-radius: 0;
32+
33+
@include token-utils.use-tokens(
34+
tokens-mat-timepicker.$prefix, tokens-mat-timepicker.get-token-slots()) {
35+
@include token-utils.create-token-slot(border-top-left-radius, container-shape);
36+
@include token-utils.create-token-slot(border-top-right-radius, container-shape);
37+
}
38+
}
39+
}
40+
41+
// stylelint-disable material/no-prefixes
42+
.mat-timepicker-input:read-only {
43+
cursor: pointer;
44+
}
45+
// stylelint-enable material/no-prefixes
46+
47+
@include cdk.high-contrast {
48+
.mat-timepicker-toggle-default-icon {
49+
// On Chromium-based browsers the icon doesn't appear to inherit the text color in high
50+
// contrast mode so we have to set it explicitly. This is a no-op on IE and Firefox.
51+
color: CanvasText;
952
}
1053
}

‎src/material/timepicker/timepicker.spec.ts

+1,329-6
Large diffs are not rendered by default.

‎src/material/timepicker/timepicker.ts

+433-5
Large diffs are not rendered by default.

‎src/material/timepicker/util.ts

+53
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,30 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import {InjectionToken} from '@angular/core';
910
import {DateAdapter, MatDateFormats} from '@angular/material/core';
1011

1112
/** Pattern that interval strings have to match. */
1213
const INTERVAL_PATTERN = /^(\d*\.?\d+)(h|m|s)?$/i;
1314

15+
/**
16+
* Object that can be used to configure the default options for the timepicker component.
17+
*/
18+
export interface MatTimepickerConfig {
19+
/** Default interval for all time pickers. */
20+
interval?: string | number;
21+
22+
/** Whether ripples inside the timepicker should be disabled by default. */
23+
disableRipple?: boolean;
24+
}
25+
26+
/**
27+
* Injection token that can be used to configure the default options for the timepicker component.
28+
*/
29+
export const MAT_TIMEPICKER_CONFIG = new InjectionToken<MatTimepickerConfig>(
30+
'MAT_TIMEPICKER_CONFIG',
31+
);
32+
1433
/**
1534
* Time selection option that can be displayed within a `mat-timepicker`.
1635
*/
@@ -84,3 +103,37 @@ export function generateOptions<D>(
84103

85104
return options;
86105
}
106+
107+
/** Checks whether a date adapter is set up correctly for use with the timepicker. */
108+
export function validateAdapter(
109+
adapter: DateAdapter<unknown> | null,
110+
formats: MatDateFormats | null,
111+
) {
112+
function missingAdapterError(provider: string) {
113+
return Error(
114+
`MatTimepicker: No provider found for ${provider}. You must add one of the following ` +
115+
`to your app config: provideNativeDateAdapter, provideDateFnsAdapter, ` +
116+
`provideLuxonDateAdapter, provideMomentDateAdapter, or provide a custom implementation.`,
117+
);
118+
}
119+
120+
if (!adapter) {
121+
throw missingAdapterError('DateAdapter');
122+
}
123+
124+
if (!formats) {
125+
throw missingAdapterError('MAT_DATE_FORMATS');
126+
}
127+
128+
if (
129+
formats.display.timeInput === undefined ||
130+
formats.display.timeOptionLabel === undefined ||
131+
formats.parse.timeInput === undefined
132+
) {
133+
throw new Error(
134+
'MatTimepicker: Incomplete `MAT_DATE_FORMATS` has been provided. ' +
135+
'`MAT_DATE_FORMATS` must provide `display.timeInput`, `display.timeOptionLabel` ' +
136+
'and `parse.timeInput` formats in order to be compatible with MatTimepicker.',
137+
);
138+
}
139+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
## API Report File for "components-srcs"
2+
3+
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
4+
5+
```ts
6+
7+
import { AbstractControl } from '@angular/forms';
8+
import { ControlValueAccessor } from '@angular/forms';
9+
import { ElementRef } from '@angular/core';
10+
import * as i0 from '@angular/core';
11+
import * as i4 from '@angular/cdk/scrolling';
12+
import { InjectionToken } from '@angular/core';
13+
import { InputSignal } from '@angular/core';
14+
import { InputSignalWithTransform } from '@angular/core';
15+
import { MatOption } from '@angular/material/core';
16+
import { MatOptionParentComponent } from '@angular/material/core';
17+
import { ModelSignal } from '@angular/core';
18+
import { OnDestroy } from '@angular/core';
19+
import { OutputEmitterRef } from '@angular/core';
20+
import { Signal } from '@angular/core';
21+
import { TemplateRef } from '@angular/core';
22+
import { ValidationErrors } from '@angular/forms';
23+
import { Validator } from '@angular/forms';
24+
25+
// @public
26+
export const MAT_TIMEPICKER_CONFIG: InjectionToken<MatTimepickerConfig>;
27+
28+
// @public
29+
export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {
30+
constructor();
31+
readonly activeDescendant: Signal<string | null>;
32+
readonly ariaLabel: InputSignal<string | null>;
33+
readonly ariaLabelledby: InputSignal<string | null>;
34+
close(): void;
35+
readonly closed: OutputEmitterRef<void>;
36+
readonly disableRipple: InputSignalWithTransform<boolean, unknown>;
37+
protected _getAriaLabelledby(): string | null;
38+
readonly interval: InputSignalWithTransform<number | null, number | string | null>;
39+
readonly isOpen: Signal<boolean>;
40+
// (undocumented)
41+
ngOnDestroy(): void;
42+
open(): void;
43+
readonly opened: OutputEmitterRef<void>;
44+
readonly options: InputSignal<readonly MatTimepickerOption<D>[] | null>;
45+
// (undocumented)
46+
protected _options: Signal<readonly MatOption<any>[]>;
47+
readonly panelId: string;
48+
// (undocumented)
49+
protected _panelTemplate: Signal<TemplateRef<unknown>>;
50+
registerInput(input: MatTimepickerInput<D>): void;
51+
readonly selected: OutputEmitterRef<MatTimepickerSelected<D>>;
52+
protected _selectValue(value: D): void;
53+
// (undocumented)
54+
protected _timeOptions: readonly MatTimepickerOption<D>[];
55+
// (undocumented)
56+
static ɵcmp: i0.ɵɵComponentDeclaration<MatTimepicker<any>, "mat-timepicker", ["matTimepicker"], { "interval": { "alias": "interval"; "required": false; "isSignal": true; }; "options": { "alias": "options"; "required": false; "isSignal": true; }; "disableRipple": { "alias": "disableRipple"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "aria-label"; "required": false; "isSignal": true; }; "ariaLabelledby": { "alias": "aria-labelledby"; "required": false; "isSignal": true; }; }, { "selected": "selected"; "opened": "opened"; "closed": "closed"; }, never, never, true, never>;
57+
// (undocumented)
58+
static ɵfac: i0.ɵɵFactoryDeclaration<MatTimepicker<any>, never>;
59+
}
60+
61+
// @public
62+
export interface MatTimepickerConfig {
63+
disableRipple?: boolean;
64+
interval?: string | number;
65+
}
66+
67+
// @public
68+
export class MatTimepickerInput<D> implements ControlValueAccessor, Validator, OnDestroy {
69+
constructor();
70+
protected readonly _ariaActiveDescendant: Signal<string | null>;
71+
protected readonly _ariaControls: Signal<string | null>;
72+
protected readonly _ariaExpanded: Signal<string>;
73+
readonly disabled: Signal<boolean>;
74+
protected readonly disabledInput: InputSignalWithTransform<boolean, unknown>;
75+
focus(): void;
76+
_getLabelId(): string | null;
77+
getOverlayOrigin(): ElementRef<HTMLElement>;
78+
protected _handleBlur(): void;
79+
protected _handleInput(value: string): void;
80+
protected _handleKeydown(event: KeyboardEvent): void;
81+
readonly max: InputSignalWithTransform<D | null, unknown>;
82+
readonly min: InputSignalWithTransform<D | null, unknown>;
83+
// (undocumented)
84+
ngOnDestroy(): void;
85+
registerOnChange(fn: (value: any) => void): void;
86+
registerOnTouched(fn: () => void): void;
87+
registerOnValidatorChange(fn: () => void): void;
88+
setDisabledState(isDisabled: boolean): void;
89+
readonly timepicker: InputSignal<MatTimepicker<D>>;
90+
validate(control: AbstractControl): ValidationErrors | null;
91+
readonly value: ModelSignal<D | null>;
92+
writeValue(value: any): void;
93+
// (undocumented)
94+
static ɵdir: i0.ɵɵDirectiveDeclaration<MatTimepickerInput<any>, "input[matTimepicker]", ["matTimepickerInput"], { "value": { "alias": "value"; "required": false; "isSignal": true; }; "timepicker": { "alias": "matTimepicker"; "required": true; "isSignal": true; }; "min": { "alias": "matTimepickerMin"; "required": false; "isSignal": true; }; "max": { "alias": "matTimepickerMax"; "required": false; "isSignal": true; }; "disabledInput": { "alias": "disabled"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; }, never, never, true, never>;
95+
// (undocumented)
96+
static ɵfac: i0.ɵɵFactoryDeclaration<MatTimepickerInput<any>, never>;
97+
}
98+
99+
// @public (undocumented)
100+
export class MatTimepickerModule {
101+
// (undocumented)
102+
static ɵfac: i0.ɵɵFactoryDeclaration<MatTimepickerModule, never>;
103+
// (undocumented)
104+
static ɵinj: i0.ɵɵInjectorDeclaration<MatTimepickerModule>;
105+
// (undocumented)
106+
static ɵmod: i0.ɵɵNgModuleDeclaration<MatTimepickerModule, never, [typeof i1.MatTimepicker, typeof i2.MatTimepickerInput, typeof i3.MatTimepickerToggle], [typeof i4.CdkScrollableModule, typeof i1.MatTimepicker, typeof i2.MatTimepickerInput, typeof i3.MatTimepickerToggle]>;
107+
}
108+
109+
// @public
110+
export interface MatTimepickerOption<D = unknown> {
111+
label: string;
112+
value: D;
113+
}
114+
115+
// @public
116+
export interface MatTimepickerSelected<D> {
117+
// (undocumented)
118+
source: MatTimepicker<D>;
119+
// (undocumented)
120+
value: D;
121+
}
122+
123+
// @public
124+
export class MatTimepickerToggle<D> {
125+
readonly ariaLabel: InputSignal<string | undefined>;
126+
readonly disabled: InputSignalWithTransform<boolean, unknown>;
127+
readonly disableRipple: InputSignalWithTransform<boolean, unknown>;
128+
protected _open(event: Event): void;
129+
readonly tabIndex: InputSignal<number | null>;
130+
readonly timepicker: InputSignal<MatTimepicker<D>>;
131+
// (undocumented)
132+
static ɵcmp: i0.ɵɵComponentDeclaration<MatTimepickerToggle<any>, "mat-timepicker-toggle", ["matTimepickerToggle"], { "timepicker": { "alias": "for"; "required": true; "isSignal": true; }; "ariaLabel": { "alias": "aria-label"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "tabIndex": { "alias": "tabIndex"; "required": false; "isSignal": true; }; "disableRipple": { "alias": "disableRipple"; "required": false; "isSignal": true; }; }, {}, never, ["[matTimepickerToggleIcon]"], true, never>;
133+
// (undocumented)
134+
static ɵfac: i0.ɵɵFactoryDeclaration<MatTimepickerToggle<any>, never>;
135+
}
136+
137+
// (No @packageDocumentation comment for this package)
138+
139+
```

0 commit comments

Comments
 (0)
Please sign in to comment.