From eeb3bd0263f0f1446843f97ef8c4dbd0da156c5f Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 18 Nov 2020 21:57:25 +0100 Subject: [PATCH] fix(material/autocomplete): closing immediately when input is focused programmatically Each autocomplete has a `click` listener on the body that closes the panel when the user has clicked somewhere outside of it. This breaks down if the panel is opened through a click outside of the form field, because we bind the listener before the event has bubbled all the way to the `body` so when it does get there, it closes immediately. These changes fix the issue by taking advantage of the fact that focus usually moves on `mousedown` so for "real" click the input will have already lost focus by the time the `click` event happens. Fixes #3106. --- .../mdc-autocomplete/autocomplete.spec.ts | 19 +++++++++++++++++ .../autocomplete/autocomplete-trigger.ts | 5 +++++ .../autocomplete/autocomplete.spec.ts | 21 ++++++++++++++++++- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts b/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts index 3f8771b1b52a..701a483f9f19 100644 --- a/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts +++ b/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts @@ -9,6 +9,7 @@ import { dispatchEvent, dispatchFakeEvent, dispatchKeyboardEvent, + dispatchMouseEvent, MockNgZone, typeInElement, } from '../../cdk/testing/private'; @@ -1374,6 +1375,24 @@ describe('MDC-based MatAutocomplete', () => { .toBeFalsy(); })); + it('should not close when a click event occurs on the outside while the panel has focus', + fakeAsync(() => { + const trigger = fixture.componentInstance.trigger; + + input.focus(); + flush(); + fixture.detectChanges(); + + expect(document.activeElement).toBe(input, 'Expected input to be focused.'); + expect(trigger.panelOpen).toBe(true, 'Expected panel to be open.'); + + dispatchMouseEvent(document.body, 'click'); + fixture.detectChanges(); + + expect(document.activeElement).toBe(input, 'Expected input to continue to be focused.'); + expect(trigger.panelOpen).toBe(true, 'Expected panel to stay open.'); + })); + it('should reset the active option when closing with the escape key', fakeAsync(() => { const trigger = fixture.componentInstance.trigger; diff --git a/src/material/autocomplete/autocomplete-trigger.ts b/src/material/autocomplete/autocomplete-trigger.ts index ccb6078b32cb..937a3569c4bb 100644 --- a/src/material/autocomplete/autocomplete-trigger.ts +++ b/src/material/autocomplete/autocomplete-trigger.ts @@ -349,6 +349,11 @@ export abstract class _MatAutocompleteTriggerBase return ( this._overlayAttached && clickTarget !== this._element.nativeElement && + // Normally focus moves inside `mousedown` so this condition will almost always be + // true. Its main purpose is to handle the case where the input is focused from an + // outside click which propagates up to the `body` listener within the same sequence + // and causes the panel to close immediately (see #3106). + this._document.activeElement !== this._element.nativeElement && (!formField || !formField.contains(clickTarget)) && (!customOrigin || !customOrigin.contains(clickTarget)) && !!this._overlayRef && diff --git a/src/material/autocomplete/autocomplete.spec.ts b/src/material/autocomplete/autocomplete.spec.ts index 5ea9ab5bb257..7879f06492b9 100644 --- a/src/material/autocomplete/autocomplete.spec.ts +++ b/src/material/autocomplete/autocomplete.spec.ts @@ -11,7 +11,8 @@ import { dispatchFakeEvent, dispatchKeyboardEvent, typeInElement, -} from '../../cdk/testing/private'; + dispatchMouseEvent, +} from '@angular/cdk/testing/private'; import { ChangeDetectionStrategy, Component, @@ -1357,6 +1358,24 @@ describe('MatAutocomplete', () => { .toBeFalsy(); })); + it('should not close when a click event occurs on the outside while the panel has focus', + fakeAsync(() => { + const trigger = fixture.componentInstance.trigger; + + input.focus(); + flush(); + fixture.detectChanges(); + + expect(document.activeElement).toBe(input, 'Expected input to be focused.'); + expect(trigger.panelOpen).toBe(true, 'Expected panel to be open.'); + + dispatchMouseEvent(document.body, 'click'); + fixture.detectChanges(); + + expect(document.activeElement).toBe(input, 'Expected input to continue to be focused.'); + expect(trigger.panelOpen).toBe(true, 'Expected panel to stay open.'); + })); + it('should reset the active option when closing with the escape key', fakeAsync(() => { const trigger = fixture.componentInstance.trigger;