Skip to content

Commit 8d0658d

Browse files
committed
fix(material/autocomplete): form control being marked as touched too early when clicking on an option
Currently we mark the autocomplete form control as touched on every blur event. This can be an issue for the case where a control is being validated on blur, because the input becomes blurred as soon as the user has their pointer down on something else (e.g. one of the options). This will cause validation to run before the value has been assigned. With these changes we switch to marking the control as touched once the panel has been closed. Fixes #11903.
1 parent 5bac828 commit 8d0658d

File tree

5 files changed

+60
-8
lines changed

5 files changed

+60
-8
lines changed

src/material-experimental/mdc-autocomplete/autocomplete-trigger.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const MAT_AUTOCOMPLETE_VALUE_ACCESSOR: any = {
3838
// Note: we use `focusin`, as opposed to `focus`, in order to open the panel
3939
// a little earlier. This avoids issues where IE delays the focusing of the input.
4040
'(focusin)': '_handleFocus()',
41-
'(blur)': '_onTouched()',
41+
'(blur)': '_handleBlur()',
4242
'(input)': '_handleInput($event)',
4343
'(keydown)': '_handleKeydown($event)',
4444
},

src/material-experimental/mdc-autocomplete/autocomplete.spec.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -810,9 +810,7 @@ describe('MDC-based MatAutocomplete', () => {
810810
.toBe(false, `Expected control to stay pristine if value is set programmatically.`);
811811
});
812812

813-
it('should mark the autocomplete control as touched on blur', () => {
814-
fixture.componentInstance.trigger.openPanel();
815-
fixture.detectChanges();
813+
it('should mark the autocomplete control as touched on blur while panel is closed', () => {
816814
expect(fixture.componentInstance.stateCtrl.touched)
817815
.toBe(false, `Expected control to start out untouched.`);
818816

@@ -823,6 +821,28 @@ describe('MDC-based MatAutocomplete', () => {
823821
.toBe(true, `Expected control to become touched on blur.`);
824822
});
825823

824+
it('should defer marking the control as touched if it is blurred while open', () => {
825+
fixture.componentInstance.trigger.openPanel();
826+
fixture.detectChanges();
827+
zone.simulateZoneExit();
828+
829+
expect(fixture.componentInstance.stateCtrl.touched)
830+
.toBe(false, 'Expected control to start out untouched.');
831+
832+
dispatchFakeEvent(input, 'blur');
833+
fixture.detectChanges();
834+
835+
expect(fixture.componentInstance.stateCtrl.touched)
836+
.toBe(false, 'Expected control to stay untouched.');
837+
838+
// Simulate clicking outside the panel.
839+
dispatchFakeEvent(document, 'click');
840+
fixture.detectChanges();
841+
842+
expect(fixture.componentInstance.stateCtrl.touched)
843+
.toBe(true, 'Expected control to become touched once panel closes.');
844+
});
845+
826846
it('should disable the input when used with a value accessor and without `matInput`', () => {
827847
overlayContainer.ngOnDestroy();
828848
fixture.destroy();

src/material/autocomplete/autocomplete-trigger.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,16 @@ export abstract class _MatAutocompleteTriggerBase implements ControlValueAccesso
451451
}
452452
}
453453

454+
_handleBlur(): void {
455+
// Blur events will fire as soon as the user has their pointer down on an option. We don't
456+
// mark the control as touched in this case, because it can cause the validation to be run
457+
// before a value has been assigned. Instead, we skip marking it as touched from here
458+
// and we do so once the panel has closed.
459+
if (!this.panelOpen) {
460+
this._onTouched();
461+
}
462+
}
463+
454464
/**
455465
* In "auto" mode, the label will animate down as soon as focus is lost.
456466
* This causes the value to jump when selecting an option with the mouse.
@@ -564,6 +574,7 @@ export abstract class _MatAutocompleteTriggerBase implements ControlValueAccesso
564574
}
565575

566576
this.closePanel();
577+
this._onTouched();
567578
}
568579

569580
/**
@@ -781,7 +792,7 @@ export abstract class _MatAutocompleteTriggerBase implements ControlValueAccesso
781792
// Note: we use `focusin`, as opposed to `focus`, in order to open the panel
782793
// a little earlier. This avoids issues where IE delays the focusing of the input.
783794
'(focusin)': '_handleFocus()',
784-
'(blur)': '_onTouched()',
795+
'(blur)': '_handleBlur()',
785796
'(input)': '_handleInput($event)',
786797
'(keydown)': '_handleKeydown($event)',
787798
},

src/material/autocomplete/autocomplete.spec.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -810,9 +810,7 @@ describe('MatAutocomplete', () => {
810810
.toBe(false, `Expected control to stay pristine if value is set programmatically.`);
811811
});
812812

813-
it('should mark the autocomplete control as touched on blur', () => {
814-
fixture.componentInstance.trigger.openPanel();
815-
fixture.detectChanges();
813+
it('should mark the autocomplete control as touched on blur while panel is closed', () => {
816814
expect(fixture.componentInstance.stateCtrl.touched)
817815
.toBe(false, `Expected control to start out untouched.`);
818816

@@ -823,6 +821,28 @@ describe('MatAutocomplete', () => {
823821
.toBe(true, `Expected control to become touched on blur.`);
824822
});
825823

824+
it('should defer marking the control as touched if it is blurred while open', () => {
825+
fixture.componentInstance.trigger.openPanel();
826+
fixture.detectChanges();
827+
zone.simulateZoneExit();
828+
829+
expect(fixture.componentInstance.stateCtrl.touched)
830+
.toBe(false, 'Expected control to start out untouched.');
831+
832+
dispatchFakeEvent(input, 'blur');
833+
fixture.detectChanges();
834+
835+
expect(fixture.componentInstance.stateCtrl.touched)
836+
.toBe(false, 'Expected control to stay untouched.');
837+
838+
// Simulate clicking outside the panel.
839+
dispatchFakeEvent(document, 'click');
840+
fixture.detectChanges();
841+
842+
expect(fixture.componentInstance.stateCtrl.touched)
843+
.toBe(true, 'Expected control to become touched once panel closes.');
844+
});
845+
826846
it('should disable the input when used with a value accessor and without `matInput`', () => {
827847
overlayContainer.ngOnDestroy();
828848
fixture.destroy();

tools/public_api_guard/material/autocomplete.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export declare abstract class _MatAutocompleteTriggerBase implements ControlValu
5858
get panelOpen(): boolean;
5959
position: 'auto' | 'above' | 'below';
6060
constructor(_element: ElementRef<HTMLInputElement>, _overlay: Overlay, _viewContainerRef: ViewContainerRef, _zone: NgZone, _changeDetectorRef: ChangeDetectorRef, scrollStrategy: any, _dir: Directionality, _formField: MatFormField, _document: any, _viewportRuler: ViewportRuler, _defaults?: MatAutocompleteDefaultOptions | undefined);
61+
_handleBlur(): void;
6162
_handleFocus(): void;
6263
_handleInput(event: KeyboardEvent): void;
6364
_handleKeydown(event: KeyboardEvent): void;

0 commit comments

Comments
 (0)