diff --git a/goldens/material/timepicker/index.api.md b/goldens/material/timepicker/index.api.md index f52481e228ab..4005471a4fb7 100644 --- a/goldens/material/timepicker/index.api.md +++ b/goldens/material/timepicker/index.api.md @@ -99,13 +99,14 @@ export class MatTimepickerInput implements ControlValueAccessor, Validator, O registerOnTouched(fn: () => void): void; registerOnValidatorChange(fn: () => void): void; setDisabledState(isDisabled: boolean): void; + readonly shouldDisplayUnavailableItems: InputSignalWithTransform; readonly timepicker: InputSignal>; _timepickerValueAssigned(value: D | null): void; validate(control: AbstractControl): ValidationErrors | null; readonly value: ModelSignal; writeValue(value: any): void; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration, "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>; + static ɵdir: i0.ɵɵDirectiveDeclaration, "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>; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration, never>; } @@ -122,6 +123,7 @@ export class MatTimepickerModule { // @public export interface MatTimepickerOption { + disabled?: boolean; label: string; value: D; } diff --git a/src/components-examples/material/timepicker/timepicker-options/timepicker-options-example.html b/src/components-examples/material/timepicker/timepicker-options/timepicker-options-example.html index 3641f89734e4..d76b97b4eb13 100644 --- a/src/components-examples/material/timepicker/timepicker-options/timepicker-options-example.html +++ b/src/components-examples/material/timepicker/timepicker-options/timepicker-options-example.html @@ -28,3 +28,19 @@

Custom list of options

+ +

Display unavailable options

+ +
+ + Pick available option + + + + +
diff --git a/src/components-examples/material/timepicker/timepicker-options/timepicker-options-example.ts b/src/components-examples/material/timepicker/timepicker-options/timepicker-options-example.ts index ae75213b1b55..33a05a7cde8a 100644 --- a/src/components-examples/material/timepicker/timepicker-options/timepicker-options-example.ts +++ b/src/components-examples/material/timepicker/timepicker-options/timepicker-options-example.ts @@ -15,7 +15,7 @@ import {provideNativeDateAdapter} from '@angular/material/core'; export class TimepickerOptionsExample { customOptions: MatTimepickerOption[] = [ {label: 'Morning', value: new Date(2024, 0, 1, 9, 0, 0)}, - {label: 'Noon', value: new Date(2024, 0, 1, 12, 0, 0)}, + {label: 'Noon', value: new Date(2024, 0, 1, 12, 0, 0), disabled: true}, {label: 'Evening', value: new Date(2024, 0, 1, 22, 0, 0)}, ]; } diff --git a/src/material/timepicker/timepicker-input.ts b/src/material/timepicker/timepicker-input.ts index 8e9d0106f55d..9c5bb1bcbef4 100644 --- a/src/material/timepicker/timepicker-input.ts +++ b/src/material/timepicker/timepicker-input.ts @@ -140,6 +140,18 @@ export class MatTimepickerInput implements ControlValueAccessor, Validator, O transform: (value: unknown) => this._transformDateInput(value), }); + /** + * Whether the input should display unavailable option items (if any), rather then filtering + * them out. + */ + readonly shouldDisplayUnavailableItems: InputSignalWithTransform = input( + false, + { + alias: 'matDisplayUnavailableItems', + transform: booleanAttribute, + }, + ); + /** * Whether to open the timepicker overlay when clicking on the input. Enabled by default. * Note that when disabling this option, you'll have to provide your own logic for opening diff --git a/src/material/timepicker/timepicker.html b/src/material/timepicker/timepicker.html index 7e248eae11f4..e4367c30f8fe 100644 --- a/src/material/timepicker/timepicker.html +++ b/src/material/timepicker/timepicker.html @@ -11,6 +11,7 @@ @for (option of _timeOptions; track option.value) { {{option.label}} } diff --git a/src/material/timepicker/timepicker.ts b/src/material/timepicker/timepicker.ts index e5feb08a4854..248d56d8f27a 100644 --- a/src/material/timepicker/timepicker.ts +++ b/src/material/timepicker/timepicker.ts @@ -382,13 +382,25 @@ export class MatTimepicker implements OnDestroy, MatOptionParentComponent { const timeFormat = this._dateFormats.display.timeInput; const min = input?.min() || adapter.setTime(adapter.today(), 0, 0, 0); const max = input?.max() || adapter.setTime(adapter.today(), 23, 59, 0); - const cacheKey = - interval + '/' + adapter.format(min, timeFormat) + '/' + adapter.format(max, timeFormat); + const shouldDisplayUnavailableItems = input?.shouldDisplayUnavailableItems() || false; + const cacheKey = [ + interval, + adapter.format(min, timeFormat), + adapter.format(max, timeFormat), + shouldDisplayUnavailableItems ? 'displayUnavailable' : 'hideUnavailable', + ].join('/'); // Don't re-generate the options if the inputs haven't changed. if (cacheKey !== this._optionsCacheKey) { this._optionsCacheKey = cacheKey; - this._timeOptions = generateOptions(adapter, this._dateFormats, min, max, interval); + this._timeOptions = generateOptions( + adapter, + this._dateFormats, + min, + max, + interval, + shouldDisplayUnavailableItems, + ); } } } diff --git a/src/material/timepicker/util.spec.ts b/src/material/timepicker/util.spec.ts index f3225eace971..8b847956309a 100644 --- a/src/material/timepicker/util.spec.ts +++ b/src/material/timepicker/util.spec.ts @@ -193,5 +193,41 @@ describe('timepicker utilities', () => { const options = generateOptions(adapter, formats, min, max, 3600).map(o => o.label); expect(options).toEqual(['1:00 PM']); }); + + it('should generate a list of options including unavailable items', () => { + const min = new Date(2024, 0, 1, 9, 0, 0, 0); + const max = new Date(2024, 0, 1, 22, 0, 0, 0); + const options = generateOptions(adapter, formats, min, max, 3600, true); + const enabledOptions = options.filter(o => !o.disabled).map(o => o.label); + expect(enabledOptions).toEqual([ + '9:00 AM', + '10:00 AM', + '11:00 AM', + '12:00 PM', + '1:00 PM', + '2:00 PM', + '3:00 PM', + '4:00 PM', + '5:00 PM', + '6:00 PM', + '7:00 PM', + '8:00 PM', + '9:00 PM', + '10:00 PM', + ]); + const disabledOptions = options.filter(o => o.disabled).map(o => o.label); + expect(disabledOptions).toEqual([ + '12:00 AM', + '1:00 AM', + '2:00 AM', + '3:00 AM', + '4:00 AM', + '5:00 AM', + '6:00 AM', + '7:00 AM', + '8:00 AM', + '11:00 PM', + ]); + }); }); }); diff --git a/src/material/timepicker/util.ts b/src/material/timepicker/util.ts index f8dfa4c94e61..8b59c80407c2 100644 --- a/src/material/timepicker/util.ts +++ b/src/material/timepicker/util.ts @@ -39,6 +39,9 @@ export interface MatTimepickerOption { /** Label to show to the user. */ label: string; + + /** Whether the option is disabled. */ + disabled?: boolean; } /** Parses an interval value into seconds. */ @@ -88,17 +91,39 @@ export function generateOptions( min: D, max: D, interval: number, + shouldDisplayUnavailableItems: boolean = false, ): MatTimepickerOption[] { const options: MatTimepickerOption[] = []; - let current = adapter.compareTime(min, max) < 1 ? min : max; - while ( - adapter.sameDate(current, min) && - adapter.compareTime(current, max) < 1 && - adapter.isValid(current) - ) { - options.push({value: current, label: adapter.format(current, formats.display.timeOptionLabel)}); - current = adapter.addSeconds(current, interval); + if (shouldDisplayUnavailableItems) { + const todayMin = adapter.setTime(adapter.today(), 0, 0, 0); + const todayMax = adapter.setTime(adapter.today(), 23, 59, 0); + let current = todayMin; + while ( + adapter.sameDate(current, todayMin) && + adapter.compareTime(current, todayMax) < 1 && + adapter.isValid(current) + ) { + options.push({ + value: current, + label: adapter.format(current, formats.display.timeOptionLabel), + disabled: adapter.compareTime(current, min) < 0 || adapter.compareTime(current, max) > 0, + }); + current = adapter.addSeconds(current, interval); + } + } else { + let current = adapter.compareTime(min, max) < 1 ? min : max; + while ( + adapter.sameDate(current, min) && + adapter.compareTime(current, max) < 1 && + adapter.isValid(current) + ) { + options.push({ + value: current, + label: adapter.format(current, formats.display.timeOptionLabel), + }); + current = adapter.addSeconds(current, interval); + } } return options;