Skip to content

Commit 75367e7

Browse files
crisbetoandrewseguin
authored andcommitted
fix(material/datepicker): mark date input as touched when calendar is closed (#21646)
Currently we mark the date input model as touched when it is blurred or the user types something in, however opening and closing the calendar is also an indicator that the user has interacted with it. These changes add some logic to mark the inputs as touched when the calendar is closed as well. Fixes #21643. (cherry picked from commit b559786)
1 parent 958ecb2 commit 75367e7

File tree

5 files changed

+73
-3
lines changed

5 files changed

+73
-3
lines changed

src/material/datepicker/date-range-input.spec.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,36 @@ describe('MatDateRangeInput', () => {
636636
expect(endModel.dirty).toBe(true);
637637
}));
638638

639+
it('should mark both inputs as touched when the range picker is closed', fakeAsync(() => {
640+
const fixture = createComponent(RangePickerNgModel);
641+
fixture.detectChanges();
642+
flush();
643+
const {startModel, endModel, rangePicker} = fixture.componentInstance;
644+
645+
expect(startModel.dirty).toBe(false);
646+
expect(startModel.touched).toBe(false);
647+
expect(endModel.dirty).toBe(false);
648+
expect(endModel.touched).toBe(false);
649+
650+
rangePicker.open();
651+
fixture.detectChanges();
652+
flush();
653+
654+
expect(startModel.dirty).toBe(false);
655+
expect(startModel.touched).toBe(false);
656+
expect(endModel.dirty).toBe(false);
657+
expect(endModel.touched).toBe(false);
658+
659+
rangePicker.close();
660+
fixture.detectChanges();
661+
flush();
662+
663+
expect(startModel.dirty).toBe(false);
664+
expect(startModel.touched).toBe(true);
665+
expect(endModel.dirty).toBe(false);
666+
expect(endModel.touched).toBe(true);
667+
}));
668+
639669
it('should move focus to the start input when pressing backspace on an empty end input', () => {
640670
const fixture = createComponent(StandardRangePicker);
641671
fixture.detectChanges();
@@ -928,6 +958,7 @@ class RangePickerNgModel {
928958
@ViewChild(MatEndDate, {read: NgModel}) endModel: NgModel;
929959
@ViewChild(MatStartDate, {read: ElementRef}) startInput: ElementRef<HTMLInputElement>;
930960
@ViewChild(MatEndDate, {read: ElementRef}) endInput: ElementRef<HTMLInputElement>;
961+
@ViewChild(MatDateRangePicker) rangePicker: MatDateRangePicker<Date>;
931962
start: Date | null = null;
932963
end: Date | null = null;
933964
}

src/material/datepicker/date-range-input.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
import {MatFormFieldControl, MatFormField, MAT_FORM_FIELD} from '@angular/material/form-field';
2626
import {ThemePalette, DateAdapter} from '@angular/material/core';
2727
import {NgControl, ControlContainer} from '@angular/forms';
28-
import {Subject, merge} from 'rxjs';
28+
import {Subject, merge, Subscription} from 'rxjs';
2929
import {coerceBooleanProperty, BooleanInput} from '@angular/cdk/coercion';
3030
import {
3131
MatStartDate,
@@ -68,6 +68,8 @@ let nextUniqueId = 0;
6868
export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
6969
MatDatepickerControl<D>, MatDateRangeInputParent<D>, MatDateRangePickerInput<D>,
7070
AfterContentInit, OnChanges, OnDestroy {
71+
private _closedSubscription = Subscription.EMPTY;
72+
7173
/** Current value of the range input. */
7274
get value() {
7375
return this._model ? this._model.selection : null;
@@ -105,6 +107,11 @@ export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
105107
if (rangePicker) {
106108
this._model = rangePicker.registerInput(this);
107109
this._rangePicker = rangePicker;
110+
this._closedSubscription.unsubscribe();
111+
this._closedSubscription = rangePicker.closedStream.subscribe(() => {
112+
this._startInput?._onTouched();
113+
this._endInput?._onTouched();
114+
});
108115
this._registerModel(this._model!);
109116
}
110117
}
@@ -291,6 +298,7 @@ export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
291298
}
292299

293300
ngOnDestroy() {
301+
this._closedSubscription.unsubscribe();
294302
this.stateChanges.complete();
295303
}
296304

src/material/datepicker/datepicker-input.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
forwardRef,
1313
Inject,
1414
Input,
15+
OnDestroy,
1516
Optional,
1617
} from '@angular/core';
1718
import {
@@ -28,6 +29,7 @@ import {
2829
} from '@angular/material/core';
2930
import {MatFormField, MAT_FORM_FIELD} from '@angular/material/form-field';
3031
import {MAT_INPUT_VALUE_ACCESSOR} from '@angular/material/input';
32+
import {Subscription} from 'rxjs';
3133
import {MatDatepickerInputBase, DateFilterFn} from './datepicker-input-base';
3234
import {MatDatepickerControl, MatDatepickerPanel} from './datepicker-base';
3335
import {DateSelectionModelChange} from './date-selection-model';
@@ -72,12 +74,15 @@ export const MAT_DATEPICKER_VALIDATORS: any = {
7274
exportAs: 'matDatepickerInput',
7375
})
7476
export class MatDatepickerInput<D> extends MatDatepickerInputBase<D | null, D>
75-
implements MatDatepickerControl<D | null> {
77+
implements MatDatepickerControl<D | null>, OnDestroy {
78+
private _closedSubscription = Subscription.EMPTY;
79+
7680
/** The datepicker that this input is associated with. */
7781
@Input()
7882
set matDatepicker(datepicker: MatDatepickerPanel<MatDatepickerControl<D>, D | null, D>) {
7983
if (datepicker) {
8084
this._datepicker = datepicker;
85+
this._closedSubscription = datepicker.closedStream.subscribe(() => this._onTouched());
8186
this._registerModel(datepicker.registerInput(this));
8287
}
8388
}
@@ -152,6 +157,11 @@ export class MatDatepickerInput<D> extends MatDatepickerInputBase<D | null, D>
152157
return this.value;
153158
}
154159

160+
ngOnDestroy() {
161+
super.ngOnDestroy();
162+
this._closedSubscription.unsubscribe();
163+
}
164+
155165
/** Opens the associated datepicker. */
156166
protected _openPopup(): void {
157167
if (this._datepicker) {

src/material/datepicker/datepicker.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -857,6 +857,26 @@ describe('MatDatepicker', () => {
857857
expect(inputEl.classList).toContain('ng-touched');
858858
});
859859

860+
it('should mark input as touched when the datepicker is closed', fakeAsync(() => {
861+
let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
862+
863+
expect(inputEl.classList).toContain('ng-untouched');
864+
865+
fixture.componentInstance.datepicker.open();
866+
fixture.detectChanges();
867+
flush();
868+
fixture.detectChanges();
869+
870+
expect(inputEl.classList).toContain('ng-untouched');
871+
872+
fixture.componentInstance.datepicker.close();
873+
fixture.detectChanges();
874+
flush();
875+
fixture.detectChanges();
876+
877+
expect(inputEl.classList).toContain('ng-touched');
878+
}));
879+
860880
it('should reformat the input value on blur', () => {
861881
if (SUPPORTS_INTL) {
862882
// Skip this test if the internationalization API is not supported in the current

tools/public_api_guard/material/datepicker.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ export declare class MatDatepickerContent<S, D = ExtractDateTypeFromSelection<S>
236236
static ɵfac: i0.ɵɵFactoryDef<MatDatepickerContent<any, any>, [null, null, null, null, { optional: true; }, null]>;
237237
}
238238

239-
export declare class MatDatepickerInput<D> extends MatDatepickerInputBase<D | null, D> implements MatDatepickerControl<D | null> {
239+
export declare class MatDatepickerInput<D> extends MatDatepickerInputBase<D | null, D> implements MatDatepickerControl<D | null>, OnDestroy {
240240
_datepicker: MatDatepickerPanel<MatDatepickerControl<D>, D | null, D>;
241241
protected _validator: ValidatorFn | null;
242242
get dateFilter(): DateFilterFn<D | null>;
@@ -257,6 +257,7 @@ export declare class MatDatepickerInput<D> extends MatDatepickerInputBase<D | nu
257257
getConnectedOverlayOrigin(): ElementRef;
258258
getStartValue(): D | null;
259259
getThemePalette(): ThemePalette;
260+
ngOnDestroy(): void;
260261
static ngAcceptInputType_value: any;
261262
static ɵdir: i0.ɵɵDirectiveDefWithMeta<MatDatepickerInput<any>, "input[matDatepicker]", ["matDatepickerInput"], { "matDatepicker": "matDatepicker"; "min": "min"; "max": "max"; "dateFilter": "matDatepickerFilter"; }, {}, never>;
262263
static ɵfac: i0.ɵɵFactoryDef<MatDatepickerInput<any>, [null, { optional: true; }, { optional: true; }, { optional: true; }]>;

0 commit comments

Comments
 (0)