From fa5c22e76b99bebee1a778e919f388f7870d6f21 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Tue, 20 Sep 2016 22:55:31 +0200 Subject: [PATCH] update(panel): constrain panel to viewport boundries Prevents the panel from going outside the viewport by adjusting the position. If developers want more control over how the panel gets repositioned, they can specify addition fallback positions via `addPanelPosition`. Related to #9641. Fixes #7878. --- src/components/panel/panel.js | 48 ++++- src/components/panel/panel.spec.js | 285 +++++++++++++++++++---------- 2 files changed, 239 insertions(+), 94 deletions(-) diff --git a/src/components/panel/panel.js b/src/components/panel/panel.js index 0967aa882dc..5a93fc5e975 100644 --- a/src/components/panel/panel.js +++ b/src/components/panel/panel.js @@ -2156,6 +2156,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. @@ -2525,6 +2531,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) { return; @@ -2539,12 +2548,49 @@ MdPanelPosition.prototype._setPanelPosition = function(panelEl) { this._actualPosition = this._positions[i]; this._calculatePanelPosition(panelEl, this._actualPosition); 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 92cc4cce3ef..a23dc457e75 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; + + 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, + 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, + }); }); }); @@ -2150,6 +2163,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() { @@ -2226,6 +2282,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();