diff --git a/goldens/material/timepicker/index.api.md b/goldens/material/timepicker/index.api.md index 4adfdce95c17..f52481e228ab 100644 --- a/goldens/material/timepicker/index.api.md +++ b/goldens/material/timepicker/index.api.md @@ -100,6 +100,7 @@ export class MatTimepickerInput implements ControlValueAccessor, Validator, O registerOnValidatorChange(fn: () => void): void; setDisabledState(isDisabled: boolean): void; readonly timepicker: InputSignal>; + _timepickerValueAssigned(value: D | null): void; validate(control: AbstractControl): ValidationErrors | null; readonly value: ModelSignal; writeValue(value: any): void; diff --git a/src/material/timepicker/timepicker-input.ts b/src/material/timepicker/timepicker-input.ts index 93e695139fde..8e9d0106f55d 100644 --- a/src/material/timepicker/timepicker-input.ts +++ b/src/material/timepicker/timepicker-input.ts @@ -317,6 +317,14 @@ export class MatTimepickerInput implements ControlValueAccessor, Validator, O } } + /** Called by the timepicker to sync up the user-selected value. */ + _timepickerValueAssigned(value: D | null) { + if (!this._dateAdapter.sameTime(value, this.value())) { + this._assignUserSelection(value, true); + this._formatValue(value); + } + } + /** Sets up the code that watches for changes in the value and adjusts the input. */ private _respondToValueChanges(): void { effect(() => { @@ -346,12 +354,6 @@ export class MatTimepickerInput implements ControlValueAccessor, Validator, O const timepicker = this.timepicker(); timepicker.registerInput(this); timepicker.closed.subscribe(() => this._onTouched?.()); - timepicker.selected.subscribe(({value}) => { - if (!this._dateAdapter.sameTime(value, this.value())) { - this._assignUserSelection(value, true); - this._formatValue(value); - } - }); }); } @@ -371,8 +373,10 @@ export class MatTimepickerInput implements ControlValueAccessor, Validator, O * @param propagateToAccessor Whether the value should be propagated to the ControlValueAccessor. */ private _assignUserSelection(selection: D | null, propagateToAccessor: boolean) { + let toAssign: D | null; + if (selection == null || !this._isValid(selection)) { - this.value.set(selection); + toAssign = selection; } else { // If a datepicker and timepicker are writing to the same object and the user enters an // invalid time into the timepicker, we may end up clearing their selection from the @@ -384,12 +388,15 @@ export class MatTimepickerInput implements ControlValueAccessor, Validator, O const hours = adapter.getHours(selection); const minutes = adapter.getMinutes(selection); const seconds = adapter.getSeconds(selection); - this.value.set(target ? adapter.setTime(target, hours, minutes, seconds) : selection); + toAssign = target ? adapter.setTime(target, hours, minutes, seconds) : selection; } + // Propagate to the form control before emitting to `valueChange`. if (propagateToAccessor) { - this._onChange?.(this.value()); + this._onChange?.(toAssign); } + + this.value.set(toAssign); } /** Formats the current value and assigns it to the input. */ diff --git a/src/material/timepicker/timepicker.spec.ts b/src/material/timepicker/timepicker.spec.ts index 7308e70b3260..d340714006c3 100644 --- a/src/material/timepicker/timepicker.spec.ts +++ b/src/material/timepicker/timepicker.spec.ts @@ -1156,6 +1156,75 @@ describe('MatTimepicker', () => { expect(input.disabled).toBe(true); expect(fixture.componentInstance.input.disabled()).toBe(true); }); + + it('should emit to valueChange before assigning control value when typing', () => { + const fixture = TestBed.createComponent(TimepickerWithForms); + const control = fixture.componentInstance.control; + let eventValue: Date | null = null; + let controlValue: Date | null = null; + fixture.detectChanges(); + + const subscription = fixture.componentInstance.input.value.subscribe(value => { + eventValue = value; + controlValue = control.value; + }); + + typeInElement(getInput(fixture), '1:37 PM'); + fixture.detectChanges(); + + expect(eventValue).toBeTruthy(); + expect(controlValue).toBeTruthy(); + expectSameTime(eventValue, controlValue); + subscription.unsubscribe(); + }); + + it('should emit to valueChange before assigning control value when clicking an option', () => { + const fixture = TestBed.createComponent(TimepickerWithForms); + const control = fixture.componentInstance.control; + let eventValue: Date | null = null; + let controlValue: Date | null = null; + fixture.detectChanges(); + + const subscription = fixture.componentInstance.input.value.subscribe(value => { + eventValue = value; + controlValue = control.value; + }); + + getInput(fixture).click(); + fixture.detectChanges(); + getOptions()[5].click(); + fixture.detectChanges(); + fixture.detectChanges(); + + expect(eventValue).toBeTruthy(); + expect(controlValue).toBeTruthy(); + expectSameTime(eventValue, controlValue); + subscription.unsubscribe(); + }); + + it('should emit to selected event before assigning control value when clicking an option', () => { + const fixture = TestBed.createComponent(TimepickerWithForms); + const control = fixture.componentInstance.control; + let eventValue: Date | null = null; + let controlValue: Date | null = null; + fixture.detectChanges(); + + const subscription = fixture.componentInstance.timepicker.selected.subscribe(event => { + eventValue = event.value; + controlValue = control.value; + }); + + getInput(fixture).click(); + fixture.detectChanges(); + getOptions()[5].click(); + fixture.detectChanges(); + fixture.detectChanges(); + + expect(eventValue).toBeTruthy(); + expect(controlValue).toBeTruthy(); + expectSameTime(eventValue, controlValue); + subscription.unsubscribe(); + }); }); describe('timepicker toggle', () => { @@ -1410,6 +1479,7 @@ class TimepickerTwoWayBinding { }) class TimepickerWithForms { @ViewChild(MatTimepickerInput) input: MatTimepickerInput; + @ViewChild(MatTimepicker) timepicker: MatTimepicker; readonly control = new FormControl(null, [Validators.required]); readonly min = signal(null); readonly max = signal(null); diff --git a/src/material/timepicker/timepicker.ts b/src/material/timepicker/timepicker.ts index ab63291c9699..e5feb08a4854 100644 --- a/src/material/timepicker/timepicker.ts +++ b/src/material/timepicker/timepicker.ts @@ -296,6 +296,8 @@ export class MatTimepicker implements OnDestroy, MatOptionParentComponent { current.deselect(false); } }); + // Notify the input first so it can sync up the form control before emitting to `selected`. + this._input()?._timepickerValueAssigned(option.value); this.selected.emit({value: option.value, source: this}); this._input()?.focus(); }