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;