Skip to content

Commit 8df30db

Browse files
karajelbourn
authored andcommitted
fix(positioning): fallback positions should work while scrolled (#2193)
1 parent f0c4148 commit 8df30db

File tree

3 files changed

+115
-7
lines changed

3 files changed

+115
-7
lines changed

src/lib/core/overlay/position/connected-position-strategy.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,10 +218,10 @@ export class ConnectedPositionStrategy implements PositionStrategy {
218218
viewportRect: ClientRect): boolean {
219219

220220
// TODO(jelbourn): probably also want some space between overlay edge and viewport edge.
221-
return overlayPoint.x >= viewportRect.left &&
222-
overlayPoint.x + overlayRect.width <= viewportRect.right &&
223-
overlayPoint.y >= viewportRect.top &&
224-
overlayPoint.y + overlayRect.height <= viewportRect.bottom;
221+
return overlayPoint.x >= 0 &&
222+
overlayPoint.x + overlayRect.width <= viewportRect.width &&
223+
overlayPoint.y >= 0 &&
224+
overlayPoint.y + overlayRect.height <= viewportRect.height;
225225
}
226226

227227

src/lib/select/select.spec.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,111 @@ describe('MdSelect', () => {
745745

746746
});
747747

748+
describe('when scrolled', () => {
749+
750+
// Need to set the scrollTop two different ways to support
751+
// both Chrome and Firefox.
752+
function setScrollTop(num: number) {
753+
document.body.scrollTop = num;
754+
document.documentElement.scrollTop = num;
755+
}
756+
757+
beforeEach(() => {
758+
// Make the div above the select very tall, so the page will scroll
759+
fixture.componentInstance.heightAbove = 2000;
760+
761+
// Give the select enough horizontal space to open
762+
select.style.marginLeft = '20px';
763+
select.style.marginRight = '20px';
764+
});
765+
766+
afterEach(() => {
767+
document.body.scrollTop = 0;
768+
document.documentElement.scrollTop = 0;
769+
});
770+
771+
it('should align the first option properly when scrolled', () => {
772+
// Give the select enough space to open
773+
fixture.componentInstance.heightBelow = 400;
774+
fixture.detectChanges();
775+
776+
// Scroll the select into view
777+
setScrollTop(1700);
778+
779+
trigger.click();
780+
fixture.detectChanges();
781+
782+
checkTriggerAlignedWithOption(0);
783+
});
784+
785+
786+
it('should align a centered option properly when scrolled', () => {
787+
// Give the select enough space to open
788+
fixture.componentInstance.heightBelow = 400;
789+
fixture.detectChanges();
790+
791+
fixture.componentInstance.control.setValue('chips-4');
792+
fixture.detectChanges();
793+
794+
// Scroll the select into view
795+
setScrollTop(1700);
796+
797+
trigger.click();
798+
fixture.detectChanges();
799+
800+
checkTriggerAlignedWithOption(4);
801+
});
802+
803+
it('should fall back to "above" positioning properly when scrolled', () => {
804+
// Give the select insufficient space to open below the trigger
805+
fixture.componentInstance.heightBelow = 100;
806+
fixture.detectChanges();
807+
808+
// Scroll the select into view
809+
setScrollTop(1400);
810+
811+
trigger.click();
812+
fixture.detectChanges();
813+
814+
// CSS styles aren't in the tests, so position must be absolute to reflect top/left
815+
const overlayPane = overlayContainerElement.children[0] as HTMLElement;
816+
overlayPane.style.position = 'absolute';
817+
818+
const triggerBottom = trigger.getBoundingClientRect().bottom;
819+
const overlayBottom = overlayPane.getBoundingClientRect().bottom;
820+
821+
expect(overlayBottom.toFixed(2))
822+
.toEqual(triggerBottom.toFixed(2),
823+
`Expected trigger bottom to align with overlay bottom.`);
824+
});
825+
826+
it('should fall back to "below" positioning properly when scrolled', () => {
827+
// Give plenty of space for the select to open below the trigger
828+
fixture.componentInstance.heightBelow = 650;
829+
fixture.detectChanges();
830+
831+
// Select an option too low in the list to fit in limited space above
832+
fixture.componentInstance.control.setValue('sushi-7');
833+
fixture.detectChanges();
834+
835+
// Scroll the select so that it has insufficient space to open above the trigger
836+
setScrollTop(1950);
837+
838+
trigger.click();
839+
fixture.detectChanges();
840+
841+
// CSS styles aren't in the tests, so position must be absolute to reflect top/left
842+
const overlayPane = overlayContainerElement.children[0] as HTMLElement;
843+
overlayPane.style.position = 'absolute';
844+
845+
const triggerTop = trigger.getBoundingClientRect().top;
846+
const overlayTop = overlayPane.getBoundingClientRect().top;
847+
848+
expect(overlayTop.toFixed(2))
849+
.toEqual(triggerTop.toFixed(2), `Expected trigger top to align with overlay top.`);
850+
});
851+
});
852+
748853
describe('x-axis positioning', () => {
749854

750855
beforeEach(() => {
@@ -1037,11 +1142,13 @@ describe('MdSelect', () => {
10371142
@Component({
10381143
selector: 'basic-select',
10391144
template: `
1145+
<div [style.height.px]="heightAbove"></div>
10401146
<md-select placeholder="Food" [formControl]="control" [required]="isRequired">
10411147
<md-option *ngFor="let food of foods" [value]="food.value" [disabled]="food.disabled">
10421148
{{ food.viewValue }}
10431149
</md-option>
10441150
</md-select>
1151+
<div [style.height.px]="heightBelow"></div>
10451152
`
10461153
})
10471154
class BasicSelect {
@@ -1057,6 +1164,8 @@ class BasicSelect {
10571164
];
10581165
control = new FormControl();
10591166
isRequired: boolean;
1167+
heightAbove = 0;
1168+
heightBelow = 0;
10601169

10611170
@ViewChild(MdSelect) select: MdSelect;
10621171
@ViewChildren(MdOption) options: QueryList<MdOption>;

src/lib/select/select.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -565,10 +565,9 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr
565565
const viewportRect = this._viewportRuler.getViewportRect();
566566
const triggerRect = this._getTriggerRect();
567567

568-
const topSpaceAvailable =
569-
triggerRect.top - viewportRect.top - SELECT_PANEL_VIEWPORT_PADDING;
568+
const topSpaceAvailable = triggerRect.top - SELECT_PANEL_VIEWPORT_PADDING;
570569
const bottomSpaceAvailable =
571-
viewportRect.bottom - triggerRect.bottom - SELECT_PANEL_VIEWPORT_PADDING;
570+
viewportRect.height - triggerRect.bottom - SELECT_PANEL_VIEWPORT_PADDING;
572571

573572
const panelHeightTop = Math.abs(this._offsetY);
574573
const totalPanelHeight =

0 commit comments

Comments
 (0)