Skip to content

Commit 9091959

Browse files
committed
fix(autocomplete): reposition panel on scroll
Repositions the autocomplete panel if the users scrolls while it is open.
1 parent c524438 commit 9091959

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
@@ -24,6 +24,7 @@ import 'rxjs/add/observable/merge';
2424
import 'rxjs/add/operator/startWith';
2525
import 'rxjs/add/operator/switchMap';
2626
import {MdInputContainer} from '../input/input-container';
27+
import {ScrollDispatcher} from '../core/overlay/scroll/scroll-dispatcher';
2728

2829
/**
2930
* The following style constants are necessary to save here in order
@@ -72,6 +73,10 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
7273
/** The subscription to positioning changes in the autocomplete panel. */
7374
private _panelPositionSubscription: Subscription;
7475

76+
/** Subscription to global scroll events. */
77+
private _scrolledSubscription: Subscription;
78+
79+
/** Strategy that is used to position the panel. */
7580
private _positionStrategy: ConnectedPositionStrategy;
7681

7782
/** Stream of blur events that should close the panel. */
@@ -101,6 +106,7 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
101106

102107
constructor(private _element: ElementRef, private _overlay: Overlay,
103108
private _viewContainerRef: ViewContainerRef,
109+
private _scrollDispatcher: ScrollDispatcher,
104110
@Optional() private _dir: Dir, private _zone: NgZone,
105111
@Optional() @Host() private _inputContainer: MdInputContainer) {}
106112

@@ -131,6 +137,12 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
131137
this._subscribeToClosingActions();
132138
}
133139

140+
if (!this._scrolledSubscription) {
141+
this._scrolledSubscription = this._scrollDispatcher.scrolled(0, () => {
142+
this._overlayRef.updatePosition();
143+
});
144+
}
145+
134146
this._panelOpen = true;
135147
this._floatPlaceholder();
136148
}
@@ -141,6 +153,11 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
141153
this._overlayRef.detach();
142154
}
143155

156+
if (this._scrolledSubscription) {
157+
this._scrolledSubscription.unsubscribe();
158+
this._scrolledSubscription = null;
159+
}
160+
144161
this._panelOpen = false;
145162
this._resetPlaceholder();
146163
}

src/lib/autocomplete/autocomplete.spec.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,17 @@ import {FakeViewportRuler} from '../core/overlay/position/fake-viewport-ruler';
1414
import {MdAutocomplete} from './autocomplete';
1515
import {MdInputContainer} from '../input/input-container';
1616
import {Observable} from 'rxjs/Observable';
17+
import {Subject} from 'rxjs/Subject';
1718
import {dispatchFakeEvent} from '../core/testing/dispatch-events';
1819
import {typeInElement} from '../core/testing/type-in-element';
20+
import {ScrollDispatcher} from '../core/overlay/scroll/scroll-dispatcher';
1921

2022
import 'rxjs/add/operator/map';
2123

2224
describe('MdAutocomplete', () => {
2325
let overlayContainerElement: HTMLElement;
2426
let dir: LayoutDirection;
27+
let scrolledSubject = new Subject();
2528

2629
beforeEach(async(() => {
2730
dir = 'ltr';
@@ -38,6 +41,8 @@ describe('MdAutocomplete', () => {
3841
providers: [
3942
{provide: OverlayContainer, useFactory: () => {
4043
overlayContainerElement = document.createElement('div');
44+
overlayContainerElement.classList.add('cdk-overlay-container');
45+
4146
document.body.appendChild(overlayContainerElement);
4247

4348
// remove body padding to keep consistent cross-browser
@@ -49,7 +54,12 @@ describe('MdAutocomplete', () => {
4954
{provide: Dir, useFactory: () => {
5055
return {value: dir};
5156
}},
52-
{provide: ViewportRuler, useClass: FakeViewportRuler}
57+
{provide: ViewportRuler, useClass: FakeViewportRuler},
58+
{provide: ScrollDispatcher, useFactory: () => {
59+
return {scrolled: (delay: number, callback: () => any) => {
60+
return scrolledSubject.asObservable().subscribe(callback);
61+
}};
62+
}}
5363
]
5464
});
5565

@@ -853,6 +863,29 @@ describe('MdAutocomplete', () => {
853863
.toEqual('below', `Expected autocomplete positionY to default to below.`);
854864
});
855865

866+
it('should reposition the panel on scroll', () => {
867+
const spacer = document.createElement('div');
868+
869+
spacer.style.height = '1000px';
870+
document.body.appendChild(spacer);
871+
872+
fixture.componentInstance.trigger.openPanel();
873+
fixture.detectChanges();
874+
875+
window.scroll(0, 100);
876+
scrolledSubject.next();
877+
fixture.detectChanges();
878+
879+
const inputBottom = input.getBoundingClientRect().bottom;
880+
const panel = overlayContainerElement.querySelector('.mat-autocomplete-panel');
881+
const panelTop = panel.getBoundingClientRect().top;
882+
883+
expect((inputBottom + 6).toFixed(1)).toEqual(panelTop.toFixed(1),
884+
'Expected panel top to match input bottom after scrolling.');
885+
886+
document.body.removeChild(spacer);
887+
});
888+
856889
it('should fall back to above position if panel cannot fit below', () => {
857890
// Push the autocomplete trigger down so it won't have room to open "below"
858891
input.style.top = '600px';

0 commit comments

Comments
 (0)