diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index 7be99c44f356..21fe3363ec78 100644 --- a/src/lib/autocomplete/autocomplete-trigger.ts +++ b/src/lib/autocomplete/autocomplete-trigger.ts @@ -224,6 +224,16 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { } } + /** + * Updates the position of the autocomplete suggestion panel to ensure that it fits all options + * within the viewport. + */ + updatePosition(): void { + if (this._overlayAttached) { + this._overlayRef!.updatePosition(); + } + } + /** * A stream of actions that should close the autocomplete panel, including * when an option is selected, on blur, and when TAB is pressed. diff --git a/src/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts index 7d1240b602f4..84ed0cc86538 100644 --- a/src/lib/autocomplete/autocomplete.spec.ts +++ b/src/lib/autocomplete/autocomplete.spec.ts @@ -1478,6 +1478,50 @@ describe('MatAutocomplete', () => { .toEqual(Math.floor(panelBottom), `Expected panel to stay aligned after filtering.`); })); + it('should fall back to above position when requested if options are added while ' + + 'the panel is open', fakeAsync(() => { + let fixture = createComponent(AutocompleteWithOnPushDelay); + fixture.detectChanges(); + + let inputEl = fixture.debugElement.query(By.css('input')).nativeElement; + let inputReference = fixture.debugElement.query(By.css('.mat-form-field-flex')).nativeElement; + + // Push the element down so it has a little bit of space, but not enough to render. + inputReference.style.bottom = '10px'; + inputReference.style.position = 'fixed'; + + // Focus the input to load the deferred options. + dispatchFakeEvent(inputEl, 'focusin'); + tick(1000); + + fixture.detectChanges(); + tick(); + + const inputBottom = inputReference.getBoundingClientRect().bottom; + const panel = overlayContainerElement.querySelector('.mat-autocomplete-panel')!; + const panelTop = panel.getBoundingClientRect().top; + + expect(Math.floor(inputBottom)) + .toEqual(Math.floor(panelTop), + `Expected panel top to be below input before repositioning.`); + + // Request a position update now that there are too many suggestions to fit in the viewport. + fixture.componentInstance.trigger.updatePosition(); + + const inputTop = inputReference.getBoundingClientRect().top; + const panelBottom = panel.getBoundingClientRect().bottom; + + expect(Math.floor(inputTop)) + .toEqual(Math.floor(panelBottom), + `Expected panel to fall back to above position after repositioning.`); + })); + + it('should not throw if a panel reposition is requested while the panel is closed', () => { + let fixture = createComponent(SimpleAutocomplete); + fixture.detectChanges(); + + expect(() => fixture.componentInstance.trigger.updatePosition()).not.toThrow(); + }); }); describe('Option selection', () => { @@ -2198,6 +2242,7 @@ class AutocompleteWithNumbers { ` }) class AutocompleteWithOnPushDelay implements OnInit { + @ViewChild(MatAutocompleteTrigger) trigger: MatAutocompleteTrigger; options: string[]; ngOnInit() {