diff --git a/goldens/material/timepicker/index.api.md b/goldens/material/timepicker/index.api.md index f52481e228ab..337fa9337f53 100644 --- a/goldens/material/timepicker/index.api.md +++ b/goldens/material/timepicker/index.api.md @@ -56,6 +56,8 @@ export class MatTimepicker implements OnDestroy, MatOptionParentComponent { readonly options: InputSignal[] | null>; // (undocumented) protected _options: Signal[]>; + // (undocumented) + protected _optionTemplate: Signal> | undefined>; readonly panelId: string; // (undocumented) protected _panelTemplate: Signal>; @@ -65,7 +67,7 @@ export class MatTimepicker implements OnDestroy, MatOptionParentComponent { // (undocumented) protected _timeOptions: readonly MatTimepickerOption[]; // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration, "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>; + static ɵcmp: i0.ɵɵComponentDeclaration, "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"; }, ["_optionTemplate"], never, true, never>; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration, never>; } diff --git a/src/components-examples/material/timepicker/index.ts b/src/components-examples/material/timepicker/index.ts index 196ce4b3ca88..ce556aa7e70a 100644 --- a/src/components-examples/material/timepicker/index.ts +++ b/src/components-examples/material/timepicker/index.ts @@ -5,4 +5,5 @@ export {TimepickerValidationExample} from './timepicker-validation/timepicker-va export {TimepickerOptionsExample} from './timepicker-options/timepicker-options-example'; export {TimepickerCustomIconExample} from './timepicker-custom-icon/timepicker-custom-icon-example'; export {TimepickerLocaleExample} from './timepicker-locale/timepicker-locale-example'; +export {TimepickerOptionTemplateExample} from './timepicker-option-template/timepicker-option-template-example'; export {TimepickerHarnessExample} from './timepicker-harness/timepicker-harness-example'; diff --git a/src/components-examples/material/timepicker/timepicker-option-template/timepicker-option-template-example.html b/src/components-examples/material/timepicker/timepicker-option-template/timepicker-option-template-example.html new file mode 100644 index 000000000000..9307725b9296 --- /dev/null +++ b/src/components-examples/material/timepicker/timepicker-option-template/timepicker-option-template-example.html @@ -0,0 +1,13 @@ + + Pick a time + + + + + + {{ (dateAdapter.compareTime(option.value, sunrise) > 0 && dateAdapter.compareTime(option.value, sunset) < 0) ? 'sunny' : 'bedtime' }} + + {{ option.label }} + + + diff --git a/src/components-examples/material/timepicker/timepicker-option-template/timepicker-option-template-example.ts b/src/components-examples/material/timepicker/timepicker-option-template/timepicker-option-template-example.ts new file mode 100644 index 000000000000..f865615ac34a --- /dev/null +++ b/src/components-examples/material/timepicker/timepicker-option-template/timepicker-option-template-example.ts @@ -0,0 +1,27 @@ +import {ChangeDetectionStrategy, Component, inject} from '@angular/core'; +import {MatTimepickerModule} from '@angular/material/timepicker'; +import {MatIcon} from '@angular/material/icon'; +import {MatInputModule} from '@angular/material/input'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {DateAdapter, provideNativeDateAdapter} from '@angular/material/core'; + +/** @title Timepicker with option template. */ +@Component({ + selector: 'timepicker-option-template-example', + templateUrl: 'timepicker-option-template-example.html', + providers: [provideNativeDateAdapter()], + imports: [MatFormFieldModule, MatIcon, MatInputModule, MatTimepickerModule], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TimepickerOptionTemplateExample { + readonly dateAdapter = inject>(DateAdapter); + readonly sunrise: Date; + readonly sunset: Date; + + constructor() { + this.sunrise = new Date(); + this.sunrise.setHours(7, 0, 0); + this.sunset = new Date(); + this.sunset.setHours(17, 30, 0); + } +} diff --git a/src/material/slider/_slider-theme.scss b/src/material/slider/_slider-theme.scss index 5a2bc794003a..83e528594ecf 100644 --- a/src/material/slider/_slider-theme.scss +++ b/src/material/slider/_slider-theme.scss @@ -79,7 +79,7 @@ @include token-utils.batch-create-token-values($tokens, _define-overrides()); } -/// Outputs all (base, color, typography, and density) theme styles for the mat-option. +/// Outputs all (base, color, typography, and density) theme styles for the mat-slider. /// @param {Map} $theme The theme to generate styles for. /// @param {String} $color-variant The color variant to use for the component (M3 only) @mixin theme($theme, $color-variant: null) { diff --git a/src/material/timepicker/BUILD.bazel b/src/material/timepicker/BUILD.bazel index 1e8bcf2457ad..9f11af2a66f6 100644 --- a/src/material/timepicker/BUILD.bazel +++ b/src/material/timepicker/BUILD.bazel @@ -77,6 +77,7 @@ ng_project( ":timepicker_css", ], deps = [ + "//:node_modules/@angular/common", "//:node_modules/@angular/core", "//:node_modules/@angular/forms", "//:node_modules/rxjs", diff --git a/src/material/timepicker/timepicker.html b/src/material/timepicker/timepicker.html index 7e248eae11f4..bb5f1dad1840 100644 --- a/src/material/timepicker/timepicker.html +++ b/src/material/timepicker/timepicker.html @@ -1,3 +1,5 @@ +{{ option.label }} +
{{option.label}} + (onSelectionChange)="_selectValue($event.source)"> + + + }
diff --git a/src/material/timepicker/timepicker.spec.ts b/src/material/timepicker/timepicker.spec.ts index d340714006c3..ea1a018d7053 100644 --- a/src/material/timepicker/timepicker.spec.ts +++ b/src/material/timepicker/timepicker.spec.ts @@ -1,4 +1,12 @@ -import {Component, Injector, Provider, signal, ViewChild, ViewEncapsulation} from '@angular/core'; +import { + Component, + inject, + Injector, + Provider, + signal, + ViewChild, + ViewEncapsulation, +} from '@angular/core'; import {ComponentFixture, fakeAsync, flush, TestBed} from '@angular/core/testing'; import {DateAdapter, MATERIAL_ANIMATIONS, provideNativeDateAdapter} from '../core'; import { @@ -652,6 +660,24 @@ describe('MatTimepicker', () => { fixture.detectChanges(); expect(fixture.componentInstance.timepicker.interval()).toBe(null); }); + + it('should be able to apply custom options template', () => { + const fixture = TestBed.createComponent(TimepickerOptionTemplate); + fixture.detectChanges(); + + getInput(fixture).click(); + fixture.detectChanges(); + expect(getOptions().map(o => o.textContent.trim())).toEqual([ + '12:00 AM (off-hours)', + '3:00 AM (off-hours)', + '6:00 AM (off-hours)', + '9:00 AM (working hours)', + '12:00 PM (working hours)', + '3:00 PM (working hours)', + '6:00 PM (off-hours)', + '9:00 PM (off-hours)', + ]); + }); }); describe('mat-form-field integration', () => { @@ -1512,3 +1538,28 @@ class TimepickerWithoutInput { encapsulation: ViewEncapsulation.ShadowDom, }) class TimepickerInShadowDom {} + +@Component({ + template: ` + + + + {{ option.label }} + {{ (dateAdapter.compareTime(option.value, workStartTime) > 0 && dateAdapter.compareTime(option.value, workEndTime) < 0) ? '(working hours)' : '(off-hours)' }} + + + `, + imports: [MatTimepicker, MatTimepickerInput], +}) +class TimepickerOptionTemplate { + readonly dateAdapter = inject>(DateAdapter); + readonly workStartTime: Date; + readonly workEndTime: Date; + + constructor() { + this.workStartTime = new Date(); + this.workStartTime.setHours(8, 30, 0); + this.workEndTime = new Date(); + this.workEndTime.setHours(17, 0, 0); + } +} diff --git a/src/material/timepicker/timepicker.ts b/src/material/timepicker/timepicker.ts index e5feb08a4854..f0fe6c88a0bd 100644 --- a/src/material/timepicker/timepicker.ts +++ b/src/material/timepicker/timepicker.ts @@ -13,6 +13,7 @@ import { ChangeDetectionStrategy, Component, computed, + contentChild, effect, ElementRef, inject, @@ -33,6 +34,7 @@ import { ViewContainerRef, ViewEncapsulation, } from '@angular/core'; +import {NgTemplateOutlet} from '@angular/common'; import { _animationsDisabled, DateAdapter, @@ -92,7 +94,7 @@ export const MAT_TIMEPICKER_SCROLL_STRATEGY = new InjectionToken<() => ScrollStr styleUrl: 'timepicker.css', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, - imports: [MatOption], + imports: [NgTemplateOutlet, MatOption], providers: [ { provide: MAT_OPTION_PARENT_COMPONENT, @@ -123,6 +125,7 @@ export class MatTimepicker implements OnDestroy, MatOptionParentComponent { protected _panelTemplate = viewChild.required>('panelTemplate'); protected _timeOptions: readonly MatTimepickerOption[] = []; protected _options = viewChildren(MatOption); + protected _optionTemplate = contentChild>>(TemplateRef); private _keyManager = new ActiveDescendantKeyManager(this._options, this._injector) .withHomeAndEnd(true)