Skip to content

Commit bb163c6

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 b17ed9d commit bb163c6

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';
@@ -1261,6 +1262,24 @@ describe('MDC-based MatAutocomplete', () => {
12611262
.toBeFalsy('Expected panel to be removed.');
12621263
}));
12631264

1265+
it('should not close when a click event occurs on the outside while the panel has focus',
1266+
fakeAsync(() => {
1267+
const trigger = fixture.componentInstance.trigger;
1268+
1269+
input.focus();
1270+
flush();
1271+
fixture.detectChanges();
1272+
1273+
expect(document.activeElement).toBe(input, 'Expected input to be focused.');
1274+
expect(trigger.panelOpen).toBe(true, 'Expected panel to be open.');
1275+
1276+
dispatchMouseEvent(document.body, 'click');
1277+
fixture.detectChanges();
1278+
1279+
expect(document.activeElement).toBe(input, 'Expected input to continue to be focused.');
1280+
expect(trigger.panelOpen).toBe(true, 'Expected panel to stay open.');
1281+
}));
1282+
12641283
it('should reset the active option when closing with the escape key', fakeAsync(() => {
12651284
const trigger = fixture.componentInstance.trigger;
12661285

src/material/autocomplete/autocomplete-trigger.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,11 @@ export abstract class _MatAutocompleteTriggerBase implements ControlValueAccesso
336336
const customOrigin = this.connectedTo ? this.connectedTo.elementRef.nativeElement : null;
337337

338338
return this._overlayAttached && clickTarget !== this._element.nativeElement &&
339+
// Normally focus moves inside `mousedown` so this condition will almost always be
340+
// true. Its main purpose is to handle the case where the input is focused from an
341+
// outside click which propagates up to the `body` listener within the same sequence
342+
// and causes the panel to close immediately (see #3106).
343+
this._document.activeElement !== this._element.nativeElement &&
339344
(!formField || !formField.contains(clickTarget)) &&
340345
(!customOrigin || !customOrigin.contains(clickTarget)) &&
341346
(!!this._overlayRef && !this._overlayRef.overlayElement.contains(clickTarget));

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,
@@ -1250,6 +1251,24 @@ describe('MatAutocomplete', () => {
12501251
.toBeFalsy('Expected panel to be removed.');
12511252
}));
12521253

1254+
it('should not close when a click event occurs on the outside while the panel has focus',
1255+
fakeAsync(() => {
1256+
const trigger = fixture.componentInstance.trigger;
1257+
1258+
input.focus();
1259+
flush();
1260+
fixture.detectChanges();
1261+
1262+
expect(document.activeElement).toBe(input, 'Expected input to be focused.');
1263+
expect(trigger.panelOpen).toBe(true, 'Expected panel to be open.');
1264+
1265+
dispatchMouseEvent(document.body, 'click');
1266+
fixture.detectChanges();
1267+
1268+
expect(document.activeElement).toBe(input, 'Expected input to continue to be focused.');
1269+
expect(trigger.panelOpen).toBe(true, 'Expected panel to stay open.');
1270+
}));
1271+
12531272
it('should reset the active option when closing with the escape key', fakeAsync(() => {
12541273
const trigger = fixture.componentInstance.trigger;
12551274

0 commit comments

Comments
 (0)