diff --git a/src/components/calendar/calendar.js b/src/components/calendar/calendar.js
index c2b16d91147..2936d825fc6 100644
--- a/src/components/calendar/calendar.js
+++ b/src/components/calendar/calendar.js
@@ -27,11 +27,9 @@
// TODO(jelbourn): Animations should use `.finally()` instead of `.then()`
// TODO(jelbourn): improve default date parser in locale provider.
// TODO(jelbourn): read-only state.
+ // TODO(jelbourn): make aria-live element visibly hidden (but still present on the page).
function calendarDirective() {
- // Generate a unique ID for each instance of the directive.
- var directiveId = 0;
-
return {
template:
'
' +
@@ -50,7 +48,6 @@
link: function(scope, element, attrs, controllers) {
var ngModelCtrl = controllers[0];
var mdCalendarCtrl = controllers[1];
- mdCalendarCtrl.directiveId = directiveId++;
mdCalendarCtrl.configureNgModel(ngModelCtrl);
}
};
@@ -74,6 +71,9 @@
/** Class applied to the cell for today. */
var TODAY_CLASS = 'md-calendar-date-today';
+ /** Next idientifier for calendar instance. */
+ var nextUniqueId = 0;
+
/**
* Controller for the mdCalendar component.
* @ngInject @constructor
@@ -117,6 +117,9 @@
/** @final {Date} */
this.today = new Date();
+ /** @final {number} Unique ID for this calendar instance. */
+ this.id = nextUniqueId++;
+
/** @type {!angular.NgModelController} */
this.ngModelCtrl = null;
@@ -227,7 +230,14 @@
CalendarCtrl.prototype.handleKeyEvent = function(event) {
var self = this;
this.$scope.$apply(function() {
- // Handled key events fall into two categories: selection and navigation.
+ // 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) {
+ self.$scope.$emit('md-calendar-escape');
+ return;
+ }
+
+ // Remaining key events fall into two categories: selection and navigation.
// Start by checking if this is a selection event.
if (event.which === self.keyCode.ENTER) {
self.setNgModelValue(self.displayDate);
@@ -292,6 +302,11 @@
var cell = this.calendarElement.querySelector('#' + cellId);
cell.focus();
};
+
+ /** Focus the calendar. */
+ CalendarCtrl.prototype.focus = function() {
+ this.focusDateElement(this.selectedDate);
+ };
/*** Animation ***/
@@ -470,11 +485,15 @@
*/
CalendarCtrl.prototype.changeSelectedDate = function(date) {
var self = this;
+ var previousSelectedDate = this.selectedDate;
+ this.selectedDate = date;
+
this.changeDisplayDate(date).then(function() {
// Remove the selected class from the previously selected date, if any.
- if (self.selectedDate) {
- var prevDateCell = self.calendarElement.querySelector('#' + self.getDateId_(self.selectedDate));
+ if (previousSelectedDate) {
+ var prevDateCell =
+ self.calendarElement.querySelector('#' + self.getDateId_(previousSelectedDate));
if (prevDateCell) {
prevDateCell.classList.remove(SELECTED_DATE_CLASS);
}
@@ -487,8 +506,6 @@
dateCell.classList.add(SELECTED_DATE_CLASS);
}
}
-
- self.selectedDate = date;
});
};
@@ -701,7 +718,7 @@
CalendarCtrl.prototype.getDateId_ = function (date) {
return [
'md',
- this.directiveId,
+ this.id,
date.getFullYear(),
date.getMonth(),
date.getDate()
diff --git a/src/components/calendar/dateLocaleProvider.js b/src/components/calendar/dateLocaleProvider.js
index 4f03c1921e7..1c5e3cf7d6e 100644
--- a/src/components/calendar/dateLocaleProvider.js
+++ b/src/components/calendar/dateLocaleProvider.js
@@ -77,6 +77,7 @@
window.$locale = $locale;
+ // TODO(jelbourn): Freeze this object.
return {
months: this.months || $locale.DATETIME_FORMATS.MONTH,
shortMonths: this.shortMonths || $locale.DATETIME_FORMATS.SHORTMONTH,
diff --git a/src/components/calendar/datePicker.js b/src/components/calendar/datePicker.js
index 184866389b8..f17b1dd7d8c 100644
--- a/src/components/calendar/datePicker.js
+++ b/src/components/calendar/datePicker.js
@@ -12,10 +12,11 @@
function datePickerDirective() {
return {
template:
- '
' +
- ' ' +
- '' +
- '
',
+ '' +
+ '
' +
+ '' +
+ '
',
+ //
require: ['ngModel', 'mdDatePicker'],
scope: {},
controller: DatePickerCtrl,
@@ -23,24 +24,46 @@
link: function(scope, element, attr, controllers) {
var ngModelCtrl = controllers[0];
var mdDatePickerCtrl = controllers[1];
+
mdDatePickerCtrl.configureNgModel(ngModelCtrl);
}
};
}
- function DatePickerCtrl($scope, $element, $$mdDateLocale, $$mdDateUtil) {
+ /**
+ * Controller for md-date-picker.
+ *
+ * @ngInject @constructor
+ */
+ function DatePickerCtrl($scope, $element, $compile, $timeout, $mdConstant, $mdUtil,
+ $$mdDateLocale, $$mdDateUtil) {
+ /** @final */
+ this.$compile = $compile;
+
+ /** @final */
+ this.$timeout = $timeout;
+
/** @final */
this.dateLocale = $$mdDateLocale;
/** @final */
this.dateUtil = $$mdDateUtil;
+ /** @final */
+ this.$mdConstant = $mdConstant;
+
+ /* @final */
+ this.$mdUtil = $mdUtil;
+
/** @type {!angular.NgModelController} */
this.ngModelCtrl = null;
/** @type {HTMLInputElement} */
this.inputElement = $element[0].querySelector('input');
+ /** @type {HTMLElement} Floating calendar pane (instantiated lazily) */
+ this.calendarPane = $element[0].querySelector('.md-date-calendar-pane');
+
/** @type {Date} */
this.date = null;
@@ -50,7 +73,19 @@
/** @final {!angular.Scope} */
this.$scope = $scope;
+ /** @type {boolean} Whether the date-picker's calendar pane is open. */
+ this.isCalendarOpen = false;
+
+ /** Pre-bound click handler is saved so that the event listener can be removed. */
+ this.bodyClickHandler = this.handleBodyClick.bind(this);
+
this.attachChangeListeners();
+ this.attachInterationListeners();
+
+ var self = this;
+ $scope.$on('$destroy', function() {
+ self.detachCalendarPane();
+ });
}
/**
@@ -78,6 +113,7 @@
self.$scope.$on('md-calendar-change', function(event, date) {
self.ngModelCtrl.$setViewValue(date);
self.inputElement.value = self.dateLocale.formatDate(date);
+ self.closeCalendarPane();
});
// TODO(jelbourn): debounce
@@ -89,4 +125,92 @@
}
});
};
+
+ /** Attach event listeners for user interaction. */
+ DatePickerCtrl.prototype.attachInterationListeners = function() {
+ var self = this;
+ var $scope = this.$scope;
+ var keyCodes = this.$mdConstant.KEY_CODE;
+
+ self.inputElement.addEventListener('keydown', function(event) {
+ $scope.$apply(function() {
+ if (event.altKey && event.keyCode == keyCodes.DOWN_ARROW) {
+ self.openCalendarPane();
+ }
+ });
+ });
+
+ self.$scope.$on('md-calendar-escape', function() {
+ self.closeCalendarPane();
+ });
+ };
+
+ /** Position and attach the floating calendar to the document. */
+ DatePickerCtrl.prototype.attachCalendarPane = function() {
+ var elementRect = this.$element[0].getBoundingClientRect();
+
+ this.calendarPane.style.left = elementRect.left + 'px';
+ this.calendarPane.style.top = elementRect.bottom + 'px';
+ document.body.appendChild(this.calendarPane);
+ };
+
+ /** Detach the floating calendar pane from the document. */
+ DatePickerCtrl.prototype.detachCalendarPane = function() {
+ // 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);
+ };
+
+ /** Open the floating calendar pane. */
+ DatePickerCtrl.prototype.openCalendarPane = function() {
+ if (!this.isCalendarOpen) {
+ this.isCalendarOpen = true;
+ this.attachCalendarPane();
+ // TODO(jelbourn): dispatch to tell other date pickers to close.
+ this.focusCalendar();
+
+ // Attach click listener inside of a timeout because, if this open call was triggered by a
+ // click, we don't want it to be immediately propogated up to the body and handled.
+ var self = this;
+ this.$timeout(function() {
+ document.body.addEventListener('click', self.bodyClickHandler);
+ }, 0, false);
+ }
+ };
+
+ /** Close the floating calendar pane. */
+ DatePickerCtrl.prototype.closeCalendarPane = function() {
+ this.isCalendarOpen = false;
+ this.detachCalendarPane();
+ this.inputElement.focus();
+ document.body.removeEventListener('click', this.bodyClickHandler);
+ };
+
+ /** Gets the controller instance for the calendar in the floating pane. */
+ DatePickerCtrl.prototype.getCalendarCtrl = function() {
+ return angular.element(this.calendarPane.querySelector('md-calendar')).controller('mdCalendar');
+ };
+
+ /** Focus the calendar in the floating pane. */
+ DatePickerCtrl.prototype.focusCalendar = function() {
+ // Use a timeout in order to allow the calendar to be rendered, as it is gated behind an ng-if.
+ var self = this;
+ this.$timeout(function() {
+ self.getCalendarCtrl().focus();
+ }, 0, false);
+ };
+
+ /**
+ * Handles a click on the document body when the floating calendar pane is open.
+ * Closes the floating calendar pane if the click is not inside of it.
+ * @param {MouseEvent} event
+ */
+ DatePickerCtrl.prototype.handleBodyClick = function(event) {
+ if (this.isCalendarOpen) {
+ var isInCalendar = this.$mdUtil.getClosest(event.target, 'md-calendar');
+ if (!isInCalendar) {
+ this.closeCalendarPane();
+ }
+ }
+ };
})();
diff --git a/src/components/calendar/datePicker.scss b/src/components/calendar/datePicker.scss
new file mode 100644
index 00000000000..7f64a65ba63
--- /dev/null
+++ b/src/components/calendar/datePicker.scss
@@ -0,0 +1,9 @@
+.md-date-calendar-pane {
+ position: absolute;
+ top: 0;
+ left: 0;
+
+ // DEBUG
+ box-shadow: 0 4px 4px;
+ background: white;
+}
diff --git a/src/components/calendar/demoDatePicker/index.html b/src/components/calendar/demoDatePicker/index.html
index 6ef90d649e1..bb558cc78ca 100644
--- a/src/components/calendar/demoDatePicker/index.html
+++ b/src/components/calendar/demoDatePicker/index.html
@@ -22,7 +22,6 @@