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

Commit fa7b383

Browse files
committed
fix(calendar): fix focus behavior on open
1 parent 56df8d5 commit fa7b383

File tree

4 files changed

+70
-41
lines changed

4 files changed

+70
-41
lines changed

src/components/calendar/calendar.js

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,24 @@
1111
'material.core', 'material.components.icon'
1212
]).directive('mdCalendar', calendarDirective);
1313

14-
// FUTURE VERSION
15-
// TODO(jelbourn): Animated month transition on ng-model change.
16-
// TODO(jelbourn): Scroll snapping
17-
// TODO(jelbourn): Month headers stick to top when scrolling
18-
// TODO(jelbourn): Previous month opacity is lowered when partially scrolled out of view.
19-
// TODO(jelbourn): Support md-calendar standalone on a page (as a tabstop w/ aria-live
20-
// announcement and key handling).
21-
2214

2315
// PRE RELEASE
2416
// TODO(jelbourn): Base colors on the theme
25-
// TODO(jelbourn): Align style with spec
2617
// TODO(jelbourn): read-only state.
2718
// TODO(jelbourn): Make sure the *time* on the written date makes sense (probably midnight).
2819
// TODO(jelbourn): Date "isComplete" logic
2920
// TODO(jelbourn): Apple + up / down == PgDown and PgUp
3021
// TODO(jelbourn): Documentation
3122
// TODO(jelbourn): Demo that uses moment.js
23+
// TODO(jelbourn): Fix NVDA stealing key presses (IE) ???
24+
25+
// FUTURE VERSION
26+
// TODO(jelbourn): Animated month transition on ng-model change.
27+
// TODO(jelbourn): Scroll snapping
28+
// TODO(jelbourn): Month headers stick to top when scrolling
29+
// TODO(jelbourn): Previous month opacity is lowered when partially scrolled out of view.
30+
// TODO(jelbourn): Support md-calendar standalone on a page (as a tabstop w/ aria-live
31+
// announcement and key handling).
3232

3333
// COULD GO EITHER WAY
3434
// TODO(jelbourn): Clicking on the month label opens the month-picker.
@@ -47,10 +47,10 @@
4747
'<table class="md-calendar-day-header"><thead></thead></table>' +
4848
'<div class="md-calendar-scroll-mask">' +
4949
'<md-virtual-repeat-container class="md-calendar-scroller">' +
50-
'<table class="md-calendar">' +
51-
'<tbody md-virtual-repeat="i in ctrl.items" md-calendar-month ' +
50+
'<table role="grid" class="md-calendar">' +
51+
'<tbody role="rowgroup" md-virtual-repeat="i in ctrl.items" md-calendar-month ' +
5252
'md-month-offset="$index" class="md-calendar-month" aria-hidden="true" ' +
53-
'md-start-index="1000" ' +
53+
'md-start-index="ctrl.getSelectedMonthIndex()" ' +
5454
'md-item-size="' + TBODY_HEIGHT + '"></tbody>' +
5555
'</table>' +
5656
'</md-virtual-repeat-container>' +
@@ -87,7 +87,7 @@
8787
$$mdDateUtil, $$mdDateLocale, $mdInkRipple, $mdUtil) {
8888

8989
/** @type {Array<number>} Dummy array-like object for virtual-repeat to iterate over. */
90-
this.items = {length: 2000};
90+
this.items = {length: 2000 * 12};
9191

9292
/** @final {!angular.$animate} */
9393
this.$animate = $animate;
@@ -156,6 +156,12 @@
156156
*/
157157
this.displayDate = null;
158158

159+
/**
160+
* The date that has or should have browser focus.
161+
* @type {Date}
162+
*/
163+
this.focusDate = null;
164+
159165
/** @type {boolean} */
160166
this.isInitialized = false;
161167

@@ -184,12 +190,6 @@
184190
}
185191
};
186192

187-
// Do a one-time scroll to the selected date once the months have done their initial render.
188-
var off = $scope.$on('md-calendar-month-initial-render', function() {
189-
//self.scrollToMonth(self.selectedDate);
190-
off();
191-
});
192-
193193
this.attachCalendarEventListeners();
194194

195195
// DEBUG
@@ -224,6 +224,14 @@
224224
this.isInitialized = true;
225225
};
226226

