Skip to content

Commit 216ae0c

Browse files
authored
fix(material/autocomplete): requireSelection sometimes not clearing value when editing after selection (#28628)
Fixes that if the user has `requireSelection` enabled, selects a value and then deletes a character while the input still has a value, the selection wasn't being cleared as expected. Fixes #28432.
1 parent 616d44c commit 216ae0c

File tree

2 files changed

+81
-19
lines changed

2 files changed

+81
-19
lines changed

src/material/autocomplete/autocomplete-trigger.ts

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@ export class MatAutocompleteTrigger
144144
/** Value of the input element when the panel was attached (even if there are no options). */
145145
private _valueOnAttach: string | number | null;
146146

147+
/** Value on the previous keydown event. */
148+
private _valueOnLastKeydown: string | null;
149+
147150
/** Strategy that is used to position the panel. */
148151
private _positionStrategy: FlexibleConnectedPositionStrategy;
149152

@@ -285,13 +288,7 @@ export class MatAutocompleteTrigger
285288

286289
/** Opens the autocomplete suggestion panel. */
287290
openPanel(): void {
288-
this._attachOverlay();
289-
this._floatLabel();
290-
// Add aria-owns attribute when the autocomplete becomes visible.
291-
if (this._trackedModal) {
292-
const panelId = this.autocomplete.id;
293-
addAriaReferencedId(this._trackedModal, 'aria-owns', panelId);
294-
}
291+
this._openPanelInternal();
295292
}
296293

297294
/** Closes the autocomplete suggestion panel. */
@@ -461,6 +458,8 @@ export class MatAutocompleteTrigger
461458
event.preventDefault();
462459
}
463460

461+
this._valueOnLastKeydown = this._element.nativeElement.value;
462+
464463
if (this.activeOption && keyCode === ENTER && this.panelOpen && !hasModifier) {
465464
this.activeOption._selectViaInteraction();
466465
this._resetActiveItem();
@@ -472,15 +471,15 @@ export class MatAutocompleteTrigger
472471
if (keyCode === TAB || (isArrowKey && !hasModifier && this.panelOpen)) {
473472
this.autocomplete._keyManager.onKeydown(event);
474473
} else if (isArrowKey && this._canOpen()) {
475-
this.openPanel();
474+
this._openPanelInternal(this._valueOnLastKeydown);
476475
}
477476

478477
if (isArrowKey || this.autocomplete._keyManager.activeItem !== prevActiveItem) {
479478
this._scrollToOption(this.autocomplete._keyManager.activeItemIndex || 0);
480479

481480
if (this.autocomplete.autoSelectActiveOption && this.activeOption) {
482481
if (!this._pendingAutoselectedOption) {
483-
this._valueBeforeAutoSelection = this._element.nativeElement.value;
482+
this._valueBeforeAutoSelection = this._valueOnLastKeydown;
484483
}
485484

486485
this._pendingAutoselectedOption = this.activeOption;
@@ -523,7 +522,7 @@ export class MatAutocompleteTrigger
523522
const selectedOption = this.autocomplete.options?.find(option => option.selected);
524523

525524
if (selectedOption) {
526-
const display = this.autocomplete.displayWith?.(selectedOption) ?? selectedOption.value;
525+
const display = this._getDisplayValue(selectedOption.value);
527526

528527
if (value !== display) {
529528
selectedOption.deselect(false);
@@ -532,7 +531,14 @@ export class MatAutocompleteTrigger
532531
}
533532

534533
if (this._canOpen() && this._document.activeElement === event.target) {
535-
this.openPanel();
534+
// When the `input` event fires, the input's value will have already changed. This means
535+
// that if we take the `this._element.nativeElement.value` directly, it'll be one keystroke
536+
// behind. This can be a problem when the user selects a value, changes a character while
537+
// the input still has focus and then clicks away (see #28432). To work around it, we
538+
// capture the value in `keydown` so we can use it here.
539+
const valueOnAttach = this._valueOnLastKeydown ?? this._element.nativeElement.value;
540+
this._valueOnLastKeydown = null;
541+
this._openPanelInternal(valueOnAttach);
536542
}
537543
}
538544
}
@@ -542,14 +548,14 @@ export class MatAutocompleteTrigger
542548
this._canOpenOnNextFocus = true;
543549
} else if (this._canOpen()) {
544550
this._previousValue = this._element.nativeElement.value;
545-
this._attachOverlay();
551+
this._attachOverlay(this._previousValue);
546552
this._floatLabel(true);
547553
}
548554
}
549555

