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

Commit b1f6e1a

Browse files
committed
feat(datepicker): floating calendar panel for date picker.
1 parent cde67d6 commit b1f6e1a

File tree

5 files changed

+166
-16
lines changed

5 files changed

+166
-16
lines changed

src/components/calendar/calendar.js

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,9 @@
2727
// TODO(jelbourn): Animations should use `.finally()` instead of `.then()`
2828
// TODO(jelbourn): improve default date parser in locale provider.
2929
// TODO(jelbourn): read-only state.
30+
// TODO(jelbourn): make aria-live element visibly hidden (but still present on the page).
3031

3132
function calendarDirective() {
32-
// Generate a unique ID for each instance of the directive.
33-
var directiveId = 0;
34-
3533
return {
3634
template:
3735
'<div>' +
@@ -50,7 +48,6 @@
5048
link: function(scope, element, attrs, controllers) {
5149
var ngModelCtrl = controllers[0];
5250
var mdCalendarCtrl = controllers[1];
53-
mdCalendarCtrl.directiveId = directiveId++;
5451
mdCalendarCtrl.configureNgModel(ngModelCtrl);
5552
}
5653
};
@@ -74,6 +71,9 @@
7471
/** Class applied to the cell for today. */
7572
var TODAY_CLASS = 'md-calendar-date-today';
7673

74+
/** Next idientifier for calendar instance. */
75+
var nextUniqueId = 0;
76+
7777
/**
7878
* Controller for the mdCalendar component.
7979
* @ngInject @constructor
@@ -117,6 +117,9 @@
117117
/** @final {Date} */
118118
this.today = new Date();
119119

120+
/** @final {number} Unique ID for this calendar instance. */
121+
this.id = nextUniqueId++;
122+
120123
/** @type {!angular.NgModelController} */
121124
this.ngModelCtrl = null;
122125

@@ -227,7 +230,14 @@
227230
CalendarCtrl.prototype.handleKeyEvent = function(event) {
228231
var self = this;
229232
this.$scope.$apply(function() {
230-
// Handled key events fall into two categories: selection and navigation.
233+
// Capture escape and emit back up so that a wrapping component (such as a date-picker)
234+
// can decide to close.
235+
if (event.which == self.keyCode.ESCAPE) {
236+
self.$scope.$emit('md-calendar-escape');
237+
return;
238+
}
239+
240+
// Remaining key events fall into two categories: selection and navigation.
231241
// Start by checking if this is a selection event.
232242
if (event.which === self.keyCode.ENTER) {
233243
self.setNgModelValue(self.displayDate);
@@ -292,6 +302,11 @@
292302
var cell = this.calendarElement.querySelector('#' + cellId);
293303
cell.focus();
294304
};
305+
306+
/** Focus the calendar. */
307+
CalendarCtrl.prototype.focus = function() {
308+
this.focusDateElement(this.selectedDate);
309+
};
295310

296311

297312
/*** Animation ***/
@@ -470,11 +485,15 @@
470485
*/
471486
CalendarCtrl.prototype.changeSelectedDate = function(date) {
472487
var self = this;
488+
var previousSelectedDate = this.selectedDate;
489+
this.selectedDate = date;
490+
473491
this.changeDisplayDate(date).then(function() {
474492

475493
// Remove the selected class from the previously selected date, if any.
476-
if (self.selectedDate) {
477-
var prevDateCell = self.calendarElement.querySelector('#' + self.getDateId_(self.selectedDate));
494+
if (previousSelectedDate) {
495+
var prevDateCell =
496+
self.calendarElement.querySelector('#' + self.getDateId_(previousSelectedDate));
478497
if (prevDateCell) {
479498
prevDateCell.classList.remove(SELECTED_DATE_CLASS);
480499
}
@@ -487,8 +506,6 @@
487506
dateCell.classList.add(SELECTED_DATE_CLASS);
488507
}
489508
}
490-
491-
self.selectedDate = date;
492509
});
493510
};
494511

@@ -701,7 +718,7 @@
701718
CalendarCtrl.prototype.getDateId_ = function (date) {
702719
return [
703720
'md',
704-
this.directiveId,
721+
this.id,
705722
date.getFullYear(),
706723
date.getMonth(),
707724
date.getDate()

src/components/calendar/dateLocaleProvider.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777

7878
window.$locale = $locale;
7979

80+
// TODO(jelbourn): Freeze this object.
8081
return {
8182
months: this.months || $locale.DATETIME_FORMATS.MONTH,
8283
shortMonths: this.shortMonths || $locale.DATETIME_FORMATS.SHORTMONTH,

src/components/calendar/datePicker.js

Lines changed: 129 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,35 +12,58 @@
1212
function datePickerDirective() {
1313
return {
1414
template:
15-
'<div class="md-date-picker">' +
16-
'<input> <br>' +
17-
'<md-calendar ng-model="ctrl.date"></md-calendar>' +
18-
'</div>',
15+
'<input><button type="button" ng-click="ctrl.openCalendarPane()">📅</button>' +
16+
'<div class="md-date-calendar-pane">' +
17+
'<md-calendar ng-model="ctrl.date" ng-if="ctrl.isCalendarOpen"></md-calendar>' +
18+
'</div>',
19+
// <md-calendar ng-model="ctrl.date"></md-calendar>
1920
require: ['ngModel', 'mdDatePicker'],
2021
scope: {},
2122
controller: DatePickerCtrl,
2223
controllerAs: 'ctrl',
2324
link: function(scope, element, attr, controllers) {
2425
var ngModelCtrl = controllers[0];
2526
var mdDatePickerCtrl = controllers[1];
27+
2628
mdDatePickerCtrl.configureNgModel(ngModelCtrl);
2729
}
2830
};
2931
}
3032

31-
function DatePickerCtrl($scope, $element, $$mdDateLocale, $$mdDateUtil) {
33+
/**
34+
* Controller for md-date-picker.
35+
*
36+
* @ngInject @constructor
37+
*/
38+
function DatePickerCtrl($scope, $element, $compile, $timeout, $mdConstant, $mdUtil,
39+
$$mdDateLocale, $$mdDateUtil) {
40+
/** @final */
41+
this.$compile = $compile;
42+
43+
/** @final */
44+
this.$timeout = $timeout;
45+
3246
/** @final */
3347
this.dateLocale = $$mdDateLocale;
3448

3549
/** @final */
3650
this.dateUtil = $$mdDateUtil;
3751

52+
/** @final */
53+
this.$mdConstant = $mdConstant;
54+
55+
/* @final */
56+
this.$mdUtil = $mdUtil;
57+
3858
/** @type {!angular.NgModelController} */
3959
this.ngModelCtrl = null;
4060

4161
/** @type {HTMLInputElement} */
4262
this.inputElement = $element[0].querySelector('input');
4363

64+
/** @type {HTMLElement} Floating calendar pane (instantiated lazily) */
65+
this.calendarPane = $element[0].querySelector('.md-date-calendar-pane');
66+
4467
/** @type {Date} */
4568
this.date = null;
4669

@@ -50,7 +73,19 @@
5073
/** @final {!angular.Scope} */
5174
this.$scope = $scope;
5275

76+
/** @type {boolean} Whether the date-picker's calendar pane is open. */
77+
this.isCalendarOpen = false;
78+
79+
/** Pre-bound click handler is saved so that the event listener can be removed. */
80+
this.bodyClickHandler = this.handleBodyClick.bind(this);
81+
5382
this.attachChangeListeners();
83+
this.attachInterationListeners();
84+
85+
var self = this;
86+
$scope.$on('$destroy', function() {
87+
self.detachCalendarPane();
88+
});
5489
}
5590

5691
/**
@@ -78,6 +113,7 @@
78113
self.$scope.$on('md-calendar-change', function(event, date) {
79114
self.ngModelCtrl.$setViewValue(date);
80115
self.inputElement.value = self.dateLocale.formatDate(date);
116+
self.closeCalendarPane();
81117
});
82118

83119
// TODO(jelbourn): debounce
@@ -89,4 +125,92 @@
89125
}
90126
});
91127
};
128+
129+
/** Attach event listeners for user interaction. */
130+
DatePickerCtrl.prototype.attachInterationListeners = function() {
131+
var self = this;
132+
var $scope = this.$scope;
133+
var keyCodes = this.$mdConstant.KEY_CODE;
134+
135+
self.inputElement.addEventListener('keydown', function(event) {
136+
$scope.$apply(function() {
137+
if (event.altKey && event.keyCode == keyCodes.DOWN_ARROW) {
138+
self.openCalendarPane();
139+
}
140+
});
141+
});
142+
143+
self.$scope.$on('md-calendar-escape', function() {
144+
self.closeCalendarPane();
145+
});
146+
};
147+
148+
/** Position and attach the floating calendar to the document. */
149+
DatePickerCtrl.prototype.attachCalendarPane = function() {
150+
var elementRect = this.$element[0].getBoundingClientRect();
151+
152+
this.calendarPane.style.left = elementRect.left + 'px';
153+
this.calendarPane.style.top = elementRect.bottom + 'px';
154+
document.body.appendChild(this.calendarPane);
155+
};
156+
157+
/** Detach the floating calendar pane from the document. */
158+
DatePickerCtrl.prototype.detachCalendarPane = function() {
159+
// Use native DOM removal because we do not want any of the angular state of this element
160+
// to be disposed.
161+
this.calendarPane.parentNode.removeChild(this.calendarPane);
162+
};
163+
164+
/** Open the floating calendar pane. */
165+
DatePickerCtrl.prototype.openCalendarPane = function() {
166+
if (!this.isCalendarOpen) {
167+
this.isCalendarOpen = true;
168+
this.attachCalendarPane();
169+
// TODO(jelbourn): dispatch to tell other date pickers to close.
170+
this.focusCalendar();
171+
172+
// Attach click listener inside of a timeout because, if this open call was triggered by a
173+
// click, we don't want it to be immediately propogated up to the body and handled.
174+
var self = this;
175+
this.$timeout(function() {
176+
document.body.addEventListener('click', self.bodyClickHandler);
177+
}, 0, false);
178+
}
179+
};
180+
181+
/** Close the floating calendar pane. */
182+
DatePickerCtrl.prototype.closeCalendarPane = function() {
183+
this.isCalendarOpen = false;
184+
this.detachCalendarPane();
185+
this.inputElement.focus();
186+
document.body.removeEventListener('click', this.bodyClickHandler);
187+
};
188+
189+
/** Gets the controller instance for the calendar in the floating pane. */
190+
DatePickerCtrl.prototype.getCalendarCtrl = function() {
191+
return angular.element(this.calendarPane.querySelector('md-calendar')).controller('mdCalendar');
192+
};
193+
194+
/** Focus the calendar in the floating pane. */
195+
DatePickerCtrl.prototype.focusCalendar = function() {
196+
// Use a timeout in order to allow the calendar to be rendered, as it is gated behind an ng-if.
197+
var self = this;
198+
this.$timeout(function() {
199+
self.getCalendarCtrl().focus();
200+
}, 0, false);
201+
};
202+
203+
/**
204+
* Handles a click on the document body when the floating calendar pane is open.
205+
* Closes the floating calendar pane if the click is not inside of it.
206+
* @param {MouseEvent} event
207+
*/
208+
DatePickerCtrl.prototype.handleBodyClick = function(event) {
209+
if (this.isCalendarOpen) {
210+
var isInCalendar = this.$mdUtil.getClosest(event.target, 'md-calendar');
211+
if (!isInCalendar) {
212+
this.closeCalendarPane();
213+
}
214+
}
215+
};
92216
})();
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.md-date-calendar-pane {
2+
position: absolute;
3+
top: 0;
4+
left: 0;
5+
6+
// DEBUG
7+
box-shadow: 0 4px 4px;
8+
background: white;
9+
}

src/components/calendar/demoDatePicker/index.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ <h2>Development tools</h2>
2222
<p>Here is a bunch of stuff after the calendar</p>
2323
<p>Here is a bunch of stuff after the calendar</p>
2424
<p>Here is a bunch of stuff after the calendar</p>
25-
<input>
2625

2726
</md-content>
2827
</div>

0 commit comments

Comments
 (0)