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 @@

Development tools

Here is a bunch of stuff after the calendar

Here is a bunch of stuff after the calendar

Here is a bunch of stuff after the calendar

-