Skip to content

Commit cf95147

Browse files
committed
feat(material/timepicker): support disabled/unavailable options
close #31842
1 parent 0ab5947 commit cf95147

File tree

8 files changed

+117
-13
lines changed

8 files changed

+117
-13
lines changed

goldens/material/timepicker/index.api.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,14 @@ export class MatTimepickerInput<D> implements ControlValueAccessor, Validator, O
9999
registerOnTouched(fn: () => void): void;
100100
registerOnValidatorChange(fn: () => void): void;
101101
setDisabledState(isDisabled: boolean): void;
102+
readonly shouldDisplayUnavailableItems: InputSignalWithTransform<boolean, unknown>;
102103
readonly timepicker: InputSignal<MatTimepicker<D>>;
103104
_timepickerValueAssigned(value: D | null): void;
104105
validate(control: AbstractControl): ValidationErrors | null;
105106
readonly value: ModelSignal<D | null>;
106107
writeValue(value: any): void;
107108
// (undocumented)
108-
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; }; "openOnClick": { "alias": "matTimepickerOpenOnClick"; "required": false; "isSignal": true; }; "disabledInput": { "alias": "disabled"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; }, never, never, true, never>;
109+
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; }; "shouldDisplayUnavailableItems": { "alias": "matDisplayUnavailableItems"; "required": false; "isSignal": true; }; "openOnClick": { "alias": "matTimepickerOpenOnClick"; "required": false; "isSignal": true; }; "disabledInput": { "alias": "disabled"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; }, never, never, true, never>;
109110
// (undocumented)
110111
static ɵfac: i0.ɵɵFactoryDeclaration<MatTimepickerInput<any>, never>;
111112
}
@@ -122,6 +123,7 @@ export class MatTimepickerModule {
122123

123124
// @public
124125
export interface MatTimepickerOption<D = unknown> {
126+
disabled?: boolean;
125127
label: string;
126128
value: D;
127129
}

src/components-examples/material/timepicker/timepicker-options/timepicker-options-example.html

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,19 @@ <h3>Custom list of options</h3>
2828
<mat-timepicker [options]="customOptions" #customPicker/>
2929
</mat-form-field>
3030
</div>
31+
32+
<h3>Display unavailable options</h3>
33+
34+
<div>
35+
<mat-form-field>
36+
<mat-label>Pick available option</mat-label>
37+
<input
38+
matInput
39+
[matTimepicker]="displayAllPicker"
40+
matDisplayUnavailableItems
41+
matTimepickerMin="9:30"
42+
matTimepickerMax="17:00">
43+
<mat-timepicker-toggle matIconSuffix [for]="displayAllPicker"/>
44+
<mat-timepicker #displayAllPicker/>
45+
</mat-form-field>
46+
</div>

src/components-examples/material/timepicker/timepicker-options/timepicker-options-example.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {provideNativeDateAdapter} from '@angular/material/core';
1515
export class TimepickerOptionsExample {
1616
customOptions: MatTimepickerOption<Date>[] = [
1717
{label: 'Morning', value: new Date(2024, 0, 1, 9, 0, 0)},
18-
{label: 'Noon', value: new Date(2024, 0, 1, 12, 0, 0)},
18+
{label: 'Noon', value: new Date(2024, 0, 1, 12, 0, 0), disabled: true},
1919
{label: 'Evening', value: new Date(2024, 0, 1, 22, 0, 0)},
2020
];
2121
}

src/material/timepicker/timepicker-input.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,18 @@ export class MatTimepickerInput<D> implements ControlValueAccessor, Validator, O
140140
transform: (value: unknown) => this._transformDateInput<D>(value),
141141
});
142142

