From 8806d319c0f97dba6ba1a18092c15aa425a62bd4 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 25 Feb 2022 16:41:16 +0100 Subject: [PATCH] fix(material/autocomplete): mark control as touched once panel is closed Currently we mark the autocomplete control as touched on `blur`. The problem is that the `blur` event happens a split second before the panel is closed which can cause the error styling to show up and disappear quickly which looks glitchy. These changes change it so that the control is marked as touched once the panel is closed. Also makes a couple of underscored properties private since they weren't used anywhere in the view. Fixes #18313. --- .../mdc-autocomplete/autocomplete-trigger.ts | 2 +- .../mdc-autocomplete/autocomplete.spec.ts | 63 ++++++++++++++++++- .../autocomplete/autocomplete-trigger.ts | 9 ++- .../autocomplete/autocomplete.spec.ts | 59 ++++++++++++++++- .../public_api_guard/material/autocomplete.md | 2 + 5 files changed, 129 insertions(+), 6 deletions(-) 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;