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();