From 518ee59cd1c2e19bad608ad30a82ec7aecdfb752 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Mon, 7 Oct 2019 22:39:56 +0200 Subject: [PATCH] fix(autocomplete): marking element as touched too early when clicking on options Currently we mark the autocomplete's CVA as touched on each `blur` event, which can happen a little too early if the user holds their pointer while clicking on an item, causing the form validation to show up too early. These changes defer marking the element as touched until the panel has closed. Fixes #13732. --- .../autocomplete/autocomplete-trigger.ts | 14 ++++++- .../autocomplete/autocomplete.spec.ts | 37 ++++++++++++++++++- .../material/autocomplete.d.ts | 1 + 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/material/autocomplete/autocomplete-trigger.ts b/src/material/autocomplete/autocomplete-trigger.ts index dd032e9f6625..a2346b4df5c3 100644 --- a/src/material/autocomplete/autocomplete-trigger.ts +++ b/src/material/autocomplete/autocomplete-trigger.ts @@ -47,7 +47,7 @@ import { } from '@angular/material/core'; import {MatFormField} from '@angular/material/form-field'; import {defer, fromEvent, merge, Observable, of as observableOf, Subject, Subscription} from 'rxjs'; -import {delay, filter, map, switchMap, take, tap} from 'rxjs/operators'; +import {delay, filter, map, switchMap, take, tap, takeUntil} from 'rxjs/operators'; import {MatAutocomplete} from './autocomplete'; import {MatAutocompleteOrigin} from './autocomplete-origin'; @@ -116,7 +116,7 @@ export function getMatAutocompleteMissingPanelError(): Error { // 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)', }, @@ -460,6 +460,16 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, AfterViewIn } } + _handleBlur(): void { + if (this.panelOpen) { + this.autocomplete.closed + .pipe(take(1), takeUntil(this.autocomplete.opened.asObservable())) + .subscribe(() => this._onTouched()); + } else { + 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. diff --git a/src/material/autocomplete/autocomplete.spec.ts b/src/material/autocomplete/autocomplete.spec.ts index 3f2e27c691d7..5496468d652e 100644 --- a/src/material/autocomplete/autocomplete.spec.ts +++ b/src/material/autocomplete/autocomplete.spec.ts @@ -814,15 +814,48 @@ describe('MatAutocomplete', () => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.touched) - .toBe(false, `Expected control to start out untouched.`); + .toBe(false, 'Expected control to start out untouched.'); dispatchFakeEvent(input, 'blur'); fixture.detectChanges(); + fixture.componentInstance.trigger.closePanel(); + fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.touched) - .toBe(true, `Expected control to become touched on blur.`); + .toBe(true, 'Expected control to become touched on blur.'); }); + it('should mark the autocomplete control as touched on blur if the panel is closed', () => { + expect(fixture.componentInstance.stateCtrl.touched) + .toBe(false, 'Expected control to start out untouched.'); + + dispatchFakeEvent(input, 'blur'); + fixture.detectChanges(); + + expect(fixture.componentInstance.stateCtrl.touched) + .toBe(true, 'Expected control to become touched on blur.'); + }); + + it('should not mark the autocomplete control as touched if the input was blurred while ' + + 'the panel is open', () => { + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + 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 remain untouched.'); + + fixture.componentInstance.trigger.closePanel(); + fixture.detectChanges(); + + expect(fixture.componentInstance.stateCtrl.touched) + .toBe(true, 'Expected control to be touched once the panel is closed.'); + }); + 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 99e734c8d3e9..73512a887f83 100644 --- a/tools/public_api_guard/material/autocomplete.d.ts +++ b/tools/public_api_guard/material/autocomplete.d.ts @@ -82,6 +82,7 @@ export declare class MatAutocompleteTrigger implements ControlValueAccessor, Aft readonly 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 | undefined); + _handleBlur(): void; _handleFocus(): void; _handleInput(event: KeyboardEvent): void; _handleKeydown(event: KeyboardEvent): void;