diff --git a/src/components/panel/panel.js b/src/components/panel/panel.js index 2c2774dd528..4ce7a85c7fa 100644 --- a/src/components/panel/panel.js +++ b/src/components/panel/panel.js @@ -2153,6 +2153,12 @@ MdPanelPosition.absPosition = { LEFT: 'left' }; +/** + * Margin between the edges of a panel and the viewport. + * @const {number} + */ +MdPanelPosition.viewportMargin = 8; + /** * Sets absolute positioning for the panel. @@ -2536,6 +2542,9 @@ MdPanelPosition.prototype._reduceTranslateValues = * @private */ MdPanelPosition.prototype._setPanelPosition = function(panelEl) { + // Remove the class in case it has been added before. + panelEl.removeClass('_md-panel-position-adjusted'); + // Only calculate the position if necessary. if (this._absolute) { this._setTransform(panelEl); @@ -2554,12 +2563,49 @@ MdPanelPosition.prototype._setPanelPosition = function(panelEl) { this._setTransform(panelEl); if (this._isOnscreen(panelEl)) { - break; + return; } } + + // Class that can be used to re-style the panel if it was repositioned. + panelEl.addClass('_md-panel-position-adjusted'); + this._constrainToViewport(panelEl); }; +/** + * Constrains a panel's position to the viewport. + * @param {!angular.JQLite} panelEl + * @private + */ +MdPanelPosition.prototype._constrainToViewport = function(panelEl) { + var margin = MdPanelPosition.viewportMargin; + + if (this.getTop()) { + var top = parseInt(this.getTop()); + var bottom = panelEl[0].offsetHeight + top; + var viewportHeight = this._$window.innerHeight; + + if (top < margin) { + this._top = margin + 'px'; + } else if (bottom > viewportHeight) { + this._top = top - (bottom - viewportHeight + margin) + 'px'; + } + } + + if (this.getLeft()) { + var left = parseInt(this.getLeft()); + var right = panelEl[0].offsetWidth + left; + var viewportWidth = this._$window.innerWidth; + + if (left < margin) { + this._left = margin + 'px'; + } else if (right > viewportWidth) { + this._left = left - (right - viewportWidth + margin) + 'px'; + } + } +}; + /** * Switches between 'start' and 'end'. * @param {string} position Horizontal position of the panel diff --git a/src/components/panel/panel.spec.js b/src/components/panel/panel.spec.js index 7ec71e31135..c83235fe042 100644 --- a/src/components/panel/panel.spec.js +++ b/src/components/panel/panel.spec.js @@ -13,6 +13,8 @@ describe('$mdPanel', function() { var DEFAULT_CONFIG = { template: DEFAULT_TEMPLATE }; var PANEL_ID_PREFIX = 'panel_'; var SCROLL_MASK_CLASS = '.md-scroll-mask'; + var ADJUSTED_CLASS = '_md-panel-position-adjusted'; + var VIEWPORT_MARGIN = 8; /** * @param {!angular.$injector} $injector @@ -1434,6 +1436,7 @@ describe('$mdPanel', function() { myButton = ''; attachToBody(myButton); myButton = angular.element(document.querySelector('button')); + myButton.css('margin', '100px'); myButtonRect = myButton[0].getBoundingClientRect(); }); @@ -1483,6 +1486,7 @@ describe('$mdPanel', function() { expect(panelRect.top).toBeApproximately(myButtonRect.top); expect(panelRect.left).toBeApproximately(myButtonRect.left); + var newPosition = $mdPanel.newPanelPosition() .relativeTo(myButton) .addPanelPosition(null, yPosition.ABOVE); @@ -1898,6 +1902,7 @@ describe('$mdPanel', function() { myButton = ''; attachToBody(myButton); myButton = angular.element(document.querySelector('button')); + myButton.css('margin', '100px'); myButtonRect = myButton[0].getBoundingClientRect(); xPosition = $mdPanel.xPosition; @@ -1946,134 +1951,142 @@ describe('$mdPanel', function() { expect(panelCss.top).toBeApproximately(myButtonRect.top); }); - it('rejects offscreen position left of target element', function() { - var position = mdPanelPosition - .relativeTo(myButton) - .addPanelPosition(xPosition.OFFSET_START, yPosition.ALIGN_TOPS) - .addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS); + describe('fallback positions', function() { + beforeEach(function() { + myButton.css('margin', 0); + myButtonRect = myButton[0].getBoundingClientRect(); + }); - config['position'] = position; + it('rejects offscreen position left of target element', function() { + var position = mdPanelPosition + .relativeTo(myButton) + .addPanelPosition(xPosition.OFFSET_START, yPosition.ALIGN_TOPS) + .addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS); - openPanel(config); + config['position'] = position; - expect(position.getActualPosition()).toEqual({ - x: xPosition.ALIGN_START, - y: yPosition.ALIGN_TOPS, + openPanel(config); + + expect(position.getActualPosition()).toEqual({ + x: xPosition.ALIGN_START, + y: yPosition.ALIGN_TOPS, + }); + + var panelCss = document.querySelector(PANEL_EL).style; + expect(panelCss.left).toBeApproximately(myButtonRect.left); + expect(panelCss.top).toBeApproximately(myButtonRect.top); }); - var panelCss = document.querySelector(PANEL_EL).style; - expect(panelCss.left).toBeApproximately(myButtonRect.left); - expect(panelCss.top).toBeApproximately(myButtonRect.top); - }); - it('rejects offscreen position above target element', function() { - var position = mdPanelPosition - .relativeTo(myButton) - .addPanelPosition(xPosition.ALIGN_START, yPosition.ABOVE) - .addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS); + it('rejects offscreen position above target element', function() { + var position = mdPanelPosition + .relativeTo(myButton) + .addPanelPosition(xPosition.ALIGN_START, yPosition.ABOVE) + .addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS); - config['position'] = position; + config['position'] = position; - openPanel(config); + openPanel(config); - expect(position.getActualPosition()).toEqual({ - x: xPosition.ALIGN_START, - y: yPosition.ALIGN_TOPS, + expect(position.getActualPosition()).toEqual({ + x: xPosition.ALIGN_START, + y: yPosition.ALIGN_TOPS, + }); }); - }); - it('rejects offscreen position below target element', function() { - // reposition button at the bottom of the screen - $rootEl[0].style.height = "100%"; - myButton[0].style.position = 'absolute'; - myButton[0].style.bottom = '0px'; - myButtonRect = myButton[0].getBoundingClientRect(); + it('rejects offscreen position below target element', function() { + // reposition button at the bottom of the screen + $rootEl[0].style.height = "100%"; + myButton[0].style.position = 'absolute'; + myButton[0].style.bottom = '0px'; + myButtonRect = myButton[0].getBoundingClientRect(); - var position = mdPanelPosition - .relativeTo(myButton) - .addPanelPosition(xPosition.ALIGN_START, yPosition.BELOW) - .addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS); + var position = mdPanelPosition + .relativeTo(myButton) + .addPanelPosition(xPosition.ALIGN_START, yPosition.BELOW) + .addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS); - config['position'] = position; + config['position'] = position; - openPanel(config); + openPanel(config); - expect(position.getActualPosition()).toEqual({ - x: xPosition.ALIGN_START, - y: yPosition.ALIGN_TOPS, + expect(position.getActualPosition()).toEqual({ + x: xPosition.ALIGN_START, + y: yPosition.ALIGN_TOPS, + }); }); - }); - it('rejects offscreen position right of target element', function() { - // reposition button at the bottom of the screen - $rootEl[0].style.width = "100%"; - myButton[0].style.position = 'absolute'; - myButton[0].style.right = '0px'; - myButtonRect = myButton[0].getBoundingClientRect(); + it('rejects offscreen position right of target element', function() { + // reposition button at the bottom of the screen + $rootEl[0].style.width = "100%"; + myButton[0].style.position = 'absolute'; + myButton[0].style.right = '0px'; + myButtonRect = myButton[0].getBoundingClientRect(); - var position = mdPanelPosition - .relativeTo(myButton) - .addPanelPosition(xPosition.OFFSET_END, yPosition.ALIGN_TOPS) - .addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS); + var position = mdPanelPosition + .relativeTo(myButton) + .addPanelPosition(xPosition.OFFSET_END, yPosition.ALIGN_TOPS) + .addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS); - config['position'] = position; + config['position'] = position; - openPanel(config); + openPanel(config); - expect(position.getActualPosition()).toEqual({ - x: xPosition.ALIGN_START, - y: yPosition.ALIGN_TOPS, + expect(position.getActualPosition()).toEqual({ + x: xPosition.ALIGN_START, + y: yPosition.ALIGN_TOPS, + }); }); - }); - it('takes the x offset into account', function() { - var position = mdPanelPosition - .relativeTo(myButton) - .withOffsetX(window.innerWidth + 'px') - .addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS) - .addPanelPosition(xPosition.ALIGN_END, yPosition.ALIGN_TOPS); + it('takes the x offset into account', function() { + var position = mdPanelPosition + .relativeTo(myButton) + .withOffsetX(window.innerWidth + 'px') + .addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS) + .addPanelPosition(xPosition.ALIGN_END, yPosition.ALIGN_TOPS); - config['position'] = position; + config['position'] = position; - openPanel(config); + openPanel(config); - expect(position.getActualPosition()).toEqual({ - x: xPosition.ALIGN_END, - y: yPosition.ALIGN_TOPS + expect(position.getActualPosition()).toEqual({ + x: xPosition.ALIGN_END, + y: yPosition.ALIGN_TOPS + }); }); - }); - it('takes the y offset into account', function() { - var position = mdPanelPosition - .relativeTo(myButton) - .withOffsetY(window.innerHeight + 'px') - .addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_BOTTOMS) - .addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS); + it('takes the y offset into account', function() { + var position = mdPanelPosition + .relativeTo(myButton) + .withOffsetY(window.innerHeight + 'px') + .addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_BOTTOMS) + .addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS); - config['position'] = position; + config['position'] = position; - openPanel(config); + openPanel(config); - expect(position.getActualPosition()).toEqual({ - x: xPosition.ALIGN_START, - y: yPosition.ALIGN_TOPS + expect(position.getActualPosition()).toEqual({ + x: xPosition.ALIGN_START, + y: yPosition.ALIGN_TOPS + }); }); - }); - it('should choose last position if none are on-screen', function() { - var position = mdPanelPosition - .relativeTo(myButton) - // off-screen to the left - .addPanelPosition(xPosition.OFFSET_START, yPosition.ALIGN_TOPS) - // off-screen at the top - .addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS); + it('should choose last position if none are on-screen', function() { + var position = mdPanelPosition + .relativeTo(myButton) + // off-screen to the left + .addPanelPosition(xPosition.OFFSET_START, yPosition.ALIGN_TOPS) + // off-screen at the top + .addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS); - config['position'] = position; + config['position'] = position; - openPanel(config); + openPanel(config); - expect(position.getActualPosition()).toEqual({ - x: xPosition.ALIGN_START, - y: yPosition.ALIGN_TOPS, + expect(position.getActualPosition()).toEqual({ + x: xPosition.ALIGN_START, + y: yPosition.ALIGN_TOPS, + }); }); }); @@ -2205,6 +2218,49 @@ describe('$mdPanel', function() { .getBoundingClientRect(); expect(panelRect.top).toBeApproximately(myButtonRect.bottom); }); + + it('element outside the left boundry of the viewport', function() { + var position = mdPanelPosition + .relativeTo(myButton) + .addPanelPosition(xPosition.ALIGN_END, yPosition.ALIGN_TOPS); + + config['position'] = position; + + myButton.css({ + position: 'absolute', + left: '-100px', + margin: 0 + }); + + openPanel(config); + + var panel = document.querySelector(PANEL_EL); + + expect(panel.offsetLeft).toBe(VIEWPORT_MARGIN); + expect(panel).toHaveClass(ADJUSTED_CLASS); + }); + + it('element outside the right boundry of the viewport', function() { + var position = mdPanelPosition + .relativeTo(myButton) + .addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS); + + config['position'] = position; + + myButton.css({ + position: 'absolute', + right: '-100px', + margin: 0 + }); + + openPanel(config); + + var panel = document.querySelector(PANEL_EL); + var panelRect = panel.getBoundingClientRect(); + + expect(panelRect.left + panelRect.width).toBeLessThan(window.innerWidth); + expect(panel).toHaveClass(ADJUSTED_CLASS); + }); }); describe('horizontally', function() { @@ -2281,6 +2337,49 @@ describe('$mdPanel', function() { expect(panelRect.left).toBeApproximately(myButtonRect.right); }); + it('element outside the top boundry of the viewport', function() { + var position = mdPanelPosition + .relativeTo(myButton) + .addPanelPosition(xPosition.ALIGN_START, yPosition.ABOVE); + + config['position'] = position; + + myButton.css({ + position: 'absolute', + top: 0, + margin: 0 + }); + + openPanel(config); + + var panel = document.querySelector(PANEL_EL); + + expect(panel.offsetTop).toBe(VIEWPORT_MARGIN); + expect(panel).toHaveClass(ADJUSTED_CLASS); + }); + + it('element outside the bottom boundry of the viewport', function() { + var position = mdPanelPosition + .relativeTo(myButton) + .addPanelPosition(xPosition.ALIGN_START, yPosition.BELOW); + + config['position'] = position; + + myButton.css({ + position: 'absolute', + bottom: 0, + margin: 0 + }); + + openPanel(config); + + var panel = document.querySelector(PANEL_EL); + var panelRect = panel.getBoundingClientRect(); + + expect(panelRect.top + panelRect.height).toBeLessThan(window.innerHeight); + expect(panel).toHaveClass(ADJUSTED_CLASS); + }); + describe('rtl', function () { beforeEach(function () { setRTL();