diff --git a/src/components/datepicker/js/datepickerDirective.js b/src/components/datepicker/js/datepickerDirective.js index b71fcc80c44..b9b6bfc3e43 100644 --- a/src/components/datepicker/js/datepickerDirective.js +++ b/src/components/datepicker/js/datepickerDirective.js @@ -304,17 +304,18 @@ */ this.calendarPaneOpenedFrom = null; + /** @type {String} Unique id for the calendar pane. */ this.calendarPane.id = 'md-date-pane' + $mdUtil.nextUid(); - $mdTheming($element); - $mdTheming(angular.element(this.calendarPane)); - /** Pre-bound click handler is saved so that the event listener can be removed. */ this.bodyClickHandler = angular.bind(this, this.handleBodyClick); /** Pre-bound resize handler so that the event listener can be removed. */ this.windowResizeHandler = $mdUtil.debounce(angular.bind(this, this.closeCalendarPane), 100); + /** Pre-bound handler for the window blur event. Allows for it to be removed later. */ + this.windowBlurHandler = angular.bind(this, this.handleWindowBlur); + // 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). @@ -322,6 +323,9 @@ $element.attr('tabindex', '-1'); } + $mdTheming($element); + $mdTheming(angular.element(this.calendarPane)); + this.installPropertyInterceptors(); this.attachChangeListeners(); this.attachInteractionListeners(); @@ -415,6 +419,11 @@ if (self.openOnFocus) { self.ngInputElement.on('focus', angular.bind(self, self.openCalendarPane)); + angular.element(self.$window).on('blur', self.windowBlurHandler); + + $scope.$on('$destroy', function() { + angular.element(self.$window).off('blur', self.windowBlurHandler); + }); } $scope.$on('md-calendar-close', function() { @@ -643,8 +652,8 @@ } if (this.calendarPane.parentNode) { - // Use native DOM removal because we do not want any of the angular state of this element - // to be disposed. + // 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); } }; @@ -654,7 +663,7 @@ * @param {Event} event */ DatePickerCtrl.prototype.openCalendarPane = function(event) { - if (!this.isCalendarOpen && !this.isDisabled) { + if (!this.isCalendarOpen && !this.isDisabled && !this.inputFocusedOnWindowBlur) { this.isCalendarOpen = this.isOpen = true; this.calendarPaneOpenedFrom = event.target; @@ -754,4 +763,13 @@ this.$scope.$digest(); } }; + + /** + * Handles the event when the user navigates away from the current tab. Keeps track of + * whether the input was focused when the event happened, in order to prevent the calendar + * from re-opening. + */ + DatePickerCtrl.prototype.handleWindowBlur = function() { + this.inputFocusedOnWindowBlur = document.activeElement === this.inputElement; + }; })(); diff --git a/src/components/datepicker/js/datepickerDirective.spec.js b/src/components/datepicker/js/datepickerDirective.spec.js index 39f04bda038..5957259fcd2 100644 --- a/src/components/datepicker/js/datepickerDirective.spec.js +++ b/src/components/datepicker/js/datepickerDirective.spec.js @@ -586,10 +586,43 @@ describe('md-datepicker', function() { }); }); - it('should be able open the calendar when the input is focused', function() { - createDatepickerInstance(''); - controller.ngInputElement.triggerHandler('focus'); - expect(document.querySelector('md-calendar')).toBeTruthy(); + describe('mdOpenOnFocus attribute', function() { + beforeEach(function() { + createDatepickerInstance(''); + }); + + it('should be able open the calendar when the input is focused', function() { + controller.ngInputElement.triggerHandler('focus'); + expect(controller.isCalendarOpen).toBe(true); + }); + + it('should not reopen a closed calendar when the window is refocused', inject(function($timeout) { + // Focus the input initially to open the calendar. + // Note that the element needs to be appended to the DOM so it can be set as the activeElement. + document.body.appendChild(element); + controller.inputElement.focus(); + controller.ngInputElement.triggerHandler('focus'); + + expect(document.activeElement).toBe(controller.inputElement); + expect(controller.isCalendarOpen).toBe(true); + + // Close the calendar, but make sure that the input is still focused. + controller.closeCalendarPane(); + $timeout.flush(); + expect(document.activeElement).toBe(controller.inputElement); + expect(controller.isCalendarOpen).toBe(false); + + // Simulate the user tabbing away. + angular.element(window).triggerHandler('blur'); + expect(controller.inputFocusedOnWindowBlur).toBe(true); + + // Try opening the calendar again. + controller.ngInputElement.triggerHandler('focus'); + expect(controller.isCalendarOpen).toBe(false); + + // Clean up. + document.body.removeChild(element); + })); }); describe('hiding the icons', function() {