From 01209a2818cbe2cca607bb487f8b943a3a880221 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Sun, 13 Mar 2022 12:22:10 +0100 Subject: [PATCH] fix(material/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. --- .../mdc-autocomplete/autocomplete.spec.ts | 27 +++++++++++++++++++ .../autocomplete/autocomplete-trigger.ts | 3 ++- .../autocomplete/autocomplete.spec.ts | 27 +++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts b/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts index 621e2d69855e..5448cbd10c57 100644 --- a/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts +++ b/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts @@ -2256,6 +2256,32 @@ describe('MDC-based MatAutocomplete', () => { componentOptions.slice(1).forEach(option => expect(option.deselect).not.toHaveBeenCalled()); })); + it('should not reset the active item if the options list changes while open', fakeAsync(() => { + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + zone.simulateZoneExit(); + fixture.detectChanges(); + + const DOWN_ARROW_EVENT = createKeyboardEvent('keydown', DOWN_ARROW); + fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); + fixture.detectChanges(); + tick(); + + const classList = overlayContainerElement.querySelector('mat-option')!.classList; + expect(classList) + .withContext('Expected first option to be highlighted.') + .toContain('mat-mdc-option-active'); + + fixture.componentInstance.states.push({code: 'PR', name: 'Puerto Rico'}); + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + + expect(classList) + .withContext('Expected first option to stay highlighted.') + .toContain('mat-mdc-option-active'); + })); + it('should be able to preselect the first option', fakeAsync(() => { fixture.componentInstance.trigger.autocomplete.autoActiveFirstOption = true; fixture.componentInstance.trigger.openPanel(); @@ -2276,6 +2302,7 @@ describe('MDC-based MatAutocomplete', () => { testComponent.trigger.autocomplete.autoActiveFirstOption = true; testComponent.states[0].disabled = true; testComponent.states[1].disabled = true; + fixture.detectChanges(); testComponent.trigger.openPanel(); fixture.detectChanges(); zone.simulateZoneExit(); diff --git a/src/material/autocomplete/autocomplete-trigger.ts b/src/material/autocomplete/autocomplete-trigger.ts index b9312e8d2aec..bca3b0371524 100644 --- a/src/material/autocomplete/autocomplete-trigger.ts +++ b/src/material/autocomplete/autocomplete-trigger.ts @@ -527,7 +527,6 @@ export abstract class _MatAutocompleteTriggerBase // that were created, and flatten it so our stream only emits closing events... switchMap(() => { const wasOpen = this.panelOpen; - this._resetActiveItem(); this.autocomplete._setVisibility(); this._changeDetectorRef.detectChanges(); @@ -539,6 +538,7 @@ export abstract class _MatAutocompleteTriggerBase // can happen if the users opens the panel and there are no options, but the // options come in slightly later or as a result of the value changing. if (wasOpen !== this.panelOpen) { + this._resetActiveItem(); this.autocomplete.opened.emit(); } } @@ -653,6 +653,7 @@ export abstract class _MatAutocompleteTriggerBase // We need to do an extra `panelOpen` check in here, because the // autocomplete won't be shown if there are no options. if (this.panelOpen && wasOpen !== this.panelOpen) { + this._resetActiveItem(); this.autocomplete.opened.emit(); } } diff --git a/src/material/autocomplete/autocomplete.spec.ts b/src/material/autocomplete/autocomplete.spec.ts index ed41c3acbceb..fba6246ccf4c 100644 --- a/src/material/autocomplete/autocomplete.spec.ts +++ b/src/material/autocomplete/autocomplete.spec.ts @@ -2241,6 +2241,32 @@ describe('MatAutocomplete', () => { componentOptions.slice(1).forEach(option => expect(option.deselect).not.toHaveBeenCalled()); })); + it('should not reset the active item if the options list changes while open', fakeAsync(() => { + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + zone.simulateZoneExit(); + fixture.detectChanges(); + + const DOWN_ARROW_EVENT = createKeyboardEvent('keydown', DOWN_ARROW); + fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); + fixture.detectChanges(); + tick(); + + const classList = overlayContainerElement.querySelector('mat-option')!.classList; + expect(classList) + .withContext('Expected first option to be highlighted.') + .toContain('mat-active'); + + fixture.componentInstance.states.push({code: 'PR', name: 'Puerto Rico'}); + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + + expect(classList) + .withContext('Expected first option to stay highlighted.') + .toContain('mat-active'); + })); + it('should be able to preselect the first option', fakeAsync(() => { fixture.componentInstance.trigger.autocomplete.autoActiveFirstOption = true; fixture.componentInstance.trigger.openPanel(); @@ -2261,6 +2287,7 @@ describe('MatAutocomplete', () => { testComponent.trigger.autocomplete.autoActiveFirstOption = true; testComponent.states[0].disabled = true; testComponent.states[1].disabled = true; + fixture.detectChanges(); testComponent.trigger.openPanel(); fixture.detectChanges(); zone.simulateZoneExit();