diff --git a/src/material-experimental/mdc-autocomplete/autocomplete-trigger.ts b/src/material-experimental/mdc-autocomplete/autocomplete-trigger.ts index 4c2196b28c5b..4a69e5c347ae 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($event)', '(input)': '_handleInput($event)', '(keydown)': '_handleKeydown($event)', '(click)': '_handleClick()', diff --git a/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts b/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts index fdd8d06cd0a4..ad1bfecfe28f 100644 --- a/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts +++ b/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts @@ -917,8 +917,6 @@ describe('MDC-based MatAutocomplete', () => { }); it('should mark the autocomplete control as touched on blur', () => { - fixture.componentInstance.trigger.openPanel(); - fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.touched) .withContext(`Expected control to start out untouched.`) .toBe(false); @@ -931,6 +929,67 @@ describe('MDC-based MatAutocomplete', () => { .toBe(true); }); + it('should mark the autocomplete control as touched when the panel is closed via the keyboard', fakeAsync(() => { + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + zone.simulateZoneExit(); + + expect(fixture.componentInstance.stateCtrl.touched).toBe( + false, + `Expected control to start out untouched.`, + ); + + dispatchKeyboardEvent(input, 'keydown', TAB); + fixture.detectChanges(); + + expect(fixture.componentInstance.stateCtrl.touched).toBe( + true, + `Expected control to become touched on blur.`, + ); + })); + + it('should mark the autocomplete control as touched when the panel is closed by clicking away', fakeAsync(() => { + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + zone.simulateZoneExit(); + + expect(fixture.componentInstance.stateCtrl.touched).toBe( + false, + `Expected control to start out untouched.`, + ); + + dispatchFakeEvent(document, 'click'); + 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 when the panel is closed ' + + 'programmatically', + fakeAsync(() => { + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + zone.simulateZoneExit(); + + expect(fixture.componentInstance.stateCtrl.touched).toBe( + false, + `Expected control to start out untouched.`, + ); + + fixture.componentInstance.trigger.closePanel(); + fixture.detectChanges(); + + expect(fixture.componentInstance.stateCtrl.touched).toBe( + false, + `Expected control to stay untouched.`, + ); + }), + ); + it('should disable the input when used with a value accessor and without `matInput`', () => { fixture.destroy(); TestBed.resetTestingModule(); diff --git a/src/material/autocomplete/autocomplete-trigger.ts b/src/material/autocomplete/autocomplete-trigger.ts index e7685d53d5a7..e294362570ce 100644 --- a/src/material/autocomplete/autocomplete-trigger.ts +++ b/src/material/autocomplete/autocomplete-trigger.ts @@ -480,6 +480,12 @@ export abstract class _MatAutocompleteTriggerBase } } + _handleBlur(event: FocusEvent) { + if (!this.panelOpen || !event.isTrusted) { + 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. @@ -600,6 +606,7 @@ export abstract class _MatAutocompleteTriggerBase this._element.nativeElement.focus(); } + this._onTouched(); this.closePanel(); } @@ -835,8 +842,8 @@ export abstract class _MatAutocompleteTriggerBase // 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()', '(input)': '_handleInput($event)', + '(blur)': '_handleBlur($event)', '(keydown)': '_handleKeydown($event)', '(click)': '_handleClick()', }, diff --git a/src/material/autocomplete/autocomplete.spec.ts b/src/material/autocomplete/autocomplete.spec.ts index 3e2fc09f30c7..27a1300e564d 100644 --- a/src/material/autocomplete/autocomplete.spec.ts +++ b/src/material/autocomplete/autocomplete.spec.ts @@ -912,8 +912,6 @@ describe('MatAutocomplete', () => { }); it('should mark the autocomplete control as touched on blur', () => { - fixture.componentInstance.trigger.openPanel(); - fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.touched) .withContext(`Expected control to start out untouched.`) .toBe(false); @@ -926,6 +924,63 @@ describe('MatAutocomplete', () => { .toBe(true); }); + it('should mark the autocomplete control as touched when the panel is closed via the keyboard', fakeAsync(() => { + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + zone.simulateZoneExit(); + + expect(fixture.componentInstance.stateCtrl.touched) + .withContext(`Expected control to start out untouched.`) + .toBe(false); + + dispatchKeyboardEvent(input, 'keydown', TAB); + fixture.detectChanges(); + + expect(fixture.componentInstance.stateCtrl.touched) + .withContext(`Expected control to become touched on blur.`) + .toBe(true); + })); + + it('should mark the autocomplete control as touched when the panel is closed by clicking away', fakeAsync(() => { + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + zone.simulateZoneExit(); + + expect(fixture.componentInstance.stateCtrl.touched) + .withContext(`Expected control to start out untouched.`) + .toBe(false); + + dispatchFakeEvent(document, 'click'); + fixture.detectChanges(); + + expect(fixture.componentInstance.stateCtrl.touched) + .withContext(`Expected control to become touched on blur.`) + .toBe(true); + })); + + it( + 'should not mark the autocomplete control as touched when the panel is closed ' + + 'programmatically', + fakeAsync(() => { + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + zone.simulateZoneExit(); + + expect(fixture.componentInstance.stateCtrl.touched).toBe( + false, + `Expected control to start out untouched.`, + ); + + fixture.componentInstance.trigger.closePanel(); + fixture.detectChanges(); + + expect(fixture.componentInstance.stateCtrl.touched).toBe( + false, + `Expected control to stay untouched.`, + ); + }), + ); + 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.md b/tools/public_api_guard/material/autocomplete.md index 8aaaa0b21737..59e8f96fe7b4 100644 --- a/tools/public_api_guard/material/autocomplete.md +++ b/tools/public_api_guard/material/autocomplete.md @@ -201,6 +201,8 @@ export abstract class _MatAutocompleteTriggerBase implements ControlValueAccesso closePanel(): void; connectedTo: _MatAutocompleteOriginBase; // (undocumented) + _handleBlur(event: FocusEvent): void; + // (undocumented) _handleClick(): void; // (undocumented) _handleFocus(): void;