Skip to content

Commit

Permalink
fix(material/autocomplete): closing immediately when input is focused…
Browse files Browse the repository at this point in the history
… 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.
  • Loading branch information
crisbeto committed Feb 22, 2022
1 parent b510973 commit c0ed5ce
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 1 deletion.
19 changes: 19 additions & 0 deletions src/material-experimental/mdc-autocomplete/autocomplete.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
dispatchEvent,
dispatchFakeEvent,
dispatchKeyboardEvent,
dispatchMouseEvent,
MockNgZone,
typeInElement,
} from '../../cdk/testing/private';
Expand Down Expand Up @@ -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;

Expand Down
5 changes: 5 additions & 0 deletions src/material/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand Down
21 changes: 20 additions & 1 deletion src/material/autocomplete/autocomplete.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
dispatchFakeEvent,
dispatchKeyboardEvent,
typeInElement,
} from '../../cdk/testing/private';
dispatchMouseEvent,
} from '@angular/cdk/testing/private';
import {
ChangeDetectionStrategy,
Component,
Expand Down Expand Up @@ -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;

Expand Down

0 comments on commit c0ed5ce

Please sign in to comment.