Skip to content

Commit 93cb86f

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 4a60005 commit 93cb86f

File tree

3 files changed

+43
-0
lines changed

3 files changed

+43
-0
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 '@angular/cdk/testing/private';
@@ -1241,6 +1242,24 @@ describe('MDC-based MatAutocomplete', () => {
12411242
.toBeFalsy('Expected panel to be removed.');
12421243
}));
12431244

1245+
it('should not close when a click event occurs on the outside while the panel has focus',
1246+
fakeAsync(() => {
1247+
const trigger = fixture.componentInstance.trigger;
1248+
1249+
input.focus();
1250+
flush();
1251+
fixture.detectChanges();
1252+
1253+
expect(document.activeElement).toBe(input, 'Expected input to be focused.');
1254+
expect(trigger.panelOpen).toBe(true, 'Expected panel to be open.');
1255+
1256+
dispatchMouseEvent(document.body, 'click');
1257+
fixture.detectChanges();
1258+
1259+
expect(document.activeElement).toBe(input, 'Expected input to continue to be focused.');
1260+
expect(trigger.panelOpen).toBe(true, 'Expected panel to stay open.');
1261+
}));
1262+
12441263
it('should reset the active option when closing with the escape key', fakeAsync(() => {
12451264
const trigger = fixture.componentInstance.trigger;
12461265

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

+5
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,11 @@ export abstract class _MatAutocompleteTriggerBase implements ControlValueAccesso
360360
const customOrigin = this.connectedTo ? this.connectedTo.elementRef.nativeElement : null;
361361

362362
return this._overlayAttached && clickTarget !== this._element.nativeElement &&
363+
// Normally focus moves inside `mousedown` so this condition will almost always be
364+
// true. Its main purpose is to handle the case where the input is focused from an
365+
// outside click which propagates up to the `body` listener within the same sequence
366+
// and causes the panel to close immediately (see #3106).
367+
this._document.activeElement !== this._element.nativeElement &&
363368
(!formField || !formField.contains(clickTarget)) &&
364369
(!customOrigin || !customOrigin.contains(clickTarget)) &&
365370
(!!this._overlayRef && !this._overlayRef.overlayElement.contains(clickTarget));

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

+19
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
dispatchFakeEvent,
1212
dispatchKeyboardEvent,
1313
typeInElement,
14+
dispatchMouseEvent,
1415
} from '@angular/cdk/testing/private';
1516
import {
1617
ChangeDetectionStrategy,
@@ -1238,6 +1239,24 @@ describe('MatAutocomplete', () => {
12381239
.toBeFalsy('Expected panel to be removed.');
12391240
}));
12401241

1242+
it('should not close when a click event occurs on the outside while the panel has focus',
1243+
fakeAsync(() => {
1244+
const trigger = fixture.componentInstance.trigger;
1245+
1246+
input.focus();
1247+
flush();
1248+
fixture.detectChanges();
1249+
1250+
expect(document.activeElement).toBe(input, 'Expected input to be focused.');
1251+
expect(trigger.panelOpen).toBe(true, 'Expected panel to be open.');
1252+
1253+
dispatchMouseEvent(document.body, 'click');
1254+
fixture.detectChanges();
1255+
1256+
expect(document.activeElement).toBe(input, 'Expected input to continue to be focused.');
1257+
expect(trigger.panelOpen).toBe(true, 'Expected panel to stay open.');
1258+
}));
1259+
12411260
it('should reset the active option when closing with the escape key', fakeAsync(() => {
12421261
const trigger = fixture.componentInstance.trigger;
12431262

0 commit comments

Comments
 (0)