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

Commit 9d1f9da

Browse files
committed
feat(datepicker): prevent calendar from going off-screen. Fixes #4333.
1 parent 5551699 commit 9d1f9da

File tree

3 files changed

+84
-3
lines changed

3 files changed

+84
-3
lines changed

src/components/datepicker/datePicker.js

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,26 @@
9999
/** Default time in ms to debounce input event by. */
100100
var DEFAULT_DEBOUNCE_INTERVAL = 500;
101101

102+
/**
103+
* Height of the calendar pane used to check if the pane is going outside the boundary of
104+
* the viewport. See calendar.scss for how $md-calendar-height is computed; an extra 20px is
105+
* also added to space the pane away from the exact edge of the screen.
106+
*
107+
* This is computed statically now, but can be changed to be measured if the circumstances
108+
* of calendar sizing are changed.
109+
*/
110+
var CALENDAR_PANE_HEIGHT = 368;
111+
112+
/**
113+
* Width of the calendar pane used to check if the pane is going outside the boundary of
114+
* the viewport. See calendar.scss for how $md-calendar-width is computed; an extra 20px is
115+
* also added to space the pane away from the exact edge of the screen.
116+
*
117+
* This is computed statically now, but can be changed to be measured if the circumstances
118+
* of calendar sizing are changed.
119+
*/
120+
var CALENDAR_PANE_WIDTH = 360;
121+
102122
/**
103123
* Controller for md-datepicker.
104124
*
@@ -187,6 +207,9 @@
187207
/** Pre-bound click handler is saved so that the event listener can be removed. */
188208
this.bodyClickHandler = angular.bind(this, this.handleBodyClick);
189209

210+
/** Pre-bound resize handler so that the event listener can be removed. */
211+
this.windowResizeHandler = $mdUtil.debounce(angular.bind(this, this.closeCalendarPane), 100);
212+
190213
// Unless the user specifies so, the datepicker should not be a tab stop.
191214
// This is necessary because ngAria might add a tabindex to anything with an ng-model
192215
// (based on whether or not the user has turned that particular feature on/off).
@@ -328,12 +351,33 @@
328351
var elementRect = this.inputContainer.getBoundingClientRect();
329352
var bodyRect = document.body.getBoundingClientRect();
330353

331-
calendarPane.style.left = (elementRect.left - bodyRect.left) + 'px';
332-
calendarPane.style.top = (elementRect.top - bodyRect.top) + 'px';
354+
// Check to see if the calendar pane would go off the screen. If so, adjust position
355+
// accordingly to keep it within the viewport.
356+
var paneTop = elementRect.top - bodyRect.top;
357+
var paneLeft = elementRect.left - bodyRect.left;
358+
359+
// If the right edge of the pane would be off the screen and shifting it left by the
360+
// difference would not go past the left edge of the screen.
361+
if (paneLeft + CALENDAR_PANE_WIDTH > bodyRect.right &&
362+
bodyRect.right - CALENDAR_PANE_WIDTH > 0) {
363+
paneLeft = bodyRect.right - CALENDAR_PANE_WIDTH;
364+
calendarPane.classList.add('md-datepicker-pos-adjusted');
365+
}
366+
367+
// If the bottom edge of the pane would be off the screen and shifting it up by the
368+
// difference would not go past the top edge of the screen.
369+
if (paneTop + CALENDAR_PANE_HEIGHT > bodyRect.bottom &&
370+
bodyRect.bottom - CALENDAR_PANE_HEIGHT > 0) {
371+
paneTop = bodyRect.bottom - CALENDAR_PANE_HEIGHT;
372+
calendarPane.classList.add('md-datepicker-pos-adjusted');
373+
}
374+
375+
calendarPane.style.left = paneLeft + 'px';
376+
calendarPane.style.top = paneTop + 'px';
333377
document.body.appendChild(this.calendarPane);
334378

335379
// The top of the calendar pane is a transparent box that shows the text input underneath.
336-
// Since the pane is flowing, though, the page underneath the pane *adjacent* to the input is
380+
// Since the pane is floating, though, the page underneath the pane *adjacent* to the input is
337381
// also shown unless we cover it up. The inputMask does this by filling up the remaining space
338382
// based on the width of the input.
339383
this.inputMask.style.left = elementRect.width + 'px';
@@ -344,10 +388,15 @@
344388
});
345389
};
346390

391+
DatePickerCtrl.prototype.positionCalendarPane = function() {
392+
393+
};
394+
347395
/** Detach the floating calendar pane from the document. */
348396
DatePickerCtrl.prototype.detachCalendarPane = function() {
349397
this.$element.removeClass('md-datepicker-open');
350398
this.calendarPane.classList.remove('md-pane-open');
399+
this.calendarPane.classList.remove('md-datepicker-pos-adjusted');
351400

352401
if (this.calendarPane.parentNode) {
353402
// Use native DOM removal because we do not want any of the angular state of this element
@@ -380,6 +429,8 @@
380429
this.$mdUtil.nextTick(function() {
381430
document.body.addEventListener('click', self.bodyClickHandler);
382431
}, false);
432+
433+
window.addEventListener('resize', this.windowResizeHandler);
383434
}
384435
};
385436

@@ -392,6 +443,7 @@
392443
this.$mdUtil.enableScrolling();
393444

394445
document.body.removeEventListener('click', this.bodyClickHandler);
446+
window.removeEventListener('resize', this.windowResizeHandler);
395447
};
396448

397449
/** Gets the controller instance for the calendar in the floating pane. */

src/components/datepicker/datePicker.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,13 @@ md-datepicker[disabled] {
164164
}
165165
}
166166

167+
// When the position of the floating calendar pane is adjusted to remain inside
168+
// of the viewport, hide the inputput mask, as the text input will no longer be
169+
// directly underneath it.
170+
.md-datepicker-pos-adjusted .md-datepicker-input-mask {
171+
display: none;
172+
}
173+
167174
// Animate the calendar inside of the floating calendar pane such that it appears to "scroll" into
168175
// view while the pane is opening. This is done as a cue to users that the calendar is scrollable.
169176
.md-datepicker-calendar-pane {

src/components/datepicker/datePicker.spec.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,28 @@ describe('md-date-picker', function() {
8080

8181
});
8282

83+
it('should adjust the position of the floating pane if it would go off-screen', function() {
84+
// Absolutely position the picker near the edge of the screen.
85+
var bodyRect = document.body.getBoundingClientRect();
86+
element.style.position = 'absolute';
87+
element.style.top = bodyRect.bottom + 'px';
88+
element.style.left = bodyRect.right + 'px';
89+
document.body.appendChild(element);
90+
91+
// Open the pane.
92+
element.querySelector('md-button').click();
93+
$timeout.flush();
94+
95+
// Expect that the whole pane is on-screen.
96+
var paneRect = controller.calendarPane.getBoundingClientRect();
97+
expect(paneRect.right).toBeLessThan(bodyRect.right + 1);
98+
expect(paneRect.bottom).toBeLessThan(bodyRect.bottom + 1);
99+
expect(paneRect.top).toBeGreaterThan(0);
100+
expect(paneRect.left).toBeGreaterThan(0);
101+
102+
document.body.removeChild(element);
103+
});
104+
83105
it('should disable the internal inputs based on ng-disabled binding', function() {
84106
expect(controller.inputElement.disabled).toBe(false);
85107
expect(controller.calendarButton.disabled).toBe(false);

0 commit comments

Comments
 (0)