Skip to content

Commit 81a6f8d

Browse files
crisbetokara
authored andcommitted
fix(autocomplete): reposition panel on scroll (#3745)
1 parent 76faae5 commit 81a6f8d

File tree

2 files changed

+51
-1
lines changed

2 files changed

+51
-1
lines changed

src/lib/autocomplete/autocomplete-trigger.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {MdOptionSelectionChange, MdOption} from '../core/option/option';
2222
import {ENTER, UP_ARROW, DOWN_ARROW} from '../core/keyboard/keycodes';
2323
import {Dir} from '../core/rtl/dir';
2424
import {MdInputContainer} from '../input/input-container';
25+
import {ScrollDispatcher} from '../core/overlay/scroll/scroll-dispatcher';
2526
import {Subscription} from 'rxjs/Subscription';
2627
import 'rxjs/add/observable/merge';
2728
import 'rxjs/add/observable/fromEvent';
@@ -76,6 +77,10 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
7677
/** The subscription to positioning changes in the autocomplete panel. */
7778
private _panelPositionSubscription: Subscription;
7879

80+
/** Subscription to global scroll events. */
81+
private _scrollSubscription: Subscription;
82+
83+
/** Strategy that is used to position the panel. */
7984
private _positionStrategy: ConnectedPositionStrategy;
8085

8186
/** Whether or not the placeholder state is being overridden. */
@@ -103,6 +108,7 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
103108
constructor(private _element: ElementRef, private _overlay: Overlay,
104109
private _viewContainerRef: ViewContainerRef,
105110
private _changeDetectorRef: ChangeDetectorRef,
111+
private _scrollDispatcher: ScrollDispatcher,
106112
@Optional() private _dir: Dir, private _zone: NgZone,
107113
@Optional() @Host() private _inputContainer: MdInputContainer,
108114
@Optional() @Inject(DOCUMENT) private _document: any) {}
@@ -134,6 +140,12 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
134140
this._subscribeToClosingActions();
135141
}
136142

143+
if (!this._scrollSubscription) {
144+
this._scrollSubscription = this._scrollDispatcher.scrolled(0, () => {
145+
this._overlayRef.updatePosition();
146+
});
147+
}
148+
137149
this.autocomplete._setVisibility();
138150
this._floatPlaceholder();
139151
this._panelOpen = true;
@@ -145,6 +157,11 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
145157
this._overlayRef.detach();
146158
}
147159

160+
if (this._scrollSubscription) {
161+
this._scrollSubscription.unsubscribe();
162+
this._scrollSubscription = null;
163+
}
164+
148165
this._panelOpen = false;
149166
this._resetPlaceholder();
150167

src/lib/autocomplete/autocomplete.spec.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,17 @@ import {FakeViewportRuler} from '../core/overlay/position/fake-viewport-ruler';
2323
import {MdAutocomplete} from './autocomplete';
2424
import {MdInputContainer} from '../input/input-container';
2525
import {Observable} from 'rxjs/Observable';
26+
import {Subject} from 'rxjs/Subject';
2627
import {dispatchFakeEvent} from '../core/testing/dispatch-events';
2728
import {typeInElement} from '../core/testing/type-in-element';
29+
import {ScrollDispatcher} from '../core/overlay/scroll/scroll-dispatcher';
2830

2931
import 'rxjs/add/operator/map';
3032

3133
describe('MdAutocomplete', () => {
3234
let overlayContainerElement: HTMLElement;
3335
let dir: LayoutDirection;
36+
let scrolledSubject = new Subject();
3437

3538
beforeEach(async(() => {
3639
dir = 'ltr';
@@ -52,6 +55,8 @@ describe('MdAutocomplete', () => {
5255
providers: [
5356
{provide: OverlayContainer, useFactory: () => {
5457
overlayContainerElement = document.createElement('div');
58+
overlayContainerElement.classList.add('cdk-overlay-container');
59+
5560
document.body.appendChild(overlayContainerElement);
5661

5762
// remove body padding to keep consistent cross-browser
@@ -63,7 +68,12 @@ describe('MdAutocomplete', () => {
6368
{provide: Dir, useFactory: () => {
6469
return {value: dir};
6570
}},
66-
{provide: ViewportRuler, useClass: FakeViewportRuler}
71+
{provide: ViewportRuler, useClass: FakeViewportRuler},
72+
{provide: ScrollDispatcher, useFactory: () => {
73+
return {scrolled: (delay: number, callback: () => any) => {
74+
return scrolledSubject.asObservable().subscribe(callback);
75+
}};
76+
}}
6777
]
6878
});
6979

@@ -925,6 +935,29 @@ describe('MdAutocomplete', () => {
925935
.toEqual('below', `Expected autocomplete positionY to default to below.`);
926936
});
927937

938+
it('should reposition the panel on scroll', () => {
939+
const spacer = document.createElement('div');
940+
941+
spacer.style.height = '1000px';
942+
document.body.appendChild(spacer);
943+
944+
fixture.componentInstance.trigger.openPanel();
945+
fixture.detectChanges();
946+
947+
window.scroll(0, 100);
948+
scrolledSubject.next();
949+
fixture.detectChanges();
950+
951+
const inputBottom = input.getBoundingClientRect().bottom;
952+
const panel = overlayContainerElement.querySelector('.mat-autocomplete-panel');
953+
const panelTop = panel.getBoundingClientRect().top;
954+
955+
expect((inputBottom + 6).toFixed(1)).toEqual(panelTop.toFixed(1),
956+
'Expected panel top to match input bottom after scrolling.');
957+
958+
document.body.removeChild(spacer);
959+
});
960+
928961
it('should fall back to above position if panel cannot fit below', () => {
929962
// Push the autocomplete trigger down so it won't have room to open "below"
930963
input.style.top = '600px';

0 commit comments

Comments
 (0)