227+
/**
228+
* Gets the "index" of the currently selected date as it would be in the virtual-repeat.
229+
* @returns {number}
230+
*/
231+
CalendarCtrl.prototype.getSelectedMonthIndex = function() {
232+
return this.dateUtil.getMonthDistance(firstRenderableDate, this.selectedDate || this.today);
233+
};
234+
227235
/**
228236
* Hides the vertical scrollbar on the calendar scroller by setting the width on the
229237
* calendar scroller and the `overflow: hidden` wrapper around the scroller, and then setting
@@ -263,7 +271,7 @@
263271
*/
264272
CalendarCtrl.prototype.attachCalendarEventListeners = function() {
265273
// Keyboard interaction.
266-
angular.element(this.calendarElement).on('keydown', this.handleKeyEvent.bind(this));
274+
this.$element.on('keydown', this.handleKeyEvent.bind(this));
267275
};
268276

269277
/*** User input handling ***/
@@ -293,11 +301,7 @@
293301

294302
// Selection isn't occuring, so the key event is either navigation or nothing.
295303
var date = self.getFocusDateFromKeyEvent(event);
296-
297-
// Prevent the default on the key event only if it triggered a date navigation.
298-
if (!self.dateUtil.isSameDay(date, self.displayDate)) {
299-
event.preventDefault();
300-
}
304+
event.preventDefault();
301305

302306
// Since this is a keyboard interaction, actually give the newly focused date keyboard
303307
// focus after the been brought into view.
@@ -344,10 +348,13 @@
344348
* @param {Date=} opt_date
345349
*/
346350
CalendarCtrl.prototype.focus = function(opt_date) {
347-
var cellId = this.getDateId(opt_date || this.selectedDate);
351+
var date = opt_date || this.selectedDate;
352+
var cellId = this.getDateId(date);
348353
var cell = this.calendarElement.querySelector('#' + cellId);
349354
if (cell) {
350355
cell.focus();
356+
} else {
357+
this.focusDate = date;
351358
}
352359
};
353360

src/components/calendar/calendarMonth.js

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@
2525
monthCtrl.calendarCtrl = calendarCtrl;
2626
monthCtrl.generateContent();
2727

28-
// Emit an event to let the parent md-calendar know that initial render has happened.
29-
scope.$emit('md-calendar-month-initial-render');
30-
3128
// The virtual-repeat re-uses the same DOM elements, so there are only a limited number
3229
// of repeated items that are linked, and then those elements have their bindings updataed.
3330
// Since the months are not generated by bindings, we simply regenerate the entire thing
@@ -63,6 +60,12 @@
6360
* @type {number}
6461
*/
6562
this.offset = 0;
63+
64+
/**
65+
* Date cell to focus after appending the month to the document.
66+
* @type {HTMLElement}
67+
*/
68+
this.focusAfterAppend = null;
6669
}
6770

6871
/** Generate and append the content for this month to the directive element. */
@@ -71,6 +74,10 @@
7174
var date = this.dateUtil.incrementMonths(this.calendarCtrl.today, offset);
7275
this.$element.empty();
7376
this.$element.append(this.buildCalendarForMonth(date));
77+
if (this.focusAfterAppend) {
78+
this.focusAfterAppend.focus();
79+
this.focusAfterAppend = null;
80+
}
7481
};
7582

