Skip to content

Commit f9294f0

Browse files
committed
fix(autocomplete): don't reset active option if list of options changes
Currently we reset the active option whenever the list of items changes, however this means that the user's selection could be lost while they're interacting, if some items get added to the end of the list out of view (e.g. if the options are fetched via polling). These changes address the issue by only resetting the active option when the panel is opened. Fixes #16608.
1 parent 9661530 commit f9294f0

File tree

2 files changed

+31
-1
lines changed

2 files changed

+31
-1
lines changed

src/material/autocomplete/autocomplete-trigger.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,9 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, AfterViewIn
155155
/** Whether the element is inside of a ShadowRoot component. */
156156
private _isInsideShadowRoot: boolean;
157157

158+
/** Whether the user has interacted with the panel after it was opened. */
159+
private _hasInteracted = false;
160+
158161
/** Stream of keyboard events that can close the panel. */
159162
private readonly _closeKeyEventStream = new Subject<void>();
160163

@@ -420,6 +423,7 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, AfterViewIn
420423
}
421424

422425
if (isArrowKey || this.autocomplete._keyManager.activeItem !== prevActiveItem) {
426+
this._hasInteracted = true;
423427
this._scrollToOption();
424428
}
425429
}
@@ -529,7 +533,9 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, AfterViewIn
529533
// that were created, and flatten it so our stream only emits closing events...
530534
switchMap(() => {
531535
const wasOpen = this.panelOpen;
532-
this._resetActiveItem();
536+
if (!this._hasInteracted) {
537+
this._resetActiveItem();
538+
}
533539
this.autocomplete._setVisibility();
534540

535541
if (this.panelOpen) {
@@ -652,13 +658,15 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, AfterViewIn
652658

653659
if (overlayRef && !overlayRef.hasAttached()) {
654660
overlayRef.attach(this._portal);
661+
this._resetActiveItem();
655662
this._closingActionsSubscription = this._subscribeToClosingActions();
656663
}
657664

658665
const wasOpen = this.panelOpen;
659666

660667
this.autocomplete._setVisibility();
661668
this.autocomplete._isOpen = this._overlayAttached = true;
669+
this._hasInteracted = false;
662670

663671
// We need to do an extra `panelOpen` check in here, because the
664672
// autocomplete won't be shown if there are no options.

src/material/autocomplete/autocomplete.spec.ts

+22
Original file line numberDiff line numberDiff line change
@@ -1807,6 +1807,28 @@ describe('MatAutocomplete', () => {
18071807
componentOptions.slice(1).forEach(option => expect(option.deselect).not.toHaveBeenCalled());
18081808
}));
18091809

1810+
it('should not reset the active item if the options list changes while open', fakeAsync(() => {
1811+
fixture.componentInstance.trigger.openPanel();
1812+
fixture.detectChanges();
1813+
zone.simulateZoneExit();
1814+
fixture.detectChanges();
1815+
1816+
const DOWN_ARROW_EVENT = createKeyboardEvent('keydown', DOWN_ARROW);
1817+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
1818+
fixture.detectChanges();
1819+
tick();
1820+
1821+
const classList = overlayContainerElement.querySelector('mat-option')!.classList;
1822+
expect(classList).toContain('mat-active', 'Expected first option to be highlighted.');
1823+
1824+
fixture.componentInstance.states.push({code: 'PR', name: 'Puerto Rico'});
1825+
fixture.detectChanges();
1826+
tick();
1827+
fixture.detectChanges();
1828+
1829+
expect(classList).toContain('mat-active', 'Expected first option to stay highlighted.');
1830+
}));
1831+
18101832
it('should be able to preselect the first option', fakeAsync(() => {
18111833
fixture.componentInstance.trigger.autocomplete.autoActiveFirstOption = true;
18121834
fixture.componentInstance.trigger.openPanel();

0 commit comments

Comments
 (0)