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

feat(datepicker): Add min/max dates in datepicker #4306

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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