Skip to content
This repository was archived by the owner on Sep 5, 2024. It is now read-only.

Commit d80b0b8

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 #9641. Fixes #7878.
1 parent 421fed4 commit d80b0b8

File tree

2 files changed

+215
-70
lines changed

2 files changed

+215
-70
lines changed

src/components/panel/panel.js

+47-1
Original file line numberDiff line numberDiff line change
@@ -1930,6 +1930,12 @@ MdPanelPosition.absPosition = {
19301930
LEFT: 'left'
19311931
};
19321932

1933+
/**
1934+
* Margin between the edges of a panel and the viewport.
1935+
* @const {number}
1936+
*/
1937+
MdPanelPosition.viewportMargin = 8;
1938+
19331939

19341940
/**
19351941
* Sets absolute positioning for the panel.
@@ -2295,6 +2301,9 @@ MdPanelPosition.prototype._reduceTranslateValues =
22952301
* @private
22962302
*/
22972303
MdPanelPosition.prototype._setPanelPosition = function(panelEl) {
2304+
// Remove the class in case it has been added before.
2305+
panelEl.removeClass('_md-panel-position-adjusted');
2306+
22982307
// Only calculate the position if necessary.
22992308
if (this._absolute) {
23002309
return;
@@ -2309,12 +2318,49 @@ MdPanelPosition.prototype._setPanelPosition = function(panelEl) {
23092318
this._actualPosition = this._positions[i];
23102319
this._calculatePanelPosition(panelEl, this._actualPosition);
23112320
if (this._isOnscreen(panelEl)) {
2312-
break;
2321+
return;
23132322
}
23142323
}
2324+
2325+
// Class that can be used to re-style the panel if it was repositioned.
2326+
panelEl.addClass('_md-panel-position-adjusted');
2327+
this._constrainToViewport(panelEl);
23152328
};
23162329

23172330

2331+
/**
2332+
* Constrains a panel's position to the viewport.
2333+
* @param {!angular.JQLite} panelEl
2334+
* @private
2335+
*/
2336+
MdPanelPosition.prototype._constrainToViewport = function(panelEl) {
2337+
var margin = MdPanelPosition.viewportMargin;
2338+
2339+
if (this.getTop()) {
2340+
var top = parseInt(this.getTop());
2341+
var bottom = panelEl[0].offsetHeight + top;
2342+
var viewportHeight = this._$window.innerHeight;
2343+
2344+
if (top < margin) {
2345+
this._top = margin + 'px';
2346+
} else if (bottom > viewportHeight) {
2347+
this._top = top - (bottom - viewportHeight + margin) + 'px';
2348+
}
2349+
}
2350+
2351+
if (this.getLeft()) {
2352+
var left = parseInt(this.getLeft());
2353+
var right = panelEl[0].offsetWidth + left;
2354+
var viewportWidth = this._$window.innerWidth;
2355+
2356+
if (left < margin) {
2357+
this._left = margin + 'px';
2358+
} else if (right > viewportWidth) {
2359+
this._left = left - (right - viewportWidth + margin) + 'px';
2360+
}
2361+
}
2362+
};
2363+
23182364
/**
23192365
* Switches between 'start' and 'end'.
23202366
* @param {string} position Horizontal position of the panel

src/components/panel/panel.spec.js

+168-69
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ 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';
17+
var VIEWPORT_MARGIN = 8;
1618

1719
/**
1820
* @param {!angular.$injector} $injector
@@ -1261,6 +1263,7 @@ describe('$mdPanel', function() {
12611263
myButton = '<button>myButton</button>';
12621264
attachToBody(myButton);
12631265
myButton = angular.element(document.querySelector('button'));
1266+
myButton.css('margin', '100px');
12641267
myButtonRect = myButton[0].getBoundingClientRect();
12651268
});
12661269

@@ -1310,6 +1313,7 @@ describe('$mdPanel', function() {
13101313
expect(panelRect.top).toBeApproximately(myButtonRect.top);
13111314
expect(panelRect.left).toBeApproximately(myButtonRect.left);
13121315

1316+
13131317
var newPosition = $mdPanel.newPanelPosition()
13141318
.relativeTo(myButton)
13151319
.addPanelPosition(null, yPosition.ABOVE);
@@ -1725,6 +1729,7 @@ describe('$mdPanel', function() {
17251729
myButton = '<button>myButton</button>';
17261730
attachToBody(myButton);
17271731
myButton = angular.element(document.querySelector('button'));
1732+
myButton.css('margin', '100px');
17281733
myButtonRect = myButton[0].getBoundingClientRect();
17291734

17301735
xPosition = $mdPanel.xPosition;
@@ -1773,100 +1778,108 @@ describe('$mdPanel', function() {
17731778
expect(panelCss.top).toBeApproximately(myButtonRect.top);
17741779
});
17751780

1776-
it('rejects offscreen position left of target element', function() {
1777-
var position = mdPanelPosition
1778-
.relativeTo(myButton)
1779-
.addPanelPosition(xPosition.OFFSET_START, yPosition.ALIGN_TOPS)
1780-
.addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS);
1781+
describe('fallback positions', function() {
1782+
beforeEach(function() {
1783+
myButton.css('margin', 0);
1784+
myButtonRect = myButton[0].getBoundingClientRect();
1785+
});
17811786

1782-
config['position'] = position;
1787+
it('rejects offscreen position left of target element', function() {
1788+
var position = mdPanelPosition
1789+
.relativeTo(myButton)
1790+
.addPanelPosition(xPosition.OFFSET_START, yPosition.ALIGN_TOPS)
1791+
.addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS);
17831792

1784-
openPanel(config);
1793+
config['position'] = position;
1794+
1795+
openPanel(config);
1796+
1797+
expect(position.getActualPosition()).toEqual({
1798+
x: xPosition.ALIGN_START,
1799+
y: yPosition.ALIGN_TOPS,
1800+
});
17851801

1786-
expect(position.getActualPosition()).toEqual({
1787-
x: xPosition.ALIGN_START,
1788-
y: yPosition.ALIGN_TOPS,
1802+
var panelCss = document.querySelector(PANEL_EL).style;
1803+
expect(panelCss.left).toBeApproximately(myButtonRect.left);
1804+
expect(panelCss.top).toBeApproximately(myButtonRect.top);
17891805
});
1790-
var panelCss = document.querySelector(PANEL_EL).style;
1791-
expect(panelCss.left).toBeApproximately(myButtonRect.left);
1792-
expect(panelCss.top).toBeApproximately(myButtonRect.top);
1793-
});
17941806

1795-
it('rejects offscreen position above target element', function() {
1796-
var position = mdPanelPosition
1797-
.relativeTo(myButton)
1798-
.addPanelPosition(xPosition.ALIGN_START, yPosition.ABOVE)
1799-
.addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS);
1807+
it('rejects offscreen position above target element', function() {
1808+
var position = mdPanelPosition
1809+
.relativeTo(myButton)
1810+
.addPanelPosition(xPosition.ALIGN_START, yPosition.ABOVE)
1811+
.addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS);
18001812

1801-
config['position'] = position;
1813+
config['position'] = position;
18021814

1803-
openPanel(config);
1815+
openPanel(config);
18041816

1805-
expect(position.getActualPosition()).toEqual({
1806-
x: xPosition.ALIGN_START,
1807-
y: yPosition.ALIGN_TOPS,
1817+
expect(position.getActualPosition()).toEqual({
1818+
x: xPosition.ALIGN_START,
1819+
y: yPosition.ALIGN_TOPS,
1820+
});
18081821
});
1809-
});
18101822

1811-
it('rejects offscreen position below target element', function() {
1812-
// reposition button at the bottom of the screen
1813-
$rootEl[0].style.height = "100%";
1814-
myButton[0].style.position = 'absolute';
1815-
myButton[0].style.bottom = '0px';
1816-
myButtonRect = myButton[0].getBoundingClientRect();
1823+
it('rejects offscreen position below target element', function() {
1824+
// reposition button at the bottom of the screen
1825+
$rootEl[0].style.height = "100%";
1826+
myButton[0].style.position = 'absolute';
1827+
myButton[0].style.bottom = '0px';
1828+
myButtonRect = myButton[0].getBoundingClientRect();
18171829

1818-
var position = mdPanelPosition
1819-
.relativeTo(myButton)
1820-
.addPanelPosition(xPosition.ALIGN_START, yPosition.BELOW)
1821-
.addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS);
1830+
var position = mdPanelPosition
1831+
.relativeTo(myButton)
1832+
.addPanelPosition(xPosition.ALIGN_START, yPosition.BELOW)
1833+
.addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS);
18221834

1823-
config['position'] = position;
1835+
config['position'] = position;
18241836

1825-
openPanel(config);
1837+
openPanel(config);
18261838

1827-
expect(position.getActualPosition()).toEqual({
1828-
x: xPosition.ALIGN_START,
1829-
y: yPosition.ALIGN_TOPS,
1839+
expect(position.getActualPosition()).toEqual({
1840+
x: xPosition.ALIGN_START,
1841+
y: yPosition.ALIGN_TOPS,
1842+
});
18301843
});
1831-
});
18321844

1833-
it('rejects offscreen position right of target element', function() {
1834-
// reposition button at the bottom of the screen
1835-
$rootEl[0].style.width = "100%";
1836-
myButton[0].style.position = 'absolute';
1837-
myButton[0].style.right = '0px';
1838-
myButtonRect = myButton[0].getBoundingClientRect();
1845+
it('rejects offscreen position right of target element', function() {
1846+
// reposition button at the bottom of the screen
1847+
$rootEl[0].style.width = "100%";
1848+
myButton[0].style.position = 'absolute';
1849+
myButton[0].style.right = '0px';
1850+
myButtonRect = myButton[0].getBoundingClientRect();
18391851

1840-
var position = mdPanelPosition
1841-
.relativeTo(myButton)
1842-
.addPanelPosition(xPosition.OFFSET_END, yPosition.ALIGN_TOPS)
1843-
.addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS);
1852+
var position = mdPanelPosition
1853+
.relativeTo(myButton)
1854+
.addPanelPosition(xPosition.OFFSET_END, yPosition.ALIGN_TOPS)
1855+
.addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS);
18441856

1845-
config['position'] = position;
1857+
config['position'] = position;
18461858

1847-
openPanel(config);
1859+
openPanel(config);
18481860

1849-
expect(position.getActualPosition()).toEqual({
1850-
x: xPosition.ALIGN_START,
1851-
y: yPosition.ALIGN_TOPS,
1861+
expect(position.getActualPosition()).toEqual({
1862+
x: xPosition.ALIGN_START,
1863+
y: yPosition.ALIGN_TOPS,
1864+
});
18521865
});
1853-
});
18541866

1855-
it('should choose last position if none are on-screen', function() {
1856-
var position = mdPanelPosition
1857-
.relativeTo(myButton)
1858-
// off-screen to the left
1859-
.addPanelPosition(xPosition.OFFSET_START, yPosition.ALIGN_TOPS)
1860-
// off-screen at the top
1861-
.addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS);
1867+
it('should choose last position if none are on-screen', function() {
1868+
var position = mdPanelPosition
1869+
.relativeTo(myButton)
1870+
// off-screen to the left
1871+
.addPanelPosition(xPosition.OFFSET_START, yPosition.ALIGN_TOPS)
1872+
// off-screen at the top
1873+
.addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS);
18621874

1863-
config['position'] = position;
1875+
config['position'] = position;
18641876

1865-
openPanel(config);
1877+
openPanel(config);
18661878

1867-
expect(position.getActualPosition()).toEqual({
1868-
x: xPosition.ALIGN_START,
1869-
y: yPosition.ALIGN_TOPS,
1879+
expect(position.getActualPosition()).toEqual({
1880+
x: xPosition.ALIGN_START,
1881+
y: yPosition.ALIGN_TOPS,
1882+
});
18701883
});
18711884
});
18721885

@@ -1943,6 +1956,49 @@ describe('$mdPanel', function() {
19431956
.getBoundingClientRect();
19441957
expect(panelRect.top).toBeApproximately(myButtonRect.bottom);
19451958
});
1959+
1960+
it('element outside the left boundry of the viewport', function() {
1961+
var position = mdPanelPosition
1962+
.relativeTo(myButton)
1963+
.addPanelPosition(xPosition.ALIGN_END, yPosition.ALIGN_TOPS);
1964+
1965+
config['position'] = position;
1966+
1967+
myButton.css({
1968+
position: 'absolute',
1969+
left: '-100px',
1970+
margin: 0
1971+
});
1972+
1973+
openPanel(config);
1974+
1975+
var panel = document.querySelector(PANEL_EL);
1976+
1977+
expect(panel.offsetLeft).toBe(VIEWPORT_MARGIN);
1978+
expect(panel).toHaveClass(ADJUSTED_CLASS);
1979+
});
1980+
1981+
it('element outside the right boundry of the viewport', function() {
1982+
var position = mdPanelPosition
1983+
.relativeTo(myButton)
1984+
.addPanelPosition(xPosition.ALIGN_START, yPosition.ALIGN_TOPS);
1985+
1986+
config['position'] = position;
1987+
1988+
myButton.css({
1989+
position: 'absolute',
1990+
right: '-100px',
1991+
margin: 0
1992+
});
1993+
1994+
openPanel(config);
1995+
1996+
var panel = document.querySelector(PANEL_EL);
1997+
var panelRect = panel.getBoundingClientRect();
1998+
1999+
expect(panelRect.left + panelRect.width).toBeLessThan(window.innerWidth);
2000+
expect(panel).toHaveClass(ADJUSTED_CLASS);
2001+
});
19462002
});
19472003

19482004
describe('horizontally', function() {
@@ -2019,6 +2075,49 @@ describe('$mdPanel', function() {
20192075
expect(panelRect.left).toBeApproximately(myButtonRect.right);
20202076
});
20212077

2078+
it('element outside the top boundry of the viewport', function() {
2079+
var position = mdPanelPosition
2080+
.relativeTo(myButton)
2081+
.addPanelPosition(xPosition.ALIGN_START, yPosition.ABOVE);
2082+
2083+
config['position'] = position;
2084+
2085+
myButton.css({
2086+
position: 'absolute',
2087+
top: 0,
2088+
margin: 0
2089+
});
2090+
2091+
openPanel(config);
2092+
2093+
var panel = document.querySelector(PANEL_EL);
2094+
2095+
expect(panel.offsetTop).toBe(VIEWPORT_MARGIN);
2096+
expect(panel).toHaveClass(ADJUSTED_CLASS);
2097+
});
2098+
2099+
it('element outside the bottom boundry of the viewport', function() {
2100+
var position = mdPanelPosition
2101+
.relativeTo(myButton)
2102+
.addPanelPosition(xPosition.ALIGN_START, yPosition.BELOW);
2103+
2104+
config['position'] = position;
2105+
2106+
myButton.css({
2107+
position: 'absolute',
2108+
bottom: 0,
2109+
margin: 0
2110+
});
2111+
2112+
openPanel(config);
2113+
2114+
var panel = document.querySelector(PANEL_EL);
2115+
var panelRect = panel.getBoundingClientRect();
2116+
2117+
expect(panelRect.top + panelRect.height).toBeLessThan(window.innerHeight);
2118+
expect(panel).toHaveClass(ADJUSTED_CLASS);
2119+
});
2120+
20222121
describe('rtl', function () {
20232122
beforeEach(function () {
20242123
setRTL();

0 commit comments

Comments
 (0)