Skip to content
This repository has been archived by the owner on Aug 29, 2023. It is now read-only.

Commit

Permalink
update(panel): constrain panel to viewport boundries
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
crisbeto committed Sep 21, 2016
1 parent 421fed4 commit d80b0b8
Show file tree
Hide file tree
Showing 2 changed files with 215 additions and 70 deletions.
48 changes: 47 additions & 1 deletion src/components/panel/panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -1930,6 +1930,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.
Expand Down Expand Up @@ -2295,6 +2301,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;
Expand All @@ -2309,12 +2318,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
Expand Down
237 changes: 168 additions & 69 deletions src/components/panel/panel.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1261,6 +1263,7 @@ describe('$mdPanel', function() {
myButton = '<button>myButton</button>';
attachToBody(myButton);
myButton = angular.element(document.querySelector('button'));
myButton.css('margin', '100px');
myButtonRect = myButton[0].getBoundingClientRect();
});

Expand Down Expand Up @@ -1310,6 +1313,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);
Expand Down Expand Up @@ -1725,6 +1729,7 @@ describe('$mdPanel', function() {
myButton = '<button>myButton</button>';
attachToBody(myButton);
myButton = angular.element(document.querySelector('button'));
myButton.css('margin', '100px');
myButtonRect = myButton[0].getBoundingClientRect();

xPosition = $mdPanel.xPosition;
Expand Down Expand Up @@ -1773,100 +1778,108 @@ 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('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,
});
});
});

Expand Down Expand Up @@ -1943,6 +1956,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() {
Expand Down Expand Up @@ -2019,6 +2075,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();
Expand Down

0 comments on commit d80b0b8

Please sign in to comment.