Skip to content

Commit 2505bd8

Browse files
committed
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 angular#9641. Fixes angular#7878.
1 parent 72d0685 commit 2505bd8

File tree

2 files changed

+214
-70
lines changed

2 files changed

+214
-70
lines changed

Diff for: src/components/panel/panel.js

+47-1
Original file line numberDiff line numberDiff line change
@@ -1767,6 +1767,12 @@ MdPanelPosition.absPosition = {
17671767
LEFT: 'left'
17681768
};
17691769

1770+
/**
1771+
* Maximum margin between the edges of a panel and the viewport.
1772+
* @type {Number}
1773+
*/
1774+
MdPanelPosition.viewportMargin = 5;
1775+
17701776

17711777
/**
17721778
* Sets absolute positioning for the panel.
@@ -2129,6 +2135,9 @@ MdPanelPosition.prototype._reduceTranslateValues =
21292135
* @private
21302136
*/
21312137
MdPanelPosition.prototype._setPanelPosition = function(panelEl) {
2138+
// Remove the class in case it has been added before.
2139+
panelEl.removeClass('_md-panel-position-adjusted');
2140+
21322141
// Only calculate the position if necessary.
21332142
if (this._absolute) {
21342143
return;
@@ -2143,12 +2152,49 @@ MdPanelPosition.prototype._setPanelPosition = function(panelEl) {
21432152
this._actualPosition = this._positions[i];
21442153
this._calculatePanelPosition(panelEl, this._actualPosition);
21452154
if (this._isOnscreen(panelEl)) {
2146-
break;
2155+
return;
21472156
}
21482157
}
2158+
2159+
// Class that can be used to re-style the panel if it was repositioned.
2160+
panelEl.addClass('_md-panel-position-adjusted');
2161+
this._constrainToViewport(panelEl);
21492162
};
21502163

21512164

2165+
/**
2166+
* Constrains a panel's position to the viewport.
2167+
* @param {!angular.JQLite} panelEl
2168+
* @private
2169+
*/
2170+
MdPanelPosition.prototype._constrainToViewport = function(panelEl) {
2171+
var margin = MdPanelPosition.viewportMargin;
2172+
2173+
if (this.getTop()) {
2174+
var top = parseInt(this.getTop());
2175+
var bottom = panelEl[0].offsetHeight + top;
2176+
var viewportHeight = this._$window.innerHeight;
2177+
2178+
if (top < margin) {
2179+
this._top = margin + 'px';
2180+
} else if (bottom > viewportHeight) {
2181+
this._top = top - (bottom - viewportHeight + margin) + 'px';
2182+
}
2183+
}
2184+
2185+
if (this.getLeft()) {
2186+
var left = parseInt(this.getLeft());
2187+
var right = panelEl[0].offsetWidth + left;
2188+
var viewportWidth = this._$window.innerWidth;
2189+
2190+
if (left < margin) {
2191+
this._left = margin + 'px';
2192+
} else if (right > viewportWidth) {
2193+
this._left = left - (right - viewportWidth + margin) + 'px';
2194+
}
2195+
}
2196+
};
2197+
21522198
/**
21532199
* Switches between 'start' and 'end'.
21542200
* @param {string} position Horizontal position of the panel

Diff for: src/components/panel/panel.spec.js

+167-69
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ describe('$mdPanel', function() {
1313
var DEFAULT_CONFIG = { template: DEFAULT_TEMPLATE };
1414
var PANEL_ID_PREFIX = 'panel_';
1515
var SCROLL_MASK_CLASS = '.md-scroll-mask';
16+
var ADJUSTED_CLASS = '_md-panel-position-adjusted';
1617

1718
/**
1819
* @param {!angular.$injector} $injector
@@ -1261,6 +1262,7 @@ describe('$mdPanel', function() {
12611262
myButton = '<button>myButton</button>';
12621263
attachToBody(myButton);
12631264
myButton = angular.element(document.querySelector('button'));
1265+
myButton.css('margin', '100px');
12641266
myButtonRect = myButton[0].getBoundingClientRect();
12651267
});
12661268

@@ -1310,6 +1312,7 @@ describe('$mdPanel', function() {
13101312
expect(panelRect.top).toBeApproximately(myButtonRect.top);
13111313
expect(panelRect.left).toBeApproximately(myButtonRect.left);
13121314

1315+
13131316
var newPosition = $mdPanel.newPanelPosition()
13141317
.relativeTo(myButton)
13151318
.addPanelPosition(null, yPosition.ABOVE);
@@ -1669,6 +1672,7 @@ describe('$mdPanel', function() {
16691672
myButton = '<button>myButton</button>';
16701673
attachToBody(myButton);
16711674
myButton = angular.element(document.querySelector('button'));
1675+
myButton.css('margin', '100px');
16721676
myButtonRect = myButton[0].getBoundingClientRect();
16731677

16741678
xPosition = $mdPanel.xPosition;
@@ -1717,100 +1721,108 @@ describe('$mdPanel', function() {
17171721
expect(panelCss.top).toBeApproximately(myButtonRect.top);
17181722
});
17191723

1720-
it('rejects offscreen position left of target element', function() {
1721-
var position = mdPanelPosition
1722-
.relativeTo(myButton)
1723-
.addPanelPosition(xPosition.OFFSET_START, yPosition.ALIGN_TOPS)
1724-
.addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS);
1724+
describe('fallback positions', function() {
1725+
beforeEach(function() {
1726+
myButton.css('margin', 0);
1727+
myButtonRect = myButton[0].getBoundingClientRect();
1728+
});
17251729

1726-
config['position'] = position;
1730+
it('rejects offscreen position left of target element', function() {
1731+
var position = mdPanelPosition
1732+
.relativeTo(myButton)
1733+
.addPanelPosition(xPosition.OFFSET_START, yPosition.ALIGN_TOPS)
1734+
.addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS);
17271735

1728-
openPanel(config);
1736+
config['position'] = position;
1737+
1738+
openPanel(config);
1739+
1740+
expect(position.getActualPosition()).toEqual({
1741+
x: xPosition.ALIGN_START,
1742+
y: yPosition.ALIGN_TOPS,
1743+
});
17291744

1730-
expect(position.getActualPosition()).toEqual({
1731-
x: xPosition.ALIGN_START,
1732-
y: yPosition.ALIGN_TOPS,
1745+
var panelCss = document.querySelector(PANEL_EL).style;
1746+
expect(panelCss.left).toBeApproximately(myButtonRect.left);
1747+
expect(panelCss.top).toBeApproximately(myButtonRect.top);
17331748
});
1734-
var panelCss = document.querySelector(PANEL_EL).style;
1735-
expect(panelCss.left).toBeApproximately(myButtonRect.left);
1736-
expect(panelCss.top).toBeApproximately(myButtonRect.top);
1737-
});
17381749

1739-
it('rejects offscreen position above target element', function() {
1740-
var position = mdPanelPosition
1741-
.relativeTo(myButton)
1742-
.addPanelPosition(xPosition.ALIGN_START, yPosition.ABOVE)
1743-
.addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS);
1750+
it('rejects offscreen position above target element', function() {
1751+
var position = mdPanelPosition
1752+
.relativeTo(myButton)
1753+
.addPanelPosition(xPosition.ALIGN_START, yPosition.ABOVE)
1754+
.addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS);
17441755

1745-
config['position'] = position;
1756+
config['position'] = position;
17461757

1747-
openPanel(config);
1758+
openPanel(config);
17481759

1749-
expect(position.getActualPosition()).toEqual({
1750-
x: xPosition.ALIGN_START,
1751-
y: yPosition.ALIGN_TOPS,
1760+
expect(position.getActualPosition()).toEqual({
1761+
x: xPosition.ALIGN_START,
1762+
y: yPosition.ALIGN_TOPS,
1763+
});
17521764
});
1753-
});
17541765

1755-
it('rejects offscreen position below target element', function() {
1756-
// reposition button at the bottom of the screen
1757-
$rootEl[0].style.height = "100%";
1758-
myButton[0].style.position = 'absolute';
1759-
myButton[0].style.bottom = '0px';
1760-
myButtonRect = myButton[0].getBoundingClientRect();
1766+
it('rejects offscreen position below target element', function() {
1767+
// reposition button at the bottom of the screen
1768+
$rootEl[0].style.height = "100%";
1769+
myButton[0].style.position = 'absolute';
1770+
myButton[0].style.bottom = '0px';
1771+
myButtonRect = myButton[0].getBoundingClientRect();
17611772

1762-
var position = mdPanelPosition
1763-
.relativeTo(myButton)
1764-
.addPanelPosition(xPosition.ALIGN_START, yPosition.BELOW)
1765-
.addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS);
1773+
var position = mdPanelPosition
1774+
.relativeTo(myButton)
1775+
.addPanelPosition(xPosition.ALIGN_START, yPosition.BELOW)
1776+
.addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS);
17661777

1767-
config['position'] = position;
1778+
config['position'] = position;
17681779

1769-
openPanel(config);
1780+
openPanel(config);
17701781

1771-
expect(position.getActualPosition()).toEqual({
1772-
x: xPosition.ALIGN_START,
1773-
y: yPosition.ALIGN_TOPS,
1782+
expect(position.getActualPosition()).toEqual({
1783+
x: xPosition.ALIGN_START,
1784+
y: yPosition.ALIGN_TOPS,
1785+
});
17741786
});
1775-
});
17761787

1777-
it('rejects offscreen position right of target element', function() {
1778-
// reposition button at the bottom of the screen
1779-
$rootEl[0].style.width = "100%";
1780-
myButton[0].style.position = 'absolute';
1781-
myButton[0].style.right = '0px';
1782-
myButtonRect = myButton[0].getBoundingClientRect();
1788+
it('rejects offscreen position right of target element', function() {
1789+
// reposition button at the bottom of the screen
1790+
$rootEl[0].style.width = "100%";
1791+
myButton[0].style.position = 'absolute';
1792+
myButton[0].style.right = '0px';
1793+
myButtonRect = myButton[0].getBoundingClientRect();
17831794

1784-
var position = mdPanelPosition
1785-
.relativeTo(myButton)
1786-
.addPanelPosition(xPosition.OFFSET_END, yPosition.ALIGN_TOPS)
1787-
.addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS);
1795+
var position = mdPanelPosition
1796+
.relativeTo(myButton)
1797+
.addPanelPosition(xPosition.OFFSET_END, yPosition.ALIGN_TOPS)
1798+
.addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS);
17881799

1789-
config['position'] = position;
1800+
config['position'] = position;
17901801

1791-
openPanel(config);
1802+
openPanel(config);
17921803

1793-
expect(position.getActualPosition()).toEqual({
1794-
x: xPosition.ALIGN_START,
1795-
y: yPosition.ALIGN_TOPS,
1804+
expect(position.getActualPosition()).toEqual({
1805+
x: xPosition.ALIGN_START,
1806+
y: yPosition.ALIGN_TOPS,
1807+
});
17961808
});
1797-
});
17981809

1799-
it('should choose last position if none are on-screen', function() {
1800-
var position = mdPanelPosition
1801-
.relativeTo(myButton)
1802-
// off-screen to the left
1803-
.addPanelPosition(xPosition.OFFSET_START, yPosition.ALIGN_TOPS)
1804-
// off-screen at the top
1805-
.addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS);
1810+
it('should choose last position if none are on-screen', function() {
1811+
var position = mdPanelPosition
1812+
.relativeTo(myButton)
1813+
// off-screen to the left
1814+
.addPanelPosition(xPosition.OFFSET_START, yPosition.ALIGN_TOPS)
1815+
// off-screen at the top
1816+
.addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS);
18061817

1807-
config['position'] = position;
1818+
config['position'] = position;
18081819

1809-
openPanel(config);
1820+
openPanel(config);
18101821

1811-
expect(position.getActualPosition()).toEqual({
1812-
x: xPosition.ALIGN_START,
1813-
y: yPosition.ALIGN_TOPS,
1822+
expect(position.getActualPosition()).toEqual({
1823+
x: xPosition.ALIGN_START,
1824+
y: yPosition.ALIGN_TOPS,
1825+
});
18141826
});
18151827
});
18161828

@@ -1887,6 +1899,49 @@ describe('$mdPanel', function() {
18871899
.getBoundingClientRect();
18881900
expect(panelRect.top).toBeApproximately(myButtonRect.bottom);
18891901
});
1902+
1903+
it('element outside the left boundry of the viewport', function() {
1904+
var position = mdPanelPosition
1905+
.relativeTo(myButton)
1906+
.addPanelPosition(xPosition.ALIGN_END, yPosition.ALIGN_TOPS);
1907+
1908+
config['position'] = position;
1909+
1910+
myButton.css({
1911+
position: 'absolute',
1912+
left: '-100px',
1913+
margin: 0
1914+
});
1915+
1916+
openPanel(config);
1917+
1918+
var panel = document.querySelector(PANEL_EL);
1919+
1920+
expect(panel.offsetLeft).toBe(MdPanelPosition.viewportMargin);
1921+
expect(panel).toHaveClass(ADJUSTED_CLASS);
1922+
});
1923+
1924+
it('element outside the right boundry of the viewport', function() {
1925+
var position = mdPanelPosition
1926+
.relativeTo(myButton)
1927+
.addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS);
1928+
1929+
config['position'] = position;
1930+
1931+
myButton.css({
1932+
position: 'absolute',
1933+
right: '-100px',
1934+
margin: 0
1935+
});
1936+
1937+
openPanel(config);
1938+
1939+
var panel = document.querySelector(PANEL_EL);
1940+
var panelRect = panel.getBoundingClientRect();
1941+
1942+
expect(panelRect.left + panelRect.width).toBeLessThan(window.innerWidth);
1943+
expect(panel).toHaveClass(ADJUSTED_CLASS);
1944+
});
18901945
});
18911946

18921947
describe('horizontally', function() {
@@ -1963,6 +2018,49 @@ describe('$mdPanel', function() {
19632018
expect(panelRect.left).toBeApproximately(myButtonRect.right);
19642019
});
19652020

2021+
it('element outside the top boundry of the viewport', function() {
2022+
var position = mdPanelPosition
2023+
.relativeTo(myButton)
2024+
.addPanelPosition(xPosition.ALIGN_START, yPosition.ABOVE);
2025+
2026+
config['position'] = position;
2027+
2028+
myButton.css({
2029+
position: 'absolute',
2030+
top: 0,
2031+
margin: 0
2032+
});
2033+
2034+
openPanel(config);
2035+
2036+
var panel = document.querySelector(PANEL_EL);
2037+
2038+
expect(panel.offsetTop).toBe(MdPanelPosition.viewportMargin);
2039+
expect(panel).toHaveClass(ADJUSTED_CLASS);
2040+
});
2041+
2042+
it('element outside the bottom boundry of the viewport', function() {
2043+
var position = mdPanelPosition
2044+
.relativeTo(myButton)
2045+
.addPanelPosition(xPosition.ALIGN_START, yPosition.BELOW);
2046+
2047+
config['position'] = position;
2048+
2049+
myButton.css({
2050+
position: 'absolute',
2051+
bottom: 0,
2052+
margin: 0
2053+
});
2054+
2055+
openPanel(config);
2056+
2057+
var panel = document.querySelector(PANEL_EL);
2058+
var panelRect = panel.getBoundingClientRect();
2059+
2060+
expect(panelRect.top + panelRect.height).toBeLessThan(window.innerHeight);
2061+
expect(panel).toHaveClass(ADJUSTED_CLASS);
2062+
});
2063+
19662064
describe('rtl', function () {
19672065
beforeEach(function () {
19682066
setRTL();

0 commit comments

Comments
 (0)