Skip to content

Commit eeb3bd0

Browse files
committed
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.
1 parent 03485cd commit eeb3bd0

File tree

3 files changed

+44
-1
lines changed

3 files changed

+44
-1
lines changed

src/material-experimental/mdc-autocomplete/autocomplete.spec.ts

Lines changed: 19 additions & 0 deletions
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';
@@ -1374,6 +1375,24 @@ describe('MDC-based MatAutocomplete', () => {
13741375
.toBeFalsy();
13751376
}));
13761377

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

src/material/autocomplete/autocomplete-trigger.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,11 @@ export abstract class _MatAutocompleteTriggerBase
349349
return (
350350
this._overlayAttached &&
351351
clickTarget !== this._element.nativeElement &&
352+
// Normally focus moves inside `mousedown` so this condition will almost always be
353+
// true. Its main purpose is to handle the case where the input is focused from an
354+
// outside click which propagates up to the `body` listener within the same sequence
355+
// and causes the panel to close immediately (see #3106).
356+
this._document.activeElement !== this._element.nativeElement &&
352357
(!formField || !formField.contains(clickTarget)) &&
353358
(!customOrigin || !customOrigin.contains(clickTarget)) &&
354359
!!this._overlayRef &&

src/material/autocomplete/autocomplete.spec.ts

Lines changed: 20 additions & 1 deletion
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,
@@ -1357,6 +1358,24 @@ describe('MatAutocomplete', () => {
13571358
.toBeFalsy();
13581359
}));
13591360

1361+
it('should not close when a click event occurs on the outside while the panel has focus',
1362+
fakeAsync(() => {
1363+
const trigger = fixture.componentInstance.trigger;
1364+
1365+
input.focus();
1366+
flush();
1367+
fixture.detectChanges();
1368+
1369+
expect(document.activeElement).toBe(input, 'Expected input to be focused.');
1370+
expect(trigger.panelOpen).toBe(true, 'Expected panel to be open.');
1371+
1372+
dispatchMouseEvent(document.body, 'click');
1373+
fixture.detectChanges();
1374+
1375+
expect(document.activeElement).toBe(input, 'Expected input to continue to be focused.');
1376+
expect(trigger.panelOpen).toBe(true, 'Expected panel to stay open.');
1377+
}));
1378+
13601379
it('should reset the active option when closing with the escape key', fakeAsync(() => {
13611380
const trigger = fixture.componentInstance.trigger;
13621381

0 commit comments

Comments
 (0)