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;