Skip to content

Commit 6b76469

Browse files
crisbetoandrewseguin
authored andcommitted
fix(material/autocomplete): closing immediately when input is focused programmatically (#21081)
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. (cherry picked from commit c0ed5ce)
1 parent db17484 commit 6b76469

File tree

3 files changed

+44
-1
lines changed

3 files changed

+44
-1
lines changed

Diff for: src/material-experimental/mdc-autocomplete/autocomplete.spec.ts

+19
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
dispatchEvent,
1010
dispatchFakeEvent,
1111
dispatchKeyboardEvent,
12+
dispatchMouseEvent,
1213
MockNgZone,
1314
typeInElement,
1415
} from '../../cdk/testing/private';
@@ -1395,6 +1396,24 @@ describe('MDC-based MatAutocomplete', () => {
13951396
.toBeFalsy();
13961397
}));
13971398

1399+
it('should not close when a click event occurs on the outside while the panel has focus',
1400+
fakeAsync(() => {
1401+
const trigger = fixture.componentInstance.trigger;
1402+
1403+
input.focus();
1404+
flush();
1405+
fixture.detectChanges();
1406+
1407+
expect(document.activeElement).toBe(input, 'Expected input to be focused.');
1408+
expect(trigger.panelOpen).toBe(true, 'Expected panel to be open.');
1409+
1410+
dispatchMouseEvent(document.body, 'click');
1411+
fixture.detectChanges();
1412+
1413+
expect(document.activeElement).toBe(input, 'Expected input to continue to be focused.');
1414+
expect(trigger.panelOpen).toBe(true, 'Expected panel to stay open.');
1415+
}));
1416+
13981417
it('should reset the active option when closing with the escape key', fakeAsync(() => {
13991418
const trigger = fixture.componentInstance.trigger;
14001419

Diff for: src/material/autocomplete/autocomplete-trigger.ts

+5
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,11 @@ export abstract class _MatAutocompleteTriggerBase
354354
return (
355355
this._overlayAttached &&
356356
clickTarget !== this._element.nativeElement &&
357+
// Normally focus moves inside `mousedown` so this condition will almost always be
358+
// true. Its main purpose is to handle the case where the input is focused from an
359+
// outside click which propagates up to the `body` listener within the same sequence
360+
// and causes the panel to close immediately (see #3106).
361+
this._document.activeElement !== this._element.nativeElement &&
357362
(!formField || !formField.contains(clickTarget)) &&
358363
(!customOrigin || !customOrigin.contains(clickTarget)) &&
359364
!!this._overlayRef &&

Diff for: src/material/autocomplete/autocomplete.spec.ts

+20-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {
1111
dispatchFakeEvent,
1212
dispatchKeyboardEvent,
1313
typeInElement,
14-
} from '../../cdk/testing/private';
14+
dispatchMouseEvent,
15+
} from '@angular/cdk/testing/private';
1516
import {
1617
ChangeDetectionStrategy,
1718
Component,
@@ -1378,6 +1379,24 @@ describe('MatAutocomplete', () => {
13781379
.toBeFalsy();
13791380
}));
13801381

1382+
it('should not close when a click event occurs on the outside while the panel has focus',
1383+
fakeAsync(() => {
1384+
const trigger = fixture.componentInstance.trigger;
1385+
1386+
input.focus();
1387+
flush();
1388+
fixture.detectChanges();
1389+
1390+
expect(document.activeElement).toBe(input, 'Expected input to be focused.');
1391+
expect(trigger.panelOpen).toBe(true, 'Expected panel to be open.');
1392+
1393+
dispatchMouseEvent(document.body, 'click');
1394+
fixture.detectChanges();
1395+
1396+
expect(document.activeElement).toBe(input, 'Expected input to continue to be focused.');
1397+
expect(trigger.panelOpen).toBe(true, 'Expected panel to stay open.');
1398+
}));
1399+
13811400
it('should reset the active option when closing with the escape key', fakeAsync(() => {
13821401
const trigger = fixture.componentInstance.trigger;
13831402

0 commit comments

Comments
 (0)