143+
/**
144+
* Whether the input should display unavailable option items (if any), rather then filtering
145+
* them out.
146+
*/
147+
readonly shouldDisplayUnavailableItems: InputSignalWithTransform<boolean, unknown> = input(
148+
false,
149+
{
150+
alias: 'matDisplayUnavailableItems',
151+
transform: booleanAttribute,
152+
},
153+
);
154+
143155
/**
144156
* Whether to open the timepicker overlay when clicking on the input. Enabled by default.
145157
* Note that when disabling this option, you'll have to provide your own logic for opening

src/material/timepicker/timepicker.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
@for (option of _timeOptions; track option.value) {
1212
<mat-option
1313
[value]="option.value"
14+
[disabled]="option.disabled"
1415
(onSelectionChange)="_selectValue($event.source)">{{option.label}}</mat-option>
1516
}
1617
</div>

src/material/timepicker/timepicker.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -382,13 +382,25 @@ export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {
382382
const timeFormat = this._dateFormats.display.timeInput;
383383
const min = input?.min() || adapter.setTime(adapter.today(), 0, 0, 0);
384384
const max = input?.max() || adapter.setTime(adapter.today(), 23, 59, 0);
385-
const cacheKey =
386-
interval + '/' + adapter.format(min, timeFormat) + '/' + adapter.format(max, timeFormat);
385+
const shouldDisplayUnavailableItems = input?.shouldDisplayUnavailableItems() || false;
386+
const cacheKey = [
387+
interval,
388+
adapter.format(min, timeFormat),
389+
adapter.format(max, timeFormat),
390+
shouldDisplayUnavailableItems ? 'displayUnavailable' : 'hideUnavailable',
391+
].join('/');
387392

388393
// Don't re-generate the options if the inputs haven't changed.
389394
if (cacheKey !== this._optionsCacheKey) {
390395
this._optionsCacheKey = cacheKey;
391-
this._timeOptions = generateOptions(adapter, this._dateFormats, min, max, interval);
396+
this._timeOptions = generateOptions(
397+
adapter,
398+
this._dateFormats,
399+
min,
400+
max,
401+
interval,
402+
shouldDisplayUnavailableItems,
403+
);
392404
}
393405
}
394406
}

src/material/timepicker/util.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,5 +193,41 @@ describe('timepicker utilities', () => {
193193
const options = generateOptions(adapter, formats, min, max, 3600).map(o => o.label);
194194
expect(options).toEqual(['1:00 PM']);
195195
});
196+
197+
it('should generate a list of options including unavailable items', () => {
198+
const min = new Date(2024, 0, 1, 9, 0, 0, 0);
199+
const max = new Date(2024, 0, 1, 22, 0, 0, 0);
200+
const options = generateOptions(adapter, formats, min, max, 3600, true);
201+
const enabledOptions = options.filter(o => !o.disabled).map(o => o.label);
202+
expect(enabledOptions).toEqual([
203+
'9:00 AM',
204+
'10:00 AM',
205+
'11:00 AM',
206+
'12:00 PM',
207+
'1:00 PM',
208+
'2:00 PM',
209+
'3:00 PM',
210+
'4:00 PM',
211+
'5:00 PM',
212+
'6:00 PM',
213+
'7:00 PM',
214+
'8:00 PM',
215+
'9:00 PM',
216+
'10:00 PM',
217+
]);
218+
const disabledOptions = options.filter(o => o.disabled).map(o => o.label);
219+
expect(disabledOptions).toEqual([
220+
'12:00 AM',
221+
'1:00 AM',
222+
'2:00 AM',
223+
'3:00 AM',
224+
'4:00 AM',
225+
'5:00 AM',
226+
'6:00 AM',
227+
'7:00 AM',
228+
'8:00 AM',
229+
'11:00 PM',
230+
]);
231+
});
196232
});
197233
});

src/material/timepicker/util.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ export interface MatTimepickerOption<D = unknown> {
3939

4040
/** Label to show to the user. */
4141
label: string;
42+
43+
/** Whether the option is disabled. */
44+
disabled?: boolean;
4245
}
4346

4447
/** Parses an interval value into seconds. */
@@ -88,17 +91,39 @@ export function generateOptions<D>(
8891
min: D,
8992
max: D,
9093
interval: number,
94+
shouldDisplayUnavailableItems: boolean = false,
9195
): MatTimepickerOption<D>[] {
9296
const options: MatTimepickerOption<D>[] = [];
93-
let current = adapter.compareTime(min, max) < 1 ? min : max;
9497

95-
while (
96-
adapter.sameDate(current, min) &&
97-
adapter.compareTime(current, max) < 1 &&
98-
adapter.isValid(current)
99-
) {
100-
options.push({value: current, label: adapter.format(current, formats.display.timeOptionLabel)});
101-
current = adapter.addSeconds(current, interval);
98+
if (shouldDisplayUnavailableItems) {
99+
const todayMin = adapter.setTime(adapter.today(), 0, 0, 0);
100+
const todayMax = adapter.setTime(adapter.today(), 23, 59, 0);
101+
let current = todayMin;
102+
while (
103+
adapter.sameDate(current, todayMin) &&
104+
adapter.compareTime(current, todayMax) < 1 &&
105+
adapter.isValid(current)
106+
) {
107+
options.push({
108+
value: current,
109+
label: adapter.format(current, formats.display.timeOptionLabel),
110+
disabled: adapter.compareTime(current, min) < 0 || adapter.compareTime(current, max) > 0,
111+
});
112+
current = adapter.addSeconds(current, interval);
113+
}
114+
} else {
115+
let current = adapter.compareTime(min, max) < 1 ? min : max;
116+
while (
117+
adapter.sameDate(current, min) &&
118+
adapter.compareTime(current, max) < 1 &&
119+
adapter.isValid(current)
120+
) {
121+
options.push({
122+
value: current,
123+
label: adapter.format(current, formats.display.timeOptionLabel),
124+
});
125+
current = adapter.addSeconds(current, interval);
126+
}
102127
}
103128

104129
return options;

0 commit comments

Comments
 (0)