From 56f4ed71a192f5f0c5b1c208719d835d7c59c6c9 Mon Sep 17 00:00:00 2001 From: Kara Erickson Date: Wed, 14 Dec 2016 14:40:37 +0800 Subject: [PATCH] fix(positioning): fallback positions should work while scrolled --- .../position/connected-position-strategy.ts | 8 +- src/lib/select/select.spec.ts | 109 ++++++++++++++++++ src/lib/select/select.ts | 5 +- 3 files changed, 115 insertions(+), 7 deletions(-) diff --git a/src/lib/core/overlay/position/connected-position-strategy.ts b/src/lib/core/overlay/position/connected-position-strategy.ts index b88098021d17..8e50a732ecf4 100644 --- a/src/lib/core/overlay/position/connected-position-strategy.ts +++ b/src/lib/core/overlay/position/connected-position-strategy.ts @@ -218,10 +218,10 @@ export class ConnectedPositionStrategy implements PositionStrategy { viewportRect: ClientRect): boolean { // TODO(jelbourn): probably also want some space between overlay edge and viewport edge. - return overlayPoint.x >= viewportRect.left && - overlayPoint.x + overlayRect.width <= viewportRect.right && - overlayPoint.y >= viewportRect.top && - overlayPoint.y + overlayRect.height <= viewportRect.bottom; + return overlayPoint.x >= 0 && + overlayPoint.x + overlayRect.width <= viewportRect.width && + overlayPoint.y >= 0 && + overlayPoint.y + overlayRect.height <= viewportRect.height; } diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts index d93404c6733e..ae7302c34901 100644 --- a/src/lib/select/select.spec.ts +++ b/src/lib/select/select.spec.ts @@ -745,6 +745,111 @@ describe('MdSelect', () => { }); + describe('when scrolled', () => { + + // Need to set the scrollTop two different ways to support + // both Chrome and Firefox. + function setScrollTop(num: number) { + document.body.scrollTop = num; + document.documentElement.scrollTop = num; + } + + beforeEach(() => { + // Make the div above the select very tall, so the page will scroll + fixture.componentInstance.heightAbove = 2000; + + // Give the select enough horizontal space to open + select.style.marginLeft = '20px'; + select.style.marginRight = '20px'; + }); + + afterEach(() => { + document.body.scrollTop = 0; + document.documentElement.scrollTop = 0; + }); + + it('should align the first option properly when scrolled', () => { + // Give the select enough space to open + fixture.componentInstance.heightBelow = 400; + fixture.detectChanges(); + + // Scroll the select into view + setScrollTop(1700); + + trigger.click(); + fixture.detectChanges(); + + checkTriggerAlignedWithOption(0); + }); + + + it('should align a centered option properly when scrolled', () => { + // Give the select enough space to open + fixture.componentInstance.heightBelow = 400; + fixture.detectChanges(); + + fixture.componentInstance.control.setValue('chips-4'); + fixture.detectChanges(); + + // Scroll the select into view + setScrollTop(1700); + + trigger.click(); + fixture.detectChanges(); + + checkTriggerAlignedWithOption(4); + }); + + it('should fall back to "above" positioning properly when scrolled', () => { + // Give the select insufficient space to open below the trigger + fixture.componentInstance.heightBelow = 100; + fixture.detectChanges(); + + // Scroll the select into view + setScrollTop(1400); + + trigger.click(); + fixture.detectChanges(); + + // CSS styles aren't in the tests, so position must be absolute to reflect top/left + const overlayPane = overlayContainerElement.children[0] as HTMLElement; + overlayPane.style.position = 'absolute'; + + const triggerBottom = trigger.getBoundingClientRect().bottom; + const overlayBottom = overlayPane.getBoundingClientRect().bottom; + + expect(overlayBottom.toFixed(2)) + .toEqual(triggerBottom.toFixed(2), + `Expected trigger bottom to align with overlay bottom.`); + }); + + it('should fall back to "below" positioning properly when scrolled', () => { + // Give plenty of space for the select to open below the trigger + fixture.componentInstance.heightBelow = 650; + fixture.detectChanges(); + + // Select an option too low in the list to fit in limited space above + fixture.componentInstance.control.setValue('sushi-7'); + fixture.detectChanges(); + + // Scroll the select so that it has insufficient space to open above the trigger + setScrollTop(1950); + + trigger.click(); + fixture.detectChanges(); + + // CSS styles aren't in the tests, so position must be absolute to reflect top/left + const overlayPane = overlayContainerElement.children[0] as HTMLElement; + overlayPane.style.position = 'absolute'; + + const triggerTop = trigger.getBoundingClientRect().top; + const overlayTop = overlayPane.getBoundingClientRect().top; + + expect(overlayTop.toFixed(2)) + .toEqual(triggerTop.toFixed(2), `Expected trigger top to align with overlay top.`); + }); + }); + describe('x-axis positioning', () => { beforeEach(() => { @@ -1037,11 +1142,13 @@ describe('MdSelect', () => { @Component({ selector: 'basic-select', template: ` +
{{ food.viewValue }} +
` }) class BasicSelect { @@ -1057,6 +1164,8 @@ class BasicSelect { ]; control = new FormControl(); isRequired: boolean; + heightAbove = 0; + heightBelow = 0; @ViewChild(MdSelect) select: MdSelect; @ViewChildren(MdOption) options: QueryList; diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts index e4b2eb936a4c..e29544741fe0 100644 --- a/src/lib/select/select.ts +++ b/src/lib/select/select.ts @@ -565,10 +565,9 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr const viewportRect = this._viewportRuler.getViewportRect(); const triggerRect = this._getTriggerRect(); - const topSpaceAvailable = - triggerRect.top - viewportRect.top - SELECT_PANEL_VIEWPORT_PADDING; + const topSpaceAvailable = triggerRect.top - SELECT_PANEL_VIEWPORT_PADDING; const bottomSpaceAvailable = - viewportRect.bottom - triggerRect.bottom - SELECT_PANEL_VIEWPORT_PADDING; + viewportRect.height - triggerRect.bottom - SELECT_PANEL_VIEWPORT_PADDING; const panelHeightTop = Math.abs(this._offsetY); const totalPanelHeight =