Skip to content

Commit

Permalink
fix(material/autocomplete): requireSelection incorrectly resetting va…
Browse files Browse the repository at this point in the history
…lue when there are no options (#27781)

The autocomplete has a check not to reset the value if the user didn't interact with the input. The problem was that we only accounted for it when there are options, because while technically an autocomplete is _attached_ when the user focuses, we don't consider it _open_ until it shows some options.

Fixes #27767.

(cherry picked from commit db06fa8)
  • Loading branch information
crisbeto committed Sep 12, 2023
1 parent 0fe05b3 commit 38ebf49
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 5 deletions.
18 changes: 13 additions & 5 deletions src/material/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ export abstract class _MatAutocompleteTriggerBase
/** Old value of the native input. Used to work around issues with the `input` event on IE. */
private _previousValue: string | number | null;

/** Value of the input element when the panel was opened. */
private _valueOnOpen: string | number | null;
/** Value of the input element when the panel was attached (even if there are no options). */
private _valueOnAttach: string | number | null;

/** Strategy that is used to position the panel. */
private _positionStrategy: FlexibleConnectedPositionStrategy;
Expand Down Expand Up @@ -574,6 +574,7 @@ export abstract class _MatAutocompleteTriggerBase
// of the available options,
// - if a valid string is entered after an invalid one.
if (this.panelOpen) {
this._captureValueOnAttach();
this._emitOpened();
} else {
this.autocomplete.closed.emit();
Expand All @@ -596,10 +597,14 @@ export abstract class _MatAutocompleteTriggerBase
* the state of the trigger right before the opening sequence was finished.
*/
private _emitOpened() {
this._valueOnOpen = this._element.nativeElement.value;
this.autocomplete.opened.emit();
}

/** Intended to be called when the panel is attached. Captures the current value of the input. */
private _captureValueOnAttach() {
this._valueOnAttach = this._element.nativeElement.value;
}

/** Destroys the autocomplete suggestion panel. */
private _destroyPanel(): void {
if (this._overlayRef) {
Expand Down Expand Up @@ -650,7 +655,10 @@ export abstract class _MatAutocompleteTriggerBase
this._onChange(toSelect.value);
panel._emitSelectEvent(toSelect);
this._element.nativeElement.focus();
} else if (panel.requireSelection && this._element.nativeElement.value !== this._valueOnOpen) {
} else if (
panel.requireSelection &&
this._element.nativeElement.value !== this._valueOnAttach
) {
this._clearPreviousSelectedOption(null);
this._assignOptionValue(null);
// Wait for the animation to finish before clearing the form control value, otherwise
Expand Down Expand Up @@ -712,8 +720,8 @@ export abstract class _MatAutocompleteTriggerBase
this.autocomplete._isOpen = this._overlayAttached = true;
this.autocomplete._setColor(this._formField?.color);
this._updatePanelState();

this._applyModalPanelOwnership();
this._captureValueOnAttach();

// We need to do an extra `panelOpen` check in here, because the
// autocomplete won't be shown if there are no options.
Expand Down
31 changes: 31 additions & 0 deletions src/material/autocomplete/autocomplete.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2663,6 +2663,37 @@ describe('MDC-based MatAutocomplete', () => {
expect(spy).not.toHaveBeenCalled();
subscription.unsubscribe();
}));

it('should preserve the value if a selection is required, and there are no options', fakeAsync(() => {
const input = fixture.nativeElement.querySelector('input');
const {stateCtrl, trigger, states} = fixture.componentInstance;
fixture.componentInstance.requireSelection = true;
stateCtrl.setValue(states[1]);
fixture.detectChanges();
tick();

expect(input.value).toBe('California');
expect(stateCtrl.value).toEqual({code: 'CA', name: 'California'});

fixture.componentInstance.states = fixture.componentInstance.filteredStates = [];
fixture.detectChanges();

trigger.openPanel();
fixture.detectChanges();
zone.simulateZoneExit();

const spy = jasmine.createSpy('optionSelected spy');
const subscription = trigger.optionSelections.subscribe(spy);

dispatchFakeEvent(document, 'click');
fixture.detectChanges();
tick();

expect(input.value).toBe('California');
expect(stateCtrl.value).toEqual({code: 'CA', name: 'California'});
expect(spy).not.toHaveBeenCalled();
subscription.unsubscribe();
}));
});

describe('panel closing', () => {
Expand Down

0 comments on commit 38ebf49

Please sign in to comment.