From cf868a5089a4c9eaddd4a55fd18b952fb96d5c19 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 17 Nov 2023 18:50:57 +0100 Subject: [PATCH] fix(material/autocomplete): clear selected option if it is removed while typing (#28146) Based on an internal bug report. Clears the selected option in the autocomplete if the user types in something else. --- .../autocomplete/autocomplete-trigger.ts | 22 +++++-- .../autocomplete/autocomplete.spec.ts | 59 +++++++++++++++++++ 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/src/material/autocomplete/autocomplete-trigger.ts b/src/material/autocomplete/autocomplete-trigger.ts index e797f9a9f187..c4f8c972ea28 100644 --- a/src/material/autocomplete/autocomplete-trigger.ts +++ b/src/material/autocomplete/autocomplete-trigger.ts @@ -506,6 +506,18 @@ export class MatAutocompleteTrigger if (!value) { this._clearPreviousSelectedOption(null, false); + } else if (this.panelOpen && !this.autocomplete.requireSelection) { + // Note that we don't reset this when `requireSelection` is enabled, + // because the option will be reset when the panel is closed. + const selectedOption = this.autocomplete.options?.find(option => option.selected); + + if (selectedOption) { + const display = this.autocomplete.displayWith?.(selectedOption) ?? selectedOption.value; + + if (value !== display) { + selectedOption.deselect(false); + } + } } if (this._canOpen() && this._document.activeElement === event.target) { @@ -640,18 +652,16 @@ export class MatAutocompleteTrigger ? this.autocomplete.displayWith(value) : value; + if (value == null) { + this._clearPreviousSelectedOption(null, false); + } + // Simply falling back to an empty string if the display value is falsy does not work properly. // The display value can also be the number zero and shouldn't fall back to an empty string. this._updateNativeInputValue(toDisplay != null ? toDisplay : ''); } private _updateNativeInputValue(value: string): void { - // We want to clear the previous selection if our new value is falsy. e.g: reactive form field - // being reset. - if (!value) { - this._clearPreviousSelectedOption(null, false); - } - // If it's used within a `MatFormField`, we should set it through the property so it can go // through change detection. if (this._formField) { diff --git a/src/material/autocomplete/autocomplete.spec.ts b/src/material/autocomplete/autocomplete.spec.ts index 95986a4abffc..c81c549ab7f3 100644 --- a/src/material/autocomplete/autocomplete.spec.ts +++ b/src/material/autocomplete/autocomplete.spec.ts @@ -705,6 +705,65 @@ describe('MDC-based MatAutocomplete', () => { }).not.toThrow(); }); + it('should clear the selected option if it no longer matches the input text while typing', fakeAsync(() => { + const fixture = createComponent(SimpleAutocomplete); + fixture.detectChanges(); + tick(); + + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + zone.simulateZoneExit(); + + // Select an option and reopen the panel. + (overlayContainerElement.querySelector('mat-option') as HTMLElement).click(); + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + zone.simulateZoneExit(); + + expect(fixture.componentInstance.options.first.selected).toBe(true); + + const input = fixture.debugElement.query(By.css('input'))!.nativeElement; + input.value = ''; + typeInElement(input, 'Ala'); + fixture.detectChanges(); + tick(); + + expect(fixture.componentInstance.options.first.selected).toBe(false); + })); + + it('should not clear the selected option if it no longer matches the input text while typing with requireSelection', fakeAsync(() => { + const fixture = createComponent(SimpleAutocomplete); + fixture.componentInstance.requireSelection = true; + fixture.detectChanges(); + tick(); + + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + zone.simulateZoneExit(); + + // Select an option and reopen the panel. + (overlayContainerElement.querySelector('mat-option') as HTMLElement).click(); + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + zone.simulateZoneExit(); + + expect(fixture.componentInstance.options.first.selected).toBe(true); + + const input = fixture.debugElement.query(By.css('input'))!.nativeElement; + input.value = ''; + typeInElement(input, 'Ala'); + fixture.detectChanges(); + tick(); + + expect(fixture.componentInstance.options.first.selected).toBe(true); + })); + describe('forms integration', () => { let fixture: ComponentFixture; let input: HTMLInputElement;