From 81a6f8dbd8b01d4d9e70154c7fd8402c97ef8cfe Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 21 Apr 2017 18:14:19 +0200 Subject: [PATCH] fix(autocomplete): reposition panel on scroll (#3745) --- src/lib/autocomplete/autocomplete-trigger.ts | 17 ++++++++++ src/lib/autocomplete/autocomplete.spec.ts | 35 +++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index 39876a48534a..d7007cc6e91c 100644 --- a/src/lib/autocomplete/autocomplete-trigger.ts +++ b/src/lib/autocomplete/autocomplete-trigger.ts @@ -22,6 +22,7 @@ import {MdOptionSelectionChange, MdOption} from '../core/option/option'; import {ENTER, UP_ARROW, DOWN_ARROW} from '../core/keyboard/keycodes'; import {Dir} from '../core/rtl/dir'; import {MdInputContainer} from '../input/input-container'; +import {ScrollDispatcher} from '../core/overlay/scroll/scroll-dispatcher'; import {Subscription} from 'rxjs/Subscription'; import 'rxjs/add/observable/merge'; import 'rxjs/add/observable/fromEvent'; @@ -76,6 +77,10 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy { /** The subscription to positioning changes in the autocomplete panel. */ private _panelPositionSubscription: Subscription; + /** Subscription to global scroll events. */ + private _scrollSubscription: Subscription; + + /** Strategy that is used to position the panel. */ private _positionStrategy: ConnectedPositionStrategy; /** Whether or not the placeholder state is being overridden. */ @@ -103,6 +108,7 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy { constructor(private _element: ElementRef, private _overlay: Overlay, private _viewContainerRef: ViewContainerRef, private _changeDetectorRef: ChangeDetectorRef, + private _scrollDispatcher: ScrollDispatcher, @Optional() private _dir: Dir, private _zone: NgZone, @Optional() @Host() private _inputContainer: MdInputContainer, @Optional() @Inject(DOCUMENT) private _document: any) {} @@ -134,6 +140,12 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy { this._subscribeToClosingActions(); } + if (!this._scrollSubscription) { + this._scrollSubscription = this._scrollDispatcher.scrolled(0, () => { + this._overlayRef.updatePosition(); + }); + } + this.autocomplete._setVisibility(); this._floatPlaceholder(); this._panelOpen = true; @@ -145,6 +157,11 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy { this._overlayRef.detach(); } + if (this._scrollSubscription) { + this._scrollSubscription.unsubscribe(); + this._scrollSubscription = null; + } + this._panelOpen = false; this._resetPlaceholder(); diff --git a/src/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts index 203a29f53394..78ccfe632da9 100644 --- a/src/lib/autocomplete/autocomplete.spec.ts +++ b/src/lib/autocomplete/autocomplete.spec.ts @@ -23,14 +23,17 @@ import {FakeViewportRuler} from '../core/overlay/position/fake-viewport-ruler'; import {MdAutocomplete} from './autocomplete'; import {MdInputContainer} from '../input/input-container'; import {Observable} from 'rxjs/Observable'; +import {Subject} from 'rxjs/Subject'; import {dispatchFakeEvent} from '../core/testing/dispatch-events'; import {typeInElement} from '../core/testing/type-in-element'; +import {ScrollDispatcher} from '../core/overlay/scroll/scroll-dispatcher'; import 'rxjs/add/operator/map'; describe('MdAutocomplete', () => { let overlayContainerElement: HTMLElement; let dir: LayoutDirection; + let scrolledSubject = new Subject(); beforeEach(async(() => { dir = 'ltr'; @@ -52,6 +55,8 @@ describe('MdAutocomplete', () => { providers: [ {provide: OverlayContainer, useFactory: () => { overlayContainerElement = document.createElement('div'); + overlayContainerElement.classList.add('cdk-overlay-container'); + document.body.appendChild(overlayContainerElement); // remove body padding to keep consistent cross-browser @@ -63,7 +68,12 @@ describe('MdAutocomplete', () => { {provide: Dir, useFactory: () => { return {value: dir}; }}, - {provide: ViewportRuler, useClass: FakeViewportRuler} + {provide: ViewportRuler, useClass: FakeViewportRuler}, + {provide: ScrollDispatcher, useFactory: () => { + return {scrolled: (delay: number, callback: () => any) => { + return scrolledSubject.asObservable().subscribe(callback); + }}; + }} ] }); @@ -925,6 +935,29 @@ describe('MdAutocomplete', () => { .toEqual('below', `Expected autocomplete positionY to default to below.`); }); + it('should reposition the panel on scroll', () => { + const spacer = document.createElement('div'); + + spacer.style.height = '1000px'; + document.body.appendChild(spacer); + + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + + window.scroll(0, 100); + scrolledSubject.next(); + fixture.detectChanges(); + + const inputBottom = input.getBoundingClientRect().bottom; + const panel = overlayContainerElement.querySelector('.mat-autocomplete-panel'); + const panelTop = panel.getBoundingClientRect().top; + + expect((inputBottom + 6).toFixed(1)).toEqual(panelTop.toFixed(1), + 'Expected panel top to match input bottom after scrolling.'); + + document.body.removeChild(spacer); + }); + it('should fall back to above position if panel cannot fit below', () => { // Push the autocomplete trigger down so it won't have room to open "below" input.style.top = '600px';