Skip to content

Commit

Permalink
feat(datepicker): Add min/max dates in datepicker
Browse files Browse the repository at this point in the history
Allows date objects to be passed in to the datepicker to specify minimum
date and maximum date.

Fixes angular#4158. Closes angular#4306.
  • Loading branch information
Michael Chen authored and kennethcachia committed Sep 23, 2015
1 parent 5004a78 commit 7a48c3b
Show file tree
Hide file tree
Showing 11 changed files with 331 additions and 58 deletions.
8 changes: 8 additions & 0 deletions src/components/datepicker/calendar-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@

.md-calendar-date.md-calendar-date-today {
color: '{{primary-500}}'; // blue-500

&.md-calendar-date-disabled {
color: '{{primary-500-0.6}}';
}
}

// The CSS class `md-focus` is used instead of real browser focus for accessibility reasons
Expand All @@ -39,4 +43,8 @@
}
}

.md-calendar-date-disabled,
.md-calendar-month-label-disabled {
color: '{{background-400}}'; // grey-400
}
}
62 changes: 55 additions & 7 deletions src/components/datepicker/calendar.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,19 @@
*/
var TBODY_HEIGHT = 265;

/**
* Height of a calendar month with a single row. This is needed to calculate the offset for
* rendering an extra month in virtual-repeat that only contains one row.
*/
var TBODY_SINGLE_ROW_HEIGHT = 45;

