Skip to content

Commit c2b488e

Browse files
crisbetojelbourn
authored andcommitted
fix(autocomplete): reopening closed autocomplete when coming back to tab (#12372)
Fixes a closed autocomplete being reopened, if the user moves to another tab and coming back to the current one, while the input is still focused. Fixes #12337.
1 parent 2830a64 commit c2b488e

File tree

2 files changed

+64
-3
lines changed

2 files changed

+64
-3
lines changed

src/lib/autocomplete/autocomplete-trigger.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,28 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
136136
/** Subscription to viewport size changes. */
137137
private _viewportSubscription = Subscription.EMPTY;
138138

139+
/**
140+
* Whether the autocomplete can open the next time it is focused. Used to prevent a focused,
141+
* closed autocomplete from being reopened if the user switches to another browser tab and then
142+
* comes back.
143+
*/
144+
private _canOpenOnNextFocus = true;
145+
139146
/** Stream of keyboard events that can close the panel. */
140147
private readonly _closeKeyEventStream = new Subject<void>();
141148

149+
/**
150+
* Event handler for when the window is blurred. Needs to be an
151+
* arrow function in order to preserve the context.
152+
*/
153+
private _windowBlurHandler = () => {
154+
// If the user blurred the window while the autocomplete is focused, it means that it'll be
155+
// refocused when they come back. In this case we want to skip the first focus event, if the
156+
// pane was closed, in order to avoid reopening it unintentionally.
157+
this._canOpenOnNextFocus =
158+
document.activeElement !== this._element.nativeElement || this.panelOpen;
159+
}
160+
142161
/** `View -> model callback called when value changes` */
143162
_onChange: (value: any) => void = () => {};
144163

@@ -179,9 +198,20 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
179198
@Optional() @Host() private _formField: MatFormField,
180199
@Optional() @Inject(DOCUMENT) private _document: any,
181200
// @breaking-change 7.0.0 Make `_viewportRuler` required.
182-
private _viewportRuler?: ViewportRuler) {}
201+
private _viewportRuler?: ViewportRuler) {
202+
203+
if (typeof window !== 'undefined') {
204+
_zone.runOutsideAngular(() => {
205+
window.addEventListener('blur', this._windowBlurHandler);
206+
});
207+
}
208+
}
183209

184210
ngOnDestroy() {
211+
if (typeof window !== 'undefined') {
212+
window.removeEventListener('blur', this._windowBlurHandler);
213+
}
214+
185215
this._viewportSubscription.unsubscribe();
186216
this._componentDestroyed = true;
187217
this._destroyPanel();
@@ -386,7 +416,9 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
386416
}
387417

388418
_handleFocus(): void {
389-
if (this._canOpen()) {
419+
if (!this._canOpenOnNextFocus) {
420+
this._canOpenOnNextFocus = true;
421+
} else if (this._canOpen()) {
390422
this._previousValue = this._element.nativeElement.value;
391423
this._attachOverlay();
392424
this._floatLabel(true);
@@ -633,5 +665,4 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
633665
const element: HTMLInputElement = this._element.nativeElement;
634666
return !element.readOnly && !element.disabled && !this._autocompleteDisabled;
635667
}
636-
637668
}

src/lib/autocomplete/autocomplete.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2017,6 +2017,36 @@ describe('MatAutocomplete', () => {
20172017
expect(Math.ceil(parseFloat(overlayPane.style.width as string))).toBe(500);
20182018
});
20192019

2020+
it('should not reopen a closed autocomplete when returning to a blurred tab', () => {
2021+
const fixture = createComponent(SimpleAutocomplete);
2022+
fixture.detectChanges();
2023+
2024+
const trigger = fixture.componentInstance.trigger;
2025+
const input = fixture.debugElement.query(By.css('input')).nativeElement;
2026+
2027+
input.focus();
2028+
fixture.detectChanges();
2029+
2030+
expect(trigger.panelOpen).toBe(true, 'Expected panel to be open.');
2031+
2032+
trigger.closePanel();
2033+
fixture.detectChanges();
2034+
2035+
expect(trigger.panelOpen).toBe(false, 'Expected panel to be closed.');
2036+
2037+
// Simulate the user going to a different tab.
2038+
dispatchFakeEvent(window, 'blur');
2039+
input.blur();
2040+
fixture.detectChanges();
2041+
2042+
// Simulate the user coming back.
2043+
dispatchFakeEvent(window, 'focus');
2044+
input.focus();
2045+
fixture.detectChanges();
2046+
2047+
expect(trigger.panelOpen).toBe(false, 'Expected panel to remain closed.');
2048+
});
2049+
20202050
it('should update the panel width if the window is resized', fakeAsync(() => {
20212051
const widthFixture = createComponent(SimpleAutocomplete);
20222052

0 commit comments

Comments
 (0)