From 8d0658d851726a1ac76f4cc01bd0d9dda73441f3 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 12 Oct 2020 23:39:10 +0200 Subject: [PATCH] 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. --- .../mdc-autocomplete/autocomplete-trigger.ts | 2 +- .../mdc-autocomplete/autocomplete.spec.ts | 26 ++++++++++++++++--- .../autocomplete/autocomplete-trigger.ts | 13 +++++++++- .../autocomplete/autocomplete.spec.ts | 26 ++++++++++++++++--- .../material/autocomplete.d.ts | 1 + 5 files changed, 60 insertions(+), 8 deletions(-) diff --git a/src/material-experimental/mdc-autocomplete/autocomplete-trigger.ts b/src/material-experimental/mdc-autocomplete/autocomplete-trigger.ts index 668591a0bf23..f3388f5b969b 100644 --- a/src/material-experimental/mdc-autocomplete/autocomplete-trigger.ts +++ b/src/material-experimental/mdc-autocomplete/autocomplete-trigger.ts @@ -38,7 +38,7 @@ export const MAT_AUTOCOMPLETE_VALUE_ACCESSOR: any = { // Note: we use `focusin`, as opposed to `focus`, in order to open the panel // a little earlier. This avoids issues where IE delays the focusing of the input. '(focusin)': '_handleFocus()', - '(blur)': '_onTouched()', + '(blur)': '_handleBlur()', '(input)': '_handleInput($event)', '(keydown)': '_handleKeydown($event)', }, diff --git a/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts b/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts index 51f15af34dcc..90355a82ea7a 100644 --- a/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts +++ b/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts @@ -810,9 +810,7 @@ describe('MDC-based MatAutocomplete', () => { .toBe(false, `Expected control to stay pristine if value is set programmatically.`); }); - it('should mark the autocomplete control as touched on blur', () => { - fixture.componentInstance.trigger.openPanel(); - fixture.detectChanges(); + it('should mark the autocomplete control as touched on blur while panel is closed', () => { expect(fixture.componentInstance.stateCtrl.touched) .toBe(false, `Expected control to start out untouched.`); @@ -823,6 +821,28 @@ describe('MDC-based MatAutocomplete', () => { .toBe(true, `Expected control to become touched on blur.`); }); + it('should defer marking the control as touched if it is blurred while open', () => { + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + zone.simulateZoneExit(); + + expect(fixture.componentInstance.stateCtrl.touched) + .toBe(false, 'Expected control to start out untouched.'); + + dispatchFakeEvent(input, 'blur'); + fixture.detectChanges(); + + expect(fixture.componentInstance.stateCtrl.touched) + .toBe(false, 'Expected control to stay untouched.'); + + // Simulate clicking outside the panel. + dispatchFakeEvent(document, 'click'); + fixture.detectChanges(); + + expect(fixture.componentInstance.stateCtrl.touched) + .toBe(true, 'Expected control to become touched once panel closes.'); + }); + it('should disable the input when used with a value accessor and without `matInput`', () => { overlayContainer.ngOnDestroy(); fixture.destroy(); diff --git a/src/material/autocomplete/autocomplete-trigger.ts b/src/material/autocomplete/autocomplete-trigger.ts index e29fef802797..28f25bbce102 100644 --- a/src/material/autocomplete/autocomplete-trigger.ts +++ b/src/material/autocomplete/autocomplete-trigger.ts @@ -451,6 +451,16 @@ export abstract class _MatAutocompleteTriggerBase implements ControlValueAccesso } } + _handleBlur(): void { + // Blur events will fire as soon as the user has their pointer down on an option. We don't + // mark the control as touched in this case, because it can cause the validation to be run + // before a value has been assigned. Instead, we skip marking it as touched from here + // and we do so once the panel has closed. + if (!this.panelOpen) { + this._onTouched(); + } + } + /** * In "auto" mode, the label will animate down as soon as focus is lost. * This causes the value to jump when selecting an option with the mouse. @@ -564,6 +574,7 @@ export abstract class _MatAutocompleteTriggerBase implements ControlValueAccesso } this.closePanel(); + this._onTouched(); } /** @@ -781,7 +792,7 @@ export abstract class _MatAutocompleteTriggerBase implements ControlValueAccesso // Note: we use `focusin`, as opposed to `focus`, in order to open the panel // a little earlier. This avoids issues where IE delays the focusing of the input. '(focusin)': '_handleFocus()', - '(blur)': '_onTouched()', + '(blur)': '_handleBlur()', '(input)': '_handleInput($event)', '(keydown)': '_handleKeydown($event)', }, diff --git a/src/material/autocomplete/autocomplete.spec.ts b/src/material/autocomplete/autocomplete.spec.ts index 76d4791115c1..64426264ee71 100644 --- a/src/material/autocomplete/autocomplete.spec.ts +++ b/src/material/autocomplete/autocomplete.spec.ts @@ -810,9 +810,7 @@ describe('MatAutocomplete', () => { .toBe(false, `Expected control to stay pristine if value is set programmatically.`); }); - it('should mark the autocomplete control as touched on blur', () => { - fixture.componentInstance.trigger.openPanel(); - fixture.detectChanges(); + it('should mark the autocomplete control as touched on blur while panel is closed', () => { expect(fixture.componentInstance.stateCtrl.touched) .toBe(false, `Expected control to start out untouched.`); @@ -823,6 +821,28 @@ describe('MatAutocomplete', () => { .toBe(true, `Expected control to become touched on blur.`); }); + it('should defer marking the control as touched if it is blurred while open', () => { + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + zone.simulateZoneExit(); + + expect(fixture.componentInstance.stateCtrl.touched) + .toBe(false, 'Expected control to start out untouched.'); + + dispatchFakeEvent(input, 'blur'); + fixture.detectChanges(); + + expect(fixture.componentInstance.stateCtrl.touched) + .toBe(false, 'Expected control to stay untouched.'); + + // Simulate clicking outside the panel. + dispatchFakeEvent(document, 'click'); + fixture.detectChanges(); + + expect(fixture.componentInstance.stateCtrl.touched) + .toBe(true, 'Expected control to become touched once panel closes.'); + }); + it('should disable the input when used with a value accessor and without `matInput`', () => { overlayContainer.ngOnDestroy(); fixture.destroy(); diff --git a/tools/public_api_guard/material/autocomplete.d.ts b/tools/public_api_guard/material/autocomplete.d.ts index aa85d12288fa..1a4053d69656 100644 --- a/tools/public_api_guard/material/autocomplete.d.ts +++ b/tools/public_api_guard/material/autocomplete.d.ts @@ -58,6 +58,7 @@ export declare abstract class _MatAutocompleteTriggerBase implements ControlValu get panelOpen(): boolean; position: 'auto' | 'above' | 'below'; constructor(_element: ElementRef, _overlay: Overlay, _viewContainerRef: ViewContainerRef, _zone: NgZone, _changeDetectorRef: ChangeDetectorRef, scrollStrategy: any, _dir: Directionality, _formField: MatFormField, _document: any, _viewportRuler: ViewportRuler, _defaults?: MatAutocompleteDefaultOptions | undefined); + _handleBlur(): void; _handleFocus(): void; _handleInput(event: KeyboardEvent): void; _handleKeydown(event: KeyboardEvent): void;