550556
_handleClick(): void {
551557
if (this._canOpen() && !this.panelOpen) {
552-
this.openPanel();
558+
this._openPanelInternal();
553559
}
554560
}
555561

@@ -657,11 +663,14 @@ export class MatAutocompleteTrigger
657663
}
658664
}
659665

666+
/** Given a value, returns the string that should be shown within the input. */
667+
private _getDisplayValue<T>(value: T): T | string {
668+
const autocomplete = this.autocomplete;
669+
return autocomplete && autocomplete.displayWith ? autocomplete.displayWith(value) : value;
670+
}
671+
660672
private _assignOptionValue(value: any): void {
661-
const toDisplay =
662-
this.autocomplete && this.autocomplete.displayWith
663-
? this.autocomplete.displayWith(value)
664-
: value;
673+
const toDisplay = this._getDisplayValue(value);
665674

666675
if (value == null) {
667676
this._clearPreviousSelectedOption(null, false);
@@ -733,7 +742,17 @@ export class MatAutocompleteTrigger
733742
});
734743
}
735744

736-
private _attachOverlay(): void {
745+
private _openPanelInternal(valueOnAttach = this._element.nativeElement.value) {
746+
this._attachOverlay(valueOnAttach);
747+
this._floatLabel();
748+
// Add aria-owns attribute when the autocomplete becomes visible.
749+
if (this._trackedModal) {
750+
const panelId = this.autocomplete.id;
751+
addAriaReferencedId(this._trackedModal, 'aria-owns', panelId);
752+
}
753+
}
754+
755+
private _attachOverlay(valueOnAttach: string): void {
737756
if (!this.autocomplete && (typeof ngDevMode === 'undefined' || ngDevMode)) {
738757
throw getMatAutocompleteMissingPanelError();
739758
}
@@ -759,7 +778,8 @@ export class MatAutocompleteTrigger
759778

760779
if (overlayRef && !overlayRef.hasAttached()) {
761780
overlayRef.attach(this._portal);
762-
this._valueOnAttach = this._element.nativeElement.value;
781+
this._valueOnAttach = valueOnAttach;
782+
this._valueOnLastKeydown = null;
763783
this._closingActionsSubscription = this._subscribeToClosingActions();
764784
}
765785

src/material/autocomplete/autocomplete.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2780,6 +2780,48 @@ describe('MDC-based MatAutocomplete', () => {
27802780
expect(spy).not.toHaveBeenCalled();
27812781
subscription.unsubscribe();
27822782
}));
2783+
2784+
it('should clear the value if requireSelection is enabled and the user edits the input before clicking away', fakeAsync(() => {
2785+
const input = fixture.nativeElement.querySelector('input');
2786+
const {stateCtrl, trigger} = fixture.componentInstance;
2787+
fixture.componentInstance.requireSelection = true;
2788+
fixture.detectChanges();
2789+
tick();
2790+
2791+
// Simulate opening the input and clicking the first option.
2792+
trigger.openPanel();
2793+
fixture.detectChanges();
2794+
zone.simulateZoneExit();
2795+
(overlayContainerElement.querySelector('mat-option') as HTMLElement).click();
2796+
tick();
2797+
fixture.detectChanges();
2798+
2799+
expect(trigger.panelOpen).toBe(false);
2800+
expect(input.value).toBe('Alabama');
2801+
expect(stateCtrl.value).toEqual({code: 'AL', name: 'Alabama'});
2802+
2803+
// Simulate pressing backspace while focus is still on the input.
2804+
dispatchFakeEvent(input, 'keydown');
2805+
input.value = 'Alabam';
2806+
fixture.detectChanges();
2807+
dispatchFakeEvent(input, 'input');
2808+
fixture.detectChanges();
2809+
zone.simulateZoneExit();
2810+
2811+
expect(trigger.panelOpen).toBe(true);
2812+
expect(input.value).toBe('Alabam');
2813+
expect(stateCtrl.value).toEqual({code: 'AL', name: 'Alabama'});
2814+
2815+
// Simulate clicking away.
2816+
input.blur();
2817+
dispatchFakeEvent(document, 'click');
2818+
fixture.detectChanges();
2819+
tick();
2820+
2821+
expect(trigger.panelOpen).toBe(false);
2822+
expect(input.value).toBe('');
2823+
expect(stateCtrl.value).toBe(null);
2824+
}));
27832825
});
27842826

27852827
describe('panel closing', () => {

0 commit comments

Comments
 (0)