diff --git a/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts b/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts index 852e62aaf05c..2489b0001024 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'; @@ -1395,6 +1396,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 fa6f0b15d39e..be87ee1de4f7 100644 --- a/src/material/autocomplete/autocomplete-trigger.ts +++ b/src/material/autocomplete/autocomplete-trigger.ts @@ -354,6 +354,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 b905b193b4a3..136aa58aae05 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, @@ -1378,6 +1379,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;