diff --git a/src/components/datepicker/datePicker.js b/src/components/datepicker/datePicker.js
index 8f25256b71a..40e60d07e7b 100644
--- a/src/components/datepicker/datePicker.js
+++ b/src/components/datepicker/datePicker.js
@@ -7,5 +7,6 @@
angular.module('material.components.datepicker', [
'material.core',
'material.components.icon',
- 'material.components.virtualRepeat'
+ 'material.components.virtualRepeat',
+ 'material.components.panel'
]);
diff --git a/src/components/datepicker/datePicker.scss b/src/components/datepicker/datePicker.scss
index 650b29dcec7..6b3b51af49b 100644
--- a/src/components/datepicker/datePicker.scss
+++ b/src/components/datepicker/datePicker.scss
@@ -116,19 +116,8 @@ md-datepicker {
}
}
-.md-datepicker-is-showing .md-scroll-mask {
- z-index: $z-index-calendar-pane - 1;
-}
-
// Floating pane that contains the calendar at the bottom of the input.
.md-datepicker-calendar-pane {
- // On most browsers the `scale(0)` below prevents this element from
- // overflowing it's parent, however IE and Edge seem to disregard it.
- // The `left: -100%` pulls the element back in order to ensure that
- // it doesn't cause an overflow.
- position: absolute;
- top: 0;
- left: -100%;
z-index: $z-index-calendar-pane;
border-width: 1px;
border-style: solid;
@@ -149,19 +138,15 @@ md-datepicker {
width: $md-calendar-width;
position: relative;
overflow: hidden;
-
- background: transparent;
- pointer-events: none;
- cursor: text;
}
// The calendar portion of the floating pane (vs. the input mask).
.md-datepicker-calendar {
- opacity: 0;
// Use a modified timing function (from swift-ease-out) so that the opacity part of the
// animation doesn't come in as quickly so that the floating pane doesn't ever seem to
// cover up the trigger input.
transition: opacity $md-datepicker-open-animation-duration cubic-bezier(0.5, 0, 0.25, 1);
+ opacity: 0;
.md-pane-open & {
opacity: 1;
@@ -194,7 +179,7 @@ md-datepicker {
.md-datepicker-triangle-button {
position: absolute;
@include rtl-prop(right, left, 0, auto);
- top: $md-date-arrow-size;
+ top: $md-date-arrow-size / 2;
// TODO(jelbourn): This position isn't great on all platforms.
@include rtl(transform, translateY(-25%) translateX(45%), translateY(-25%) translateX(-45%));
diff --git a/src/components/datepicker/js/calendar.js b/src/components/datepicker/js/calendar.js
index 42ba31fb976..24c5981a832 100644
--- a/src/components/datepicker/js/calendar.js
+++ b/src/components/datepicker/js/calendar.js
@@ -58,7 +58,10 @@
minDate: '=mdMinDate',
maxDate: '=mdMaxDate',
dateFilter: '=mdDateFilter',
- _currentView: '@mdCurrentView'
+ _currentView: '@mdCurrentView',
+
+ // private way of passing in the panel from the datepicker
+ _panelRef: '=mdPanelRef'
},
require: ['ngModel', 'mdCalendar'],
controller: CalendarCtrl,
@@ -197,13 +200,18 @@
var boundKeyHandler = angular.bind(this, this.handleKeyEvent);
- // Bind the keydown handler to the body, in order to handle cases where the focused
- // element gets removed from the DOM and stops propagating click events.
- angular.element(document.body).on('keydown', boundKeyHandler);
+ if (this._panelRef) {
+ // Bind the keydown handler to the body, in order to handle cases where the focused
+ // element gets removed from the DOM and stops propagating key events.
+ angular.element(document.body).on('keydown', boundKeyHandler);
- $scope.$on('$destroy', function() {
- angular.element(document.body).off('keydown', boundKeyHandler);
- });
+ $scope.$on('$destroy', function() {
+ angular.element(document.body).off('keydown', boundKeyHandler);
+ });
+ } else {
+ // If the calendar on it's own, it shouldn't bind global key handlers.
+ $element.on('keydown', boundKeyHandler);
+ }
if (this.minDate && this.minDate > $mdDateLocale.firstRenderableDate) {
this.firstRenderableDate = this.minDate;
@@ -345,27 +353,25 @@
CalendarCtrl.prototype.handleKeyEvent = function(event) {
var self = this;
- this.$scope.$apply(function() {
- // Capture escape and emit back up so that a wrapping component
- // (such as a date-picker) can decide to close.
- if (event.which == self.keyCode.ESCAPE || event.which == self.keyCode.TAB) {
- self.$scope.$emit('md-calendar-close');
-
- if (event.which == self.keyCode.TAB) {
+ if (!this._panelRef || this._panelRef.isAttached) {
+ this.$scope.$apply(function() {
+ // Capture tabbing and emit back up so that a wrapping component
+ // (such as a date-picker) can decide to close.
+ if (event.which === self.keyCode.TAB) {
+ self.$scope.$emit('md-calendar-close');
event.preventDefault();
+ return;
}
- return;
- }
-
- // Broadcast the action that any child controllers should take.
- var action = self.getActionFromKeyEvent(event);
- if (action) {
- event.preventDefault();
- event.stopPropagation();
- self.$scope.$broadcast('md-calendar-parent-action', action);
- }
- });
+ // Broadcast the action that any child controllers should take.
+ var action = self.getActionFromKeyEvent(event);
+ if (action) {
+ event.preventDefault();
+ event.stopPropagation();
+ self.$scope.$broadcast('md-calendar-parent-action', action);
+ }
+ });
+ }
};
/**
diff --git a/src/components/datepicker/js/calendar.spec.js b/src/components/datepicker/js/calendar.spec.js
index 740912864f7..e4726301615 100644
--- a/src/components/datepicker/js/calendar.spec.js
+++ b/src/components/datepicker/js/calendar.spec.js
@@ -113,7 +113,7 @@ describe('md-calendar', function() {
function dispatchKeyEvent(keyCode, opt_modifiers) {
var mod = opt_modifiers || {};
- angular.element(document.body).triggerHandler({
+ calendarController.$element.triggerHandler({
type: 'keydown',
keyCode: keyCode,
which: keyCode,
@@ -665,19 +665,19 @@ describe('md-calendar', function() {
});
});
- it('should fire an event when escape is pressed', function() {
- var escapeHandler = jasmine.createSpy('escapeHandler');
- pageScope.$on('md-calendar-close', escapeHandler);
+ it('should fire an event when tabbing away', function() {
+ var tabHandler = jasmine.createSpy('tabHandler');
+ pageScope.$on('md-calendar-close', tabHandler);
pageScope.myDate = new Date(2014, FEB, 11);
applyDateChange();
var selectedDate = element.querySelector('.md-calendar-selected-date');
selectedDate.focus();
- dispatchKeyEvent(keyCodes.ESCAPE);
+ dispatchKeyEvent(keyCodes.TAB);
pageScope.$apply();
- expect(escapeHandler).toHaveBeenCalled();
+ expect(tabHandler).toHaveBeenCalled();
});
});
diff --git a/src/components/datepicker/js/datepickerDirective.js b/src/components/datepicker/js/datepickerDirective.js
index d8aebdc95b8..81c71d06d72 100644
--- a/src/components/datepicker/js/datepickerDirective.js
+++ b/src/components/datepicker/js/datepickerDirective.js
@@ -92,22 +92,6 @@
'ng-focus="ctrl.setFocused(true)" ' +
'ng-blur="ctrl.setFocused(false)"> ' +
triangleButton +
- '' +
-
- // This pane will be detached from here and re-attached to the document body.
- '
';
+
/**
* Controller for md-datepicker.
*
* @ngInject @constructor
*/
function DatePickerCtrl($scope, $element, $attrs, $window, $mdConstant,
- $mdTheming, $mdUtil, $mdDateLocale, $$mdDateUtil, $$rAF, $mdGesture, $filter) {
+ $mdTheming, $mdUtil, $mdDateLocale, $$mdDateUtil, $$rAF, $mdGesture,
+ $filter, $mdPanel) {
/** @final */
this.$window = $window;
@@ -242,14 +244,8 @@
/** @final */
this.$$rAF = $$rAF;
- /**
- * The root document element. This is used for attaching a top-level click handler to
- * close the calendar panel when a click outside said panel occurs. We use `documentElement`
- * instead of body because, when scrolling is disabled, some browsers consider the body element
- * to be completely off the screen and propagate events directly to the html element.
- * @type {!angular.JQLite}
- */
- this.documentElement = angular.element(document.documentElement);
+ /** @final */
+ this.$mdTheming = $mdTheming;
/** @type {!angular.NgModelController} */
this.ngModelCtrl = null;
@@ -263,18 +259,9 @@
/** @type {HTMLElement} */
this.inputContainer = $element[0].querySelector('.md-datepicker-input-container');
- /** @type {HTMLElement} Floating calendar pane. */
- this.calendarPane = $element[0].querySelector('.md-datepicker-calendar-pane');
-
/** @type {HTMLElement} Calendar icon button. */
this.calendarButton = $element[0].querySelector('.md-datepicker-button');
- /**
- * Element covering everything but the input in the top of the floating calendar pane.
- * @type {!angular.JQLite}
- */
- this.inputMask = angular.element($element[0].querySelector('.md-datepicker-input-mask-opaque'));
-
/** @final {!angular.JQLite} */
this.$element = $element;
@@ -291,12 +278,23 @@
this.isFocused = false;
/** @type {boolean} */
- this.isDisabled;
- this.setDisabled($element[0].disabled || angular.isString($attrs.disabled));
+ this.isDisabled = false;
+ this.setDisabled($element[0].disabled || $attrs.hasOwnProperty('disabled'));
- /** @type {boolean} Whether the date-picker's calendar pane is open. */
+ /** @type {boolean} Whether the date-picker's calendar pane is open. Used internally. */
this.isCalendarOpen = false;
+ /**
+ * Whether calendar should be open. Used when triggering the calendar externally.
+ * It needs to be separate from `isCalendarOpen` in order to avoid some mixups
+ * where it might trigger the calendar a couple of times in a row.
+ * @type {boolean}
+ */
+ this.isOpen = false;
+
+ /** @type {boolean} Used to prevent infinite loops when using mdOpenOnFocus. */
+ this.preventInputFocus = false;
+
/** @type {boolean} Whether the calendar should open when the input is focused. */
this.openOnFocus = $attrs.hasOwnProperty('mdOpenOnFocus');
@@ -310,12 +308,6 @@
*/
this.calendarPaneOpenedFrom = null;
- /** @type {String} Unique id for the calendar pane. */
- this.calendarPane.id = 'md-date-pane' + $mdUtil.nextUid();
-
- /** Pre-bound click handler is saved so that the event listener can be removed. */
- this.bodyClickHandler = angular.bind(this, this.handleBodyClick);
-
/**
* Name of the event that will trigger a close. Necessary to sniff the browser, because
* the resize event doesn't make sense on mobile and can have a negative impact since it
@@ -338,6 +330,28 @@
/** @type {Number} Extra margin for the top of the floating calendar. Gets determined on the first open. */
this.topMargin = null;
+ /** @type {Number} Vertical offset for the panel that helps it align the overlay properly. */
+ this.topOffset = null;
+
+ /** @type {!MdPanelPosition} Position object for the mdPanel instance. */
+ this.panelPosition = $mdPanel.newPanelPosition()
+ .relativeTo(this.inputContainer)
+ .addPanelPosition($mdPanel.xPosition.ALIGN_START, $mdPanel.yPosition.BELOW)
+ .withOffsetX(-this.leftMargin + 'px')
+ .withOffsetY(angular.bind(this, this.getPanelYOffset));
+
+ /** @type {!MdPanelRef} Reference to the mdPanel instance of the calendar. */
+ this.panelRef = $mdPanel.create({
+ attachTo: document.body,
+ template: CALENDAR,
+ clickOutsideToClose: true,
+ escapeToClose: true,
+ focusOnOpen: false,
+ scope: this.$scope.$new(),
+ position: this.panelPosition,
+ onDomRemoved: angular.bind(this, this.closeCalendarPane)
+ });
+
// Unless the user specifies so, the datepicker should not be a tab stop.
// This is necessary because ngAria might add a tabindex to anything with an ng-model
// (based on whether or not the user has turned that particular feature on/off).
@@ -349,29 +363,27 @@
}
$mdTheming($element);
- $mdTheming(angular.element(this.calendarPane));
this.installPropertyInterceptors();
this.attachChangeListeners();
this.attachInteractionListeners();
- var self = this;
-
- $scope.$on('$destroy', function() {
- self.detachCalendarPane();
- });
-
if ($attrs.mdIsOpen) {
$scope.$watch('ctrl.isOpen', function(shouldBeOpen) {
if (shouldBeOpen) {
- self.openCalendarPane({
- target: self.inputElement
- });
+ self.openCalendarPane({ target: self.inputElement });
} else {
self.closeCalendarPane();
}
});
}
+
+ var self = this;
+
+ $scope.$on('$destroy', function() {
+ self.detachCalendarPane();
+ self.panelRef.destroy();
+ });
}
/**
@@ -450,18 +462,25 @@
DatePickerCtrl.prototype.attachInteractionListeners = function() {
var self = this;
var $scope = this.$scope;
- var keyCodes = this.$mdConstant.KEY_CODE;
// Add event listener through angular so that we can triggerHandler in unit tests.
self.ngInputElement.on('keydown', function(event) {
- if (event.altKey && event.keyCode == keyCodes.DOWN_ARROW) {
+ if (event.altKey && event.keyCode === self.$mdConstant.KEY_CODE.DOWN_ARROW) {
self.openCalendarPane(event);
$scope.$digest();
}
});
if (self.openOnFocus) {
- self.ngInputElement.on('focus', angular.bind(self, self.openCalendarPane));
+ self.ngInputElement.on('focus', function(event) {
+ if (!self.preventInputFocus && !self.inputFocusedOnWindowBlur) {
+ self.preventInputFocus = true;
+ self.openCalendarPane(event);
+ } else if (self.inputFocusedOnWindowBlur) {
+ self.inputFocusedOnWindowBlur = false;
+ }
+ });
+
angular.element(self.$window).on('blur', self.windowBlurHandler);
$scope.$on('$destroy', function() {
@@ -611,103 +630,53 @@
/** Position and attach the floating calendar to the document. */
DatePickerCtrl.prototype.attachCalendarPane = function() {
- var calendarPane = this.calendarPane;
- var body = document.body;
-
- calendarPane.style.transform = '';
- this.$element.addClass(OPEN_CLASS);
- this.mdInputContainer && this.mdInputContainer.element.addClass(OPEN_CLASS);
- angular.element(body).addClass('md-datepicker-is-showing');
-
- var elementRect = this.inputContainer.getBoundingClientRect();
- var bodyRect = body.getBoundingClientRect();
-
- if (!this.topMargin || this.topMargin < 0) {
- this.topMargin = (this.inputMask.parent().prop('clientHeight') - this.ngInputElement.prop('clientHeight')) / 2;
- }
-
- // Check to see if the calendar pane would go off the screen. If so, adjust position
- // accordingly to keep it within the viewport.
- var paneTop = elementRect.top - bodyRect.top - this.topMargin;
- var paneLeft = elementRect.left - bodyRect.left - this.leftMargin;
-
- // If ng-material has disabled body scrolling (for example, if a dialog is open),
- // then it's possible that the already-scrolled body has a negative top/left. In this case,
- // we want to treat the "real" top as (0 - bodyRect.top). In a normal scrolling situation,
- // though, the top of the viewport should just be the body's scroll position.
- var viewportTop = (bodyRect.top < 0 && document.body.scrollTop == 0) ?
- -bodyRect.top :
- document.body.scrollTop;
-
- var viewportLeft = (bodyRect.left < 0 && document.body.scrollLeft == 0) ?
- -bodyRect.left :
- document.body.scrollLeft;
-
- var viewportBottom = viewportTop + this.$window.innerHeight;
- var viewportRight = viewportLeft + this.$window.innerWidth;
-
- // Creates an overlay with a hole the same size as element. We remove a pixel or two
- // on each end to make it overlap slightly. The overlay's background is added in
- // the theme in the form of a box-shadow with a huge spread.
- this.inputMask.css({
- position: 'absolute',
- left: this.leftMargin + 'px',
- top: this.topMargin + 'px',
- width: (elementRect.width - 1) + 'px',
- height: (elementRect.height - 2) + 'px'
- });
-
- // If the right edge of the pane would be off the screen and shifting it left by the
- // difference would not go past the left edge of the screen. If the calendar pane is too
- // big to fit on the screen at all, move it to the left of the screen and scale the entire
- // element down to fit.
- if (paneLeft + CALENDAR_PANE_WIDTH > viewportRight) {
- if (viewportRight - CALENDAR_PANE_WIDTH > 0) {
- paneLeft = viewportRight - CALENDAR_PANE_WIDTH;
- } else {
- paneLeft = viewportLeft;
- var scale = this.$window.innerWidth / CALENDAR_PANE_WIDTH;
- calendarPane.style.transform = 'scale(' + scale + ')';
- }
-
- calendarPane.classList.add('md-datepicker-pos-adjusted');
- }
-
- // If the bottom edge of the pane would be off the screen and shifting it up by the
- // difference would not go past the top edge of the screen.
- if (paneTop + CALENDAR_PANE_HEIGHT > viewportBottom &&
- viewportBottom - CALENDAR_PANE_HEIGHT > viewportTop) {
- paneTop = viewportBottom - CALENDAR_PANE_HEIGHT;
- calendarPane.classList.add('md-datepicker-pos-adjusted');
- }
+ var self = this;
- calendarPane.style.left = paneLeft + 'px';
- calendarPane.style.top = paneTop + 'px';
- document.body.appendChild(calendarPane);
+ return this.panelRef.open().then(function(panelRef) {
+ var calendarPane = panelRef.panelEl[0].querySelector('.md-datepicker-calendar-pane');
+ var inputMask = calendarPane.querySelector('.md-datepicker-input-mask-opaque');
+ var elementRect = self.inputContainer.getBoundingClientRect();
+
+ self.$mdTheming(angular.element(calendarPane));
+
+ // Creates an overlay with a hole the same size as element. We remove a pixel or two
+ // on each end to make it overlap slightly. The overlay's background is added in
+ // the theme in the form of a box-shadow with a huge spread.
+ angular.element(inputMask).css({
+ position: 'absolute',
+ left: self.leftMargin + 'px',
+ top: self.topMargin + 'px',
+ width: elementRect.width + 'px',
+ height: (elementRect.height - 2) + 'px'
+ });
- // Add CSS class after one frame to trigger open animation.
- this.$$rAF(function() {
- calendarPane.classList.add('md-pane-open');
+ // Add the CSS classes after one frame to trigger open animation. Note that it needs
+ // an extra timeout, because in some casses rAF might fire too early (e.g. when the
+ // input focus opens the calendar), breaking the animation.
+ self.$mdUtil.nextTick(function() {
+ self.$$rAF(function() {
+ calendarPane.classList.add('md-pane-open');
+ self.$element.addClass(OPEN_CLASS);
+ self.mdInputContainer && self.mdInputContainer.element.addClass(OPEN_CLASS);
+ });
+ }, false);
});
};
/** Detach the floating calendar pane from the document. */
DatePickerCtrl.prototype.detachCalendarPane = function() {
+ var panelRef = this.panelRef;
+
this.$element.removeClass(OPEN_CLASS);
this.mdInputContainer && this.mdInputContainer.element.removeClass(OPEN_CLASS);
- angular.element(document.body).removeClass('md-datepicker-is-showing');
- this.calendarPane.classList.remove('md-pane-open');
- this.calendarPane.classList.remove('md-datepicker-pos-adjusted');
- if (this.isCalendarOpen) {
- this.$mdUtil.enableScrolling();
+ if (panelRef.panelEl) {
+ panelRef.panelEl[0]
+ .querySelector('.md-datepicker-calendar-pane')
+ .classList.remove('md-pane-open');
}
- if (this.calendarPane.parentNode) {
- // Use native DOM removal because we do not want any of the
- // angular state of this element to be disposed.
- this.calendarPane.parentNode.removeChild(this.calendarPane);
- }
+ panelRef.close();
};
/**
@@ -715,70 +684,40 @@
* @param {Event} event
*/
DatePickerCtrl.prototype.openCalendarPane = function(event) {
- if (!this.isCalendarOpen && !this.isDisabled && !this.inputFocusedOnWindowBlur) {
+ if (!this.isCalendarOpen && !this.isDisabled) {
+ var self = this;
+
this.isCalendarOpen = this.isOpen = true;
this.calendarPaneOpenedFrom = event.target;
- // Because the calendar pane is attached directly to the body, it is possible that the
- // rest of the component (input, etc) is in a different scrolling container, such as
- // an md-content. This means that, if the container is scrolled, the pane would remain
- // stationary. To remedy this, we disable scrolling while the calendar pane is open, which
- // also matches the native behavior for things like `