7683
/**
@@ -81,9 +88,12 @@
8188
* @returns {HTMLElement}
8289
*/
8390
CalendarMonthCtrl.prototype.buildDateCell = function(opt_date) {
91+
var calendarCtrl = this.calendarCtrl;
92+
8493
// TODO(jelbourn): cloneNode is likely a faster way of doing this.
8594
var cell = document.createElement('td');
8695
cell.classList.add('md-calendar-date');
96+
cell.setAttribute('role', 'gridcell');
8797

8898
if (opt_date) {
8999
// Add a indicator for select, hover, and focus states.
@@ -93,26 +103,36 @@
93103
selectionIndicator.textContent = this.dateLocale.dates[opt_date.getDate()];
94104

95105
cell.setAttribute('tabindex', '-1');
96-
cell.id = this.calendarCtrl.getDateId(opt_date);
97-
cell.addEventListener('click', this.calendarCtrl.cellClickHandler);
106+
cell.id = calendarCtrl.getDateId(opt_date);
107+
cell.addEventListener('click', calendarCtrl.cellClickHandler);
98108

99109
// Use `data-timestamp` attribute because IE10 does not support the `dataset` property.
100110
cell.setAttribute('data-timestamp', opt_date.getTime());
101111

102112
// TODO(jelourn): Doing these comparisons for class addition during generation might be slow.
103113
// It may be better to finish the construction and then query the node and add the class.
104-
if (this.dateUtil.isSameDay(opt_date, this.calendarCtrl.today)) {
114+
if (this.dateUtil.isSameDay(opt_date, calendarCtrl.today)) {
105115
cell.classList.add(TODAY_CLASS);
106116
}
107117

108-
if (this.dateUtil.isValidDate(this.calendarCtrl.selectedDate) &&
109-
this.dateUtil.isSameDay(opt_date, this.calendarCtrl.selectedDate)) {
118+
if (this.dateUtil.isValidDate(calendarCtrl.selectedDate) &&
119+
this.dateUtil.isSameDay(opt_date, calendarCtrl.selectedDate)) {
110120
cell.classList.add(SELECTED_DATE_CLASS);
111121
}
122+
123+
if (calendarCtrl.focusDate && this.dateUtil.isSameDay(opt_date, calendarCtrl.focusDate)) {
124+
this.focusAfterAppend = cell;
125+
}
112126
}
113127

114128
return cell;
115129
};
130+
131+
CalendarMonthCtrl.prototype.buildDateRow = function() {
132+
var row = document.createElement('tr');
133+
row.setAttribute('role', 'row');
134+
return row;
135+
};
116136

117137
/**
118138
* Builds the <tbody> content for the given date's month.
@@ -129,7 +149,7 @@
129149
// Store rows for the month in a document fragment so that we can append them all at once.
130150
var monthBody = document.createDocumentFragment();
131151

132-
var row = document.createElement('tr');
152+
var row = this.buildDateRow();
133153
monthBody.appendChild(row);
134154

135155
// Add a label for the month. If the month starts on a Sun/Mon/Tues, the month label
@@ -141,7 +161,7 @@
141161
if (firstDayOfTheWeek <= 2) {
142162
monthLabelCell.setAttribute('colspan', '7');
143163

144-
var monthLabelRow = document.createElement('tr');
164+
var monthLabelRow = this.buildDateRow();
145165
monthLabelRow.appendChild(monthLabelCell);
146166
monthBody.insertBefore(monthLabelRow, row);
147167
} else {
@@ -167,7 +187,7 @@
167187
// If we've reached the end of the week, start a new row.
168188
if (dayOfWeek === 7) {
169189
dayOfWeek = 0;
170-
row = document.createElement('tr');
190+
row = this.buildDateRow();
171191
monthBody.appendChild(row);
172192
}
173193

@@ -186,7 +206,7 @@
186206
// Ensure that all months have 6 rows. This is necessary for now because the virtual-repeat
187207
// requires that all items have exactly the same height.
188208
while (monthBody.childNodes.length < 6) {
189-
var whitespaceRow = document.createElement('tr');
209+
var whitespaceRow = this.buildDateRow();
190210
for (var i = 0; i < 7; i++) {
191211
whitespaceRow.appendChild(this.buildDateCell());
192212
}

src/components/calendar/datePicker.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
'use strict';
33

44
// PRE RELEASE
5-
// TODO(jelbourn): make down-arrow a button to open the calendar
65
// TODO(jelbourn): aria attributes tying together date input and floating calendar.
76
// TODO(jelbourn): something for mobile (probably calendar panel should take up entire screen)
87
// TODO(jelbourn): make sure this plays well with validation and ngMessages.
@@ -251,6 +250,8 @@
251250
// Add shadow to the calendar pane only after the UI thread has reached idle, allowing the
252251
// content of the calender pane to be rendered.
253252
this.$timeout(function() {
253+
this.calendarPane.classList.add('md-pane-open');
254+
254255
this.calendarShadow.style.top = (elementRect.top - bodyRect.top) + 'px';
255256
this.calendarShadow.style.left = this.calendarPane.style.left;
256257
this.calendarShadow.style.height =
@@ -262,6 +263,7 @@
262263
/** Detach the floating calendar pane from the document. */
263264
DatePickerCtrl.prototype.detachCalendarPane = function() {
264265
this.$element.removeClass('md-datepicker-open');
266+
this.calendarPane.classList.remove('md-pane-open');
265267

266268
// Use native DOM removal because we do not want any of the angular state of this element
267269
// to be disposed.

src/components/calendar/datePicker.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ $md-datepicker-button-gap: 12px;
6464
width: $md-calendar-width;
6565
}
6666

67-
// Down "disclosure" triangle/arrow indicating that the datepicker can be opened.
67+
// Down triangle/arrow indicating that the datepicker can be opened.
6868
// We can do this entirely with CSS without needing to load an icon.
6969
// See https://css-tricks.com/snippets/css/css-triangle/
7070
$md-date-arrow-size: 6px;
@@ -116,7 +116,7 @@ $md-date-arrow-size: 6px;
116116
height: 40px;
117117
}
118118

119-
.md-datepicker-expand-triangle {
119+
.md-datepicker-triangle-button {
120120
display: none;
121121
}
122122

0 commit comments

Comments
 (0)