function calendarDirective() {
return {
template:
'<table aria-hidden="true" class="md-calendar-day-header"><thead></thead></table>' +
'<div class="md-calendar-scroll-mask">' +
'<md-virtual-repeat-container class="md-calendar-scroll-container">' +
'<md-virtual-repeat-container class="md-calendar-scroll-container" ' +
'md-offset-size="' + (TBODY_SINGLE_ROW_HEIGHT - TBODY_HEIGHT) + '">' +
'<table role="grid" tabindex="0" class="md-calendar" aria-readonly="true">' +
'<tbody role="rowgroup" md-virtual-repeat="i in ctrl.items" md-calendar-month ' +
'md-month-offset="$index" class="md-calendar-month" ' +
Expand All @@ -48,7 +55,10 @@
'</table>' +
'</md-virtual-repeat-container>' +
'</div>',
scope: {},
scope: {
minDate: '=mdMinDate',
maxDate: '=mdMaxDate',
},
require: ['ngModel', 'mdCalendar'],
controller: CalendarCtrl,
controllerAs: 'ctrl',
Expand Down Expand Up @@ -87,6 +97,15 @@
*/
this.items = {length: 2000};

if (this.maxDate && this.minDate) {
// Limit the number of months if min and max dates are set.
var numMonths = $$mdDateUtil.getMonthDistance(this.minDate, this.maxDate) + 1;
numMonths = Math.max(numMonths, 1);
// Add an additional month as the final dummy month for rendering purposes.
numMonths += 1;
this.items.length = numMonths;
}

/** @final {!angular.$animate} */
this.$animate = $animate;

Expand Down Expand Up @@ -123,9 +142,19 @@
/** @final {Date} */
this.today = this.dateUtil.createDateAtMidnight();

// Set the first renderable date once for all calendar instances.
firstRenderableDate =
firstRenderableDate || this.dateUtil.incrementMonths(this.today, -this.items.length / 2);
/** @type {Date} */
this.firstRenderableDate = this.dateUtil.incrementMonths(this.today, -this.items.length / 2);

if (this.minDate && this.minDate > this.firstRenderableDate) {
this.firstRenderableDate = this.minDate;
} else if (this.maxDate) {
// Calculate the difference between the start date and max date.
// Subtract 1 because it's an inclusive difference and 1 for the final dummy month.
//
var monthDifference = this.items.length - 2;
this.firstRenderableDate = this.dateUtil.incrementMonths(this.maxDate, -(this.items.length - 2));
}


/** @final {number} Unique ID for this calendar instance. */
this.id = nextUniqueId++;
Expand Down Expand Up @@ -279,6 +308,7 @@
// Selection isn't occuring, so the key event is either navigation or nothing.
var date = self.getFocusDateFromKeyEvent(event);
if (date) {
date = self.boundDateByMinAndMax(date);
event.preventDefault();
event.stopPropagation();

Expand Down Expand Up @@ -324,7 +354,8 @@
* @returns {number}
*/
CalendarCtrl.prototype.getSelectedMonthIndex = function() {
return this.dateUtil.getMonthDistance(firstRenderableDate, this.selectedDate || this.today);
return this.dateUtil.getMonthDistance(this.firstRenderableDate,
this.selectedDate || this.today);
};

/**
Expand All @@ -336,7 +367,7 @@
return;
}

var monthDistance = this.dateUtil.getMonthDistance(firstRenderableDate, date);
var monthDistance = this.dateUtil.getMonthDistance(this.firstRenderableDate, date);
this.calendarScroller.scrollTop = monthDistance * TBODY_HEIGHT;
};

Expand Down Expand Up @@ -372,6 +403,23 @@
}
};

/**
* If a date exceeds minDate or maxDate, returns date matching minDate or maxDate, respectively.
* Otherwise, returns the date.
* @param {Date} date
* @return {Date}
*/
CalendarCtrl.prototype.boundDateByMinAndMax = function(date) {
var boundDate = date;
if (this.minDate && date < this.minDate) {
boundDate = new Date(this.minDate.getTime());
}
if (this.maxDate && date > this.maxDate) {
boundDate = new Date(this.maxDate.getTime());
}
return boundDate;
};

/*** Updating the displayed / selected date ***/

/**
Expand Down
11 changes: 8 additions & 3 deletions src/components/datepicker/calendar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ $md-calendar-width: (7 * $md-calendar-cell-size) + (2 * $md-calendar-side-paddin
$md-calendar-height:
($md-calendar-weeks-to-show * $md-calendar-cell-size) + $md-calendar-header-height;


// Styles for date cells, including day-of-the-week header cells.
@mixin md-calendar-cell() {
height: $md-calendar-cell-size;
Expand Down Expand Up @@ -88,6 +87,10 @@ md-calendar {
// A single date cell in the calendar table.
.md-calendar-date {
@include md-calendar-cell();

&.md-calendar-date-disabled {
cursor: default;
}
}

// Circle element inside of every date cell used to indicate selection or focus.
Expand All @@ -97,11 +100,13 @@ md-calendar {
border-radius: 50%;
display: inline-block;

cursor: pointer;

width: $md-calendar-cell-emphasis-size;
height: $md-calendar-cell-emphasis-size;
line-height: $md-calendar-cell-emphasis-size;

.md-calendar-date:not(.md-disabled) & {
cursor: pointer;
}
}

// The label above each month (containing the month name and the year, e.g. "Jun 2014").
Expand Down
131 changes: 129 additions & 2 deletions src/components/datepicker/calendar.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,23 @@ describe('md-calendar', function() {
}
}

/**
* Finds a month `tbody` in the calendar element given a date.
*/
function findMonthElement(date) {
var months = element.querySelectorAll('[md-calendar-month]');
var monthHeader = dateLocale.monthHeaderFormatter(date);
var month;

for (var i = 0; i < months.length; i++) {
month = months[i];
if (month.querySelector('tr:first-child td:first-child').textContent === monthHeader) {
return month;
}
}
return null;
}

/**
* Gets the month label for a given date cell.
* @param {HTMLElement|DocumentView} cell
Expand All @@ -63,7 +80,8 @@ describe('md-calendar', function() {
/** Creates and compiles an md-calendar element. */
function createElement(parentScope) {
var directiveScope = parentScope || $rootScope.$new();
var template = '<md-calendar ng-model="myDate"></md-calendar>';
var template = '<md-calendar md-min-date="minDate" md-max-date="maxDate" ' +
'ng-model="myDate"></md-calendar>';
var attachedElement = angular.element(template);
document.body.appendChild(attachedElement[0]);
var newElement = $compile(attachedElement)(directiveScope);
Expand Down Expand Up @@ -135,7 +153,7 @@ describe('md-calendar', function() {

ngElement = createElement(pageScope);
element = ngElement[0];
scope = ngElement.scope();
scope = ngElement.isolateScope();
controller = ngElement.controller('mdCalendar');
}));

Expand Down Expand Up @@ -227,6 +245,40 @@ describe('md-calendar', function() {
var monthHeader = monthElement.querySelector('tr');
expect(monthHeader.textContent).toEqual('Junz 2014');
});

it('should update the model on cell click', function() {
spyOn(scope, '$emit');
var date = new Date(2014, MAY, 30);
var monthElement = monthCtrl.buildCalendarForMonth(date);
var expectedDate = new Date(2014, MAY, 5);
findDateElement(monthElement, 5).click();
expect(pageScope.myDate).toBeSameDayAs(expectedDate);
expect(scope.$emit).toHaveBeenCalledWith('md-calendar-change', expectedDate);
});

it('should disable any dates outside the min/max date range', function() {
pageScope.minDate = new Date(2014, JUN, 10);
pageScope.maxDate = new Date(2014, JUN, 20);
pageScope.$apply();

var monthElement = monthCtrl.buildCalendarForMonth(new Date(2014, JUN, 15));
expect(findDateElement(monthElement, 5)).toHaveClass('md-calendar-date-disabled');
expect(findDateElement(monthElement, 10)).not.toHaveClass('md-calendar-date-disabled');
expect(findDateElement(monthElement, 20)).not.toHaveClass('md-calendar-date-disabled');
expect(findDateElement(monthElement, 25)).toHaveClass('md-calendar-date-disabled');
});

it('should not respond to disabled cell clicks', function() {
var initialDate = new Date(2014, JUN, 15);
pageScope.myDate = initialDate;
pageScope.minDate = new Date(2014, JUN, 10);
pageScope.maxDate = new Date(2014, JUN, 20);
pageScope.$apply();

var monthElement = monthCtrl.buildCalendarForMonth(pageScope.myDate);
findDateElement(monthElement, 5).click();
expect(pageScope.myDate).toBeSameDayAs(initialDate);
});
});

it('should highlight today', function() {
Expand Down Expand Up @@ -325,6 +377,41 @@ describe('md-calendar', function() {
expect(controller.selectedDate).toBeSameDayAs(new Date(2014, MAR, 1));
});

it('should restrict date navigation to min/max dates', function() {
pageScope.minDate = new Date(2014, FEB, 5);
pageScope.maxDate = new Date(2014, FEB, 10);
pageScope.myDate = new Date(2014, FEB, 8);
applyDateChange();

var selectedDate = element.querySelector('.md-calendar-selected-date');
selectedDate.focus();

dispatchKeyEvent(keyCodes.UP_ARROW);
expect(getFocusedDateElement().textContent).toBe('5');
expect(getMonthLabelForDateCell(getFocusedDateElement())).toBe('Feb 2014');

dispatchKeyEvent(keyCodes.LEFT_ARROW);
expect(getFocusedDateElement().textContent).toBe('5');
expect(getMonthLabelForDateCell(getFocusedDateElement())).toBe('Feb 2014');

dispatchKeyEvent(keyCodes.DOWN_ARROW);
expect(getFocusedDateElement().textContent).toBe('10');
expect(getMonthLabelForDateCell(getFocusedDateElement())).toBe('Feb 2014');

dispatchKeyEvent(keyCodes.RIGHT_ARROW);
expect(getFocusedDateElement().textContent).toBe('10');
expect(getMonthLabelForDateCell(getFocusedDateElement())).toBe('Feb 2014');

dispatchKeyEvent(keyCodes.UP_ARROW, {meta: true});
expect(getFocusedDateElement().textContent).toBe('5');
expect(getMonthLabelForDateCell(getFocusedDateElement())).toBe('Feb 2014');

dispatchKeyEvent(keyCodes.DOWN_ARROW, {meta: true});
expect(getFocusedDateElement().textContent).toBe('10');
expect(getMonthLabelForDateCell(getFocusedDateElement())).toBe('Feb 2014');

});

it('should fire an event when escape is pressed', function() {
var escapeHandler = jasmine.createSpy('escapeHandler');
pageScope.$on('md-calendar-close', escapeHandler);
Expand Down Expand Up @@ -354,4 +441,44 @@ describe('md-calendar', function() {
controller.changeDisplayDate(laterDate);
expect(controller.displayDate).toBeSameDayAs(laterDate);
});

it('should not render any months before the min date', function() {
ngElement.remove();
var newScope = $rootScope.$new();
newScope.minDate = new Date(2014, JUN, 5);
newScope.myDate = new Date(2014, JUN, 15);
newScope.$apply();
element = createElement(newScope)[0];

expect(findMonthElement(new Date(2014, JUL, 1))).not.toBeNull();
expect(findMonthElement(new Date(2014, JUN, 1))).not.toBeNull();
expect(findMonthElement(new Date(2014, MAY, 1))).toBeNull();
});

it('should render one single-row month of disabled cells after the max date', function() {
ngElement.remove();
var newScope = $rootScope.$new();
newScope.myDate = new Date(2014, APR, 15);
newScope.maxDate = new Date(2014, APR, 30);
newScope.$apply();
element = createElement(newScope)[0];

expect(findMonthElement(new Date(2014, MAR, 1))).not.toBeNull();
expect(findMonthElement(new Date(2014, APR, 1))).not.toBeNull();

// First date of May 2014 on Thursday (i.e. has 3 dates on the first row).
var nextMonth = findMonthElement(new Date(2014, MAY, 1));
expect(nextMonth).not.toBeNull();
expect(nextMonth.querySelector('.md-calendar-month-label')).toHaveClass(
'md-calendar-month-label-disabled');
expect(nextMonth.querySelectorAll('tr').length).toBe(1);

var dates = nextMonth.querySelectorAll('.md-calendar-date');
for (var i = 0; i < dates.length; i++) {
date = dates[i];
if (date.textContent) {
expect(date).toHaveClass('md-calendar-date-disabled');
}
}
});
});
Loading

0 comments on commit 7a48c3b

Please sign in to comment.