Repository: mlaursen/react-md
Pull request #91 (2016-05-17 13:20:19)

Enhancement/#56 date picker tests

mlaursen: This should solve #56 

It also fixes:
- The `onChange` function was not being called correctly.
- Did not allow null value
- Did not allow Date object as value

Patch:
diff --git a/src/js/Dialogs/__mocks__/index.js b/src/js/Dialogs/__mocks__/index.js
new file mode 100644
index 0000000000..52396a7267
--- /dev/null
+++ b/src/js/Dialogs/__mocks__/index.js
@@ -0,0 +1,8 @@
+/*eslint-env jest*/
+import React from 'react';
+
+export default jest.genMockFunction().mockImplementation(({ isOpen, children }) => {
+  return (
+    <div>{isOpen && children}</div>
+  );
+});
diff --git a/src/js/Pickers/CalendarDate.js b/src/js/Pickers/CalendarDate.js
new file mode 100644
index 0000000000..6652ac0741
--- /dev/null
+++ b/src/js/Pickers/CalendarDate.js
@@ -0,0 +1,59 @@
+import React, { Component, PropTypes } from 'react';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import classnames from 'classnames';
+
+/**
+ * This component renders a selectable date in the `CalendarMonth` component.
+ */
+export default class CalendarDate extends Component {
+  constructor(props) {
+    super(props);
+
+    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
+    this.state = this.getFormattedDate(props);
+  }
+
+  static propTypes = {
+    className: PropTypes.string,
+    date: PropTypes.instanceOf(Date).isRequired,
+    DateTimeFormat: PropTypes.func.isRequired,
+    locales: PropTypes.oneOfType([
+      PropTypes.string,
+      PropTypes.arrayOf(PropTypes.string),
+    ]).isRequired,
+    disabled: PropTypes.bool,
+    onClick: PropTypes.func.isRequired,
+  };
+
+  componentWillUpdate(nextProps) {
+    const { DateTimeFormat, locales, date } = this.props;
+    if(DateTimeFormat !== nextProps.DateTimeFormat || locales !== nextProps.locales || date !== nextProps.date) {
+      this.setState(this.getFormattedDate(nextProps));
+    }
+  }
+
+  handleClick = (e) => {
+    this.props.onClick(new Date(this.props.date), e);
+  };
+
+  getFormattedDate = ({ DateTimeFormat, locales, date }) => {
+    return {
+      date: DateTimeFormat(locales, { day: 'numeric' }).format(date),
+    };
+  };
+
+  render() {
+    const { date } = this.state;
+    const { disabled, className } = this.props;
+    return (
+      <button
+        type="button"
+        className={classnames('md-calendar-date', className)}
+        onClick={this.handleClick}
+        disabled={disabled}
+      >
+        <span className="date">{date}</span>
+      </button>
+    );
+  }
+}
diff --git a/src/js/Pickers/CalendarHeader.js b/src/js/Pickers/CalendarHeader.js
new file mode 100644
index 0000000000..ba87537980
--- /dev/null
+++ b/src/js/Pickers/CalendarHeader.js
@@ -0,0 +1,88 @@
+import React, { Component, PropTypes } from 'react';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+
+import IconButton from '../Buttons/IconButton';
+import { isMonthBefore, getDayOfWeek, addDate } from '../utils/dates';
+
+/**
+ * This component renders the controls for a `DatePicker`'s Calendar.
+ * This will render a next and previous month button along with the
+ * current month/year. It also renders the abbreviiations for the days
+ * of the week.
+ */
+export default class CalendarHeader extends Component {
+  constructor(props) {
+    super(props);
+
+    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
+    this.state = this.generateDows(props);
+  }
+
+  static propTypes = {
+    className: PropTypes.string,
+    children: PropTypes.node,
+    date: PropTypes.instanceOf(Date).isRequired,
+    minDate: PropTypes.instanceOf(Date),
+    maxDate: PropTypes.instanceOf(Date),
+    previousIcon: PropTypes.node.isRequired,
+    onPreviousClick: PropTypes.func.isRequired,
+    nextIcon: PropTypes.node.isRequired,
+    onNextClick: PropTypes.func.isRequired,
+    DateTimeFormat: PropTypes.func.isRequired,
+    locales: PropTypes.oneOfType([
+      PropTypes.string,
+      PropTypes.arrayOf(PropTypes.string),
+    ]).isRequired,
+  };
+
+  componentWillReceiveProps(nextProps) {
+    const { DateTimeFormat, locales } = this.props;
+    if(DateTimeFormat !== nextProps.DateTimeFormat || locales !== nextProps.locales) {
+      this.setState(this.generateDows(nextProps));
+    }
+  }
+
+  generateDows = ({ DateTimeFormat, locales, date } = this.props) => {
+    const sunday = getDayOfWeek(date, 0);
+    const formatter = DateTimeFormat(locales, { weekday: 'narrow' });
+    const dows = [];
+    for(let i = 0; i < 7; i++) {
+      const dow = formatter.format(addDate(sunday, i, 'D'));
+      dows.push(<h4 className="dow" key={i}>{dow}</h4>);
+    }
+
+    return { dows };
+  };
+
+  render() {
+    const { dows } = this.state;
+    const {
+      date,
+      minDate,
+      maxDate,
+      onPreviousClick,
+      previousIcon,
+      onNextClick,
+      nextIcon,
+      DateTimeFormat,
+      locales,
+    } = this.props;
+
+    const isPreviousDisabled = isMonthBefore(minDate, date);
+    const isNextDisabled = isMonthBefore(date, maxDate);
+    return (
+      <header className="md-calendar-header">
+        <div className="md-calendar-controls">
+          <IconButton onClick={onPreviousClick} disabled={isPreviousDisabled}>{previousIcon}</IconButton>
+          <h4 className="md-subtitle">
+            {DateTimeFormat(locales, { month: 'long', year: 'numeric' }).format(date)}
+          </h4>
+          <IconButton onClick={onNextClick} disabled={isNextDisabled}>{nextIcon}</IconButton>
+        </div>
+        <div className="md-dows">
+          {dows}
+        </div>
+      </header>
+    );
+  }
+}
diff --git a/src/js/Pickers/CalendarMonth.js b/src/js/Pickers/CalendarMonth.js
index 1dd46cbd57..d5465c4e26 100644
--- a/src/js/Pickers/CalendarMonth.js
+++ b/src/js/Pickers/CalendarMonth.js
@@ -3,7 +3,11 @@ import PureRenderMixin from 'react-addons-pure-render-mixin';
 import classnames from 'classnames';
 
 import { getDayOfWeek, addDate, stripTime, getLastDay } from '../utils/dates';
+import CalendarDate from './CalendarDate';
 
+/**
+ * This component renders a month for the calendar view of the `DatePicker`.
+ */
 export default class CalendarMonth extends Component {
   constructor(props) {
     super(props);
@@ -12,11 +16,38 @@ export default class CalendarMonth extends Component {
   }
 
   static propTypes = {
+    /**
+     * A className to apply.
+     */
     className: PropTypes.string,
+
+    /**
+     * The current selected date of the calendar. This is
+     * the date after hitting the Ok button or `value` || `defaultValue`.
+     */
     calendarDate: PropTypes.instanceOf(Date).isRequired,
+
+    /**
+     * The current selected date of the calendar before verifying
+     * the new date.
+     */
     calendarTempDate: PropTypes.instanceOf(Date).isRequired,
+
+    /**
+     * An optional min date for the calendar. This will disable any
+     * dates that come before this date in the month.
+     */
     minDate: PropTypes.instanceOf(Date),
+
+    /**
+     * An optional max date for the calendar. This will disable any
+     * dates that come after this date in the month.
+     */
     maxDate: PropTypes.instanceOf(Date),
+
+    /**
+     * A function to call that will select a new date.
+     */
     onCalendarDateClick: PropTypes.func.isRequired,
     DateTimeFormat: PropTypes.func.isRequired,
     locales: PropTypes.oneOfType([
@@ -38,31 +69,31 @@ export default class CalendarMonth extends Component {
       ...props,
     } = this.props;
 
-    let days = [];
+    const days = [];
     let currentDate = stripTime(getDayOfWeek(new Date(calendarDate).setDate(1), 0));
     const endDate = stripTime(getDayOfWeek(getLastDay(calendarDate), 6));
     const activeDate = stripTime(new Date(calendarTempDate));
     const today = stripTime(new Date());
 
     while(currentDate <= endDate) {
-      const key = DateTimeFormat(locales).format(currentDate);
+      const key = currentDate.getMonth() + '-' + currentDate.getDate();
       let date;
       if(currentDate.getMonth() === calendarDate.getMonth()) {
         const isMinDateDisabled = minDate && minDate.getTime() > currentDate.getTime();
         const isMaxDateDisbaled = maxDate && maxDate.getTime() < currentDate.getTime();
         date = (
-          <button
-            type="button"
+          <CalendarDate
             key={key}
-            className={classnames('md-calendar-date', {
+            className={classnames({
               'today': currentDate.getTime() === today.getTime(),
               'active': currentDate.getTime() === activeDate.getTime(),
             })}
-            onClick={onCalendarDateClick.bind(this, new Date(currentDate))}
             disabled={isMinDateDisabled || isMaxDateDisbaled}
-            >
-            <span className="date">{DateTimeFormat(locales, { day: 'numeric' }).format(currentDate)}</span>
-          </button>
+            onClick={onCalendarDateClick}
+            date={currentDate}
+            DateTimeFormat={DateTimeFormat}
+            locales={locales}
+          />
         );
       } else {
         date = <div key={key} className="md-calendar-date-placeholder" />;
diff --git a/src/js/Pickers/DatePicker.js b/src/js/Pickers/DatePicker.js
index 372a6cb265..6f093ddee9 100644
--- a/src/js/Pickers/DatePicker.js
+++ b/src/js/Pickers/DatePicker.js
@@ -50,6 +50,29 @@ export default class DatePicker extends Component {
       ...props,
     } = this.props;
 
+    let picker;
+    if(calendarMode === 'calendar') {
+      picker = (
+        <DatePickerCalendar
+          {...props}
+          key="calendar"
+          calendarTempDate={calendarTempDate}
+          DateTimeFormat={DateTimeFormat}
+          locales={locales}
+        />
+      );
+    } else {
+      picker = (
+        <YearPicker
+          {...props}
+          key="year"
+          calendarTempDate={calendarTempDate}
+          DateTimeFormat={DateTimeFormat}
+          locales={locales}
+        />
+      );
+    }
+
     return (
       <div className={`${className} date-picker`}>
         <DatePickerHeader
@@ -60,20 +83,7 @@ export default class DatePicker extends Component {
           changeCalendarMode={changeCalendarMode}
         />
         <div className="md-picker-content-container">
-          {calendarMode === 'calendar' ?
-            <DatePickerCalendar
-              {...props}
-              calendarTempDate={calendarTempDate}
-              DateTimeFormat={DateTimeFormat}
-              locales={locales}
-            /> :
-            <YearPicker
-              {...props}
-              calendarTempDate={calendarTempDate}
-              DateTimeFormat={DateTimeFormat}
-              locales={locales}
-            />
-          }
+          {picker}
           <PickerFooter
             okLabel={okLabel}
             okPrimary={okPrimary}
diff --git a/src/js/Pickers/DatePickerCalendar.js b/src/js/Pickers/DatePickerCalendar.js
index 41ae94a7ad..11766af226 100644
--- a/src/js/Pickers/DatePickerCalendar.js
+++ b/src/js/Pickers/DatePickerCalendar.js
@@ -1,85 +1,84 @@
-import React, { PropTypes } from 'react';
+import React, { Component, PropTypes } from 'react';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
 
-import { IconButton } from '../Buttons';
-import { isMonthBefore } from '../utils/dates';
 import CalendarMonth from './CalendarMonth';
 import SwipeableView from '../SwipeableViews';
+import CalendarHeader from './CalendarHeader';
 
-const DatePickerCalendar = (props) => {
-  const {
-    previousIcon,
-    onPreviousClick,
-    nextIcon,
-    onNextClick,
-    calendarDate,
-    calendarTempDate,
-    onCalendarDateClick,
-    DateTimeFormat,
-    locales,
-    minDate,
-    maxDate,
-    onSwipeChange,
-    transitionName,
-  } = props;
+export default class DatePickerCalendar extends Component {
+  constructor(props) {
+    super(props);
 
-  const dows = ['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((dow, i) => (
-    <h4 className="dow" key={`dow-${dow}-${i}`}>{dow}</h4>
-  ));
+    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
+  }
 
-  const isPreviousDisabled = isMonthBefore(minDate, calendarDate);
-  const isNextDisabled = isMonthBefore(calendarDate, maxDate);
-  return (
-    <section className="md-picker-content md-calendar">
-      <header className="md-calendar-header">
-        <div className="md-calendar-controls">
-          <IconButton onClick={onPreviousClick} disabled={isPreviousDisabled}>{previousIcon}</IconButton>
-          <h4 className="md-subtitle">
-            {DateTimeFormat(locales, { month: 'long', year: 'numeric' }).format(calendarDate)}
-          </h4>
-          <IconButton onClick={onNextClick} disabled={isNextDisabled}>{nextIcon}</IconButton>
-        </div>
-        <div className="md-dows">
-          {dows}
-        </div>
-      </header>
-      <SwipeableView
-        onChange={onSwipeChange}
-        transitionName={transitionName}
-        transitionEnterTimeout={150}
-        transitionLeave={false}
-      >
-        <CalendarMonth
-          key={DateTimeFormat(locales).format(calendarDate)}
-          calendarDate={calendarDate}
-          calendarTempDate={calendarTempDate}
-          onCalendarDateClick={onCalendarDateClick}
+  static propTypes = {
+    previousIcon: PropTypes.node.isRequired,
+    onPreviousClick: PropTypes.func.isRequired,
+    nextIcon: PropTypes.node.isRequired,
+    onNextClick: PropTypes.func.isRequired,
+    onCalendarDateClick: PropTypes.func.isRequired,
+    calendarDate: PropTypes.instanceOf(Date).isRequired,
+    calendarTempDate: PropTypes.instanceOf(Date).isRequired,
+    DateTimeFormat: PropTypes.func.isRequired,
+    locales: PropTypes.oneOfType([
+      PropTypes.string,
+      PropTypes.arrayOf(PropTypes.string),
+    ]).isRequired,
+    minDate: PropTypes.instanceOf(Date),
+    maxDate: PropTypes.instanceOf(Date),
+    onSwipeChange: PropTypes.func.isRequired,
+    transitionName: PropTypes.string.isRequired,
+  };
+
+  render() {
+    const {
+      previousIcon,
+      onPreviousClick,
+      nextIcon,
+      onNextClick,
+      calendarDate,
+      calendarTempDate,
+      onCalendarDateClick,
+      DateTimeFormat,
+      locales,
+      minDate,
+      maxDate,
+      onSwipeChange,
+      transitionName,
+    } = this.props;
+
+    return (
+      <section className="md-picker-content md-calendar">
+        <CalendarHeader
+          date={calendarDate}
           minDate={minDate}
           maxDate={maxDate}
           DateTimeFormat={DateTimeFormat}
           locales={locales}
+          onPreviousClick={onPreviousClick}
+          previousIcon={previousIcon}
+          onNextClick={onNextClick}
+          nextIcon={nextIcon}
         />
-      </SwipeableView>
-    </section>
-  );
-};
-
-DatePickerCalendar.propTypes = {
-  previousIcon: PropTypes.node.isRequired,
-  onPreviousClick: PropTypes.func.isRequired,
-  nextIcon: PropTypes.node.isRequired,
-  onNextClick: PropTypes.func.isRequired,
-  onCalendarDateClick: PropTypes.func.isRequired,
-  calendarDate: PropTypes.instanceOf(Date).isRequired,
-  calendarTempDate: PropTypes.instanceOf(Date).isRequired,
-  DateTimeFormat: PropTypes.func.isRequired,
-  locales: PropTypes.oneOfType([
-    PropTypes.string,
-    PropTypes.arrayOf(PropTypes.string),
-  ]).isRequired,
-  minDate: PropTypes.instanceOf(Date),
-  maxDate: PropTypes.instanceOf(Date),
-  onSwipeChange: PropTypes.func.isRequired,
-  transitionName: PropTypes.string.isRequired,
-};
-
-export default DatePickerCalendar;
+        <SwipeableView
+          onChange={onSwipeChange}
+          transitionName={transitionName}
+          transitionEnterTimeout={150}
+          transitionLeave={false}
+        >
+          <CalendarMonth
+            key={DateTimeFormat(locales).format(calendarDate)}
+            calendarDate={calendarDate}
+            calendarTempDate={calendarTempDate}
+            onCalendarDateClick={onCalendarDateClick}
+            minDate={minDate}
+            maxDate={maxDate}
+            DateTimeFormat={DateTimeFormat}
+            locales={locales}
+          />
+        </SwipeableView>
+      </section>
+    );
+  }
+}
diff --git a/src/js/Pickers/DatePickerContainer.js b/src/js/Pickers/DatePickerContainer.js
index fcb1832975..f47f3f31fc 100644
--- a/src/js/Pickers/DatePickerContainer.js
+++ b/src/js/Pickers/DatePickerContainer.js
@@ -11,7 +11,7 @@ import { onOutsideClick } from '../utils';
 import { addDate, subtractDate, DateTimeFormat, isMonthBefore } from '../utils/dates';
 import TextField from '../TextFields';
 import Dialog from '../Dialogs';
-import { Height } from '../Transitions';
+import Height from '../Transitions/Height';
 
 export default class DatePickerContainer extends Component {
   constructor(props) {
@@ -40,10 +40,25 @@ export default class DatePickerContainer extends Component {
 
   static propTypes = {
     /**
-     * An optional className to apply to the date picker.
+     * An optional style to apply to the date picker's container.
+     */
+    style: PropTypes.object,
+
+    /**
+     * An optional className to apply to the date picker's container.
      */
     className: PropTypes.string,
 
+    /**
+     * An optional style to apply to the date picker.
+     */
+    pickerStyle: PropTypes.object,
+
+    /**
+     * An optional className to apply to the date picker.
+     */
+    pickerClassName: PropTypes.string,
+
     /**
      * An optional icon to display with the date picker.
      */
@@ -252,9 +267,7 @@ export default class DatePickerContainer extends Component {
   handleOkClick = (e) => {
     const { DateTimeFormat, locales, onChange } = this.props;
     const value = DateTimeFormat(locales).format(this.state.calendarTempDate);
-    if(typeof this.props.value !== 'undefined' && onChange) {
-      onChange(value, new Date(this.state.calendarTempDate), e);
-    }
+    onChange && onChange(value, new Date(this.state.calendarTempDate), e);
 
     this.setState({ value, isOpen: false });
   };
@@ -282,23 +295,21 @@ export default class DatePickerContainer extends Component {
   setCalendarTempDate = (calendarTempDate) => {
     const { autoOk, DateTimeFormat, locales, onChange } = this.props;
 
+    const state = { calendarTempDate };
     if(autoOk) {
       const value = DateTimeFormat(locales).format(calendarTempDate);
-      if(onChange && typeof this.props.value !== 'undefined') {
-        onChange(value, new Date(calendarTempDate));
+      onChange && onChange(value, new Date(calendarTempDate));
+
+      if(typeof this.props.value === 'undefined') {
+        state.value = value;
       }
 
-      this.setState({
-        value,
-        calendarTempDate,
-        // wait for date to be picked then hide
-        timeout: setTimeout(() => {
-          this.setState({ timeout: null, isOpen: false });
-        }, 300),
-      });
-    } else {
-      this.setState({ calendarTempDate });
+      // Add a wait for the new date to be selected, then close
+      state.timeout = setTimeout(() => {
+        this.setState({ timeout: null, isOpen: false });
+      }, 300);
     }
+    this.setState(state);
   };
 
   setCalendarTempYear = (year) => {
@@ -340,13 +351,47 @@ export default class DatePickerContainer extends Component {
     }
   };
 
+  /**
+   * Gets the current value from the date picker as a formatted string.
+   *
+   * @param {Object} props? the props object to use.
+   * @param {Object} state? the state object to use.
+   * @return {String} a formatted date string or the empty string.
+   */
+  getValue = (props = this.props, state = this.state) => {
+    const { DateTimeFormat, locales } = props;
+    const value = typeof this.props.value !== 'undefined' ? props.value : state.value;
+    if(!value) {
+      return '';
+    } else if(value instanceof Date) {
+      return DateTimeFormat(locales).format(new Date(value));
+    } else {
+      return value;
+    }
+  };
+
   render() {
-    const { label, floatingLabel, value, onChange, icon, inline, displayMode, ...props } = this.props;
     const { isOpen, ...state } = this.state;
+    const {
+      style,
+      className,
+      pickerStyle,
+      pickerClassName,
+      label,
+      floatingLabel,
+      icon,
+      inline,
+      displayMode,
+      ...props,
+    } = this.props;
+    delete props.value;
+    delete props.onChange;
+
     const pickerProps = {
       ...state,
       ...props,
-      className: classnames('md-picker', displayMode, { inline, 'with-icon': inline && icon }),
+      style: pickerStyle,
+      className: classnames('md-picker', displayMode, pickerClassName, { inline, 'with-icon': inline && icon }),
       onCancelClick: this.handleCancelClick,
       onOkClick: this.handleOkClick,
       changeCalendarMode: this.changeCalendarMode,
@@ -357,34 +402,26 @@ export default class DatePickerContainer extends Component {
       onSwipeChange: this.handleSwipeChange,
     };
 
-    let textFieldValue = typeof value === 'undefined' ? state.value : value;
-    if(isOpen && inline) {
-      textFieldValue = new props.DateTimeFormat(props.locale).format(state.calendarTempDate);
+    let picker = isOpen ? <DatePicker {...pickerProps} /> : null;
+    let content;
+    if(inline) {
+      picker = isOpen ? <Height transitionEnterTimeout={150} transitionLeaveTimeout={150}>{picker}</Height> : null;
+      content = <TransitionGroup>{picker}</TransitionGroup>;
+    } else {
+      content = <Dialog isOpen={isOpen} close={this.close}>{picker}</Dialog>;
     }
 
     return (
-      <div className="md-picker-container" ref="container">
+      <div className={classnames('md-picker-container', className)} ref="container" style={style}>
         <TextField
           icon={icon}
           onClick={this.toggleOpen}
           label={label}
           floatingLabel={floatingLabel}
-          value={textFieldValue}
-          onChange={onChange}
+          value={this.getValue()}
           readOnly={true}
         />
-        {inline ?
-          <TransitionGroup>
-            {isOpen &&
-              <Height transitionEnterTimeout={150} transitionLeaveTimeout={150}>
-                <DatePicker {...pickerProps} />
-              </Height>
-            }
-          </TransitionGroup> :
-          <Dialog isOpen={isOpen} close={this.close}>
-            {isOpen && <DatePicker {...pickerProps} />}
-          </Dialog>
-        }
+        {content}
       </div>
     );
   }
diff --git a/src/js/Pickers/DatePickerHeader.js b/src/js/Pickers/DatePickerHeader.js
index 52b41edb2b..a909596b41 100644
--- a/src/js/Pickers/DatePickerHeader.js
+++ b/src/js/Pickers/DatePickerHeader.js
@@ -1,36 +1,71 @@
-import React, { PropTypes } from 'react';
+import React, { Component, PropTypes } from 'react';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
 
 import PickerControl from './PickerControl';
 
-const DatePickerHeader = ({ DateTimeFormat, locales, changeCalendarMode, calendarMode, calendarTempDate }) => {
-  return (
-    <header className="md-picker-header">
-      <PickerControl onClick={changeCalendarMode.bind(this, 'year')} active={calendarMode === 'year'}>
-        <h6 className="md-subtitle">
-          {DateTimeFormat(locales, { year: 'numeric' }).format(calendarTempDate)}
-        </h6>
-      </PickerControl>
-      <PickerControl onClick={changeCalendarMode.bind(this, 'calendar')} active={calendarMode === 'calendar'}>
-        <h4 className="md-display-1">
-          {DateTimeFormat(locales, { weekday: 'short' }).format(calendarTempDate)},&nbsp;
-        </h4>
-        <h4 className="md-display-1">
-          {DateTimeFormat(locales, { month: 'short', day: '2-digit' }).format(calendarTempDate)}
-        </h4>
-      </PickerControl>
-    </header>
-  );
-};
-
-DatePickerHeader.propTypes = {
-  DateTimeFormat: PropTypes.func.isRequired,
-  locales: PropTypes.oneOfType([
-    PropTypes.string,
-    PropTypes.arrayOf(PropTypes.string),
-  ]).isRequired,
-  calendarTempDate: PropTypes.instanceOf(Date).isRequired,
-  calendarMode: PropTypes.oneOf(['calendar', 'year']).isRequired,
-  changeCalendarMode: PropTypes.func.isRequired,
-};
-
-export default DatePickerHeader;
+/**
+ * The `DatePickerHeader` component is the component that holds the
+ * current year and the current date. It allows the user to switch
+ * between calendar and year picker mode.
+ */
+export default class DatePickerHeader extends Component {
+  constructor(props) {
+    super(props);
+
+    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
+    this.state = this.getFormattedDate(props);
+  }
+
+  static propTypes = {
+    DateTimeFormat: PropTypes.func.isRequired,
+    locales: PropTypes.oneOfType([
+      PropTypes.string,
+      PropTypes.arrayOf(PropTypes.string),
+    ]).isRequired,
+    calendarTempDate: PropTypes.instanceOf(Date).isRequired,
+    calendarMode: PropTypes.oneOf(['calendar', 'year']).isRequired,
+    changeCalendarMode: PropTypes.func.isRequired,
+  };
+
+  componentWillUpdate(nextProps) {
+    const { DateTimeFormat, locales, calendarTempDate, changeCalendarMode } = this.props;
+    if(DateTimeFormat !== nextProps.DateTimeFormat
+      || locales !== nextProps.locales
+      || calendarTempDate !== nextProps.calendarTempDate
+      || changeCalendarMode !== nextProps.changeCalendarMode) {
+      this.setState(this.getFormattedDate(nextProps));
+    }
+  }
+
+  getFormattedDate = ({ DateTimeFormat, locales, calendarTempDate }) => {
+    return {
+      year: DateTimeFormat(locales, { year: 'numeric' }).format(calendarTempDate),
+      weekday: DateTimeFormat(locales, { weekday: 'short' }).format(calendarTempDate),
+      date: DateTimeFormat(locales, { month: 'short', day: '2-digit' }).format(calendarTempDate),
+    };
+  };
+
+  selectYear = (e) => {
+    this.props.changeCalendarMode('year', e);
+  };
+
+  selectCalendar = (e) => {
+    this.props.changeCalendarMode('calendar', e);
+  };
+
+  render() {
+    const { year, weekday, date } = this.state;
+    const { calendarMode } = this.props;
+    return (
+      <header className="md-picker-header">
+        <PickerControl onClick={this.selectYear} active={calendarMode === 'year'}>
+          <h6 className="md-subtitle">{year}</h6>
+        </PickerControl>
+        <PickerControl onClick={this.selectCalendar} active={calendarMode === 'calendar'}>
+          <h4 className="md-display-1">{weekday + ', '}</h4>
+          <h4 className="md-display-1">{date}</h4>
+        </PickerControl>
+      </header>
+    );
+  }
+}
diff --git a/src/js/Pickers/PickerControl.js b/src/js/Pickers/PickerControl.js
index af55e0ce6b..2dcc4146fa 100644
--- a/src/js/Pickers/PickerControl.js
+++ b/src/js/Pickers/PickerControl.js
@@ -1,22 +1,39 @@
-import React, { PropTypes } from 'react';
+import React, { Component, PropTypes } from 'react';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
 import classnames from 'classnames';
 
-const PickerControl = ({ active, onClick, children }) => {
-  return (
-    <button
-      type="button"
-      className={classnames('md-picker-control', { active })}
-      onClick={onClick}
-      >
-      {children}
-    </button>
-  );
-};
+/**
+ * The `PickerControl` component is the button that goes in the header
+ * of a `Picker` component. It is used to switch the view of the `Picker`
+ * from state to state.
+ *
+ * For example, this is the year and date views for the `DatePicker`.
+ */
+export default class PickerControl extends Component {
+  constructor(props) {
+    super(props);
+
+    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
+  }
 
-PickerControl.propTypes = {
-  active: PropTypes.bool.isRequired,
-  children: PropTypes.node.isRequired,
-  onClick: PropTypes.func.isRequired,
-};
+  static propTypes = {
+    className: PropTypes.string,
+    active: PropTypes.bool.isRequired,
+    onClick: PropTypes.func.isRequired,
+    children: PropTypes.node.isRequired,
+  };
 
-export default PickerControl;
+  render() {
+    const { className, onClick, active, children, ...props } = this.props;
+    return (
+      <button
+        type="button"
+        className={classnames('md-picker-control', className, { active })}
+        onClick={onClick}
+        {...props}
+      >
+        {children}
+      </button>
+    );
+  }
+}
diff --git a/src/js/Pickers/PickerFooter.js b/src/js/Pickers/PickerFooter.js
index 66a87f0b01..f6dbcc87bb 100644
--- a/src/js/Pickers/PickerFooter.js
+++ b/src/js/Pickers/PickerFooter.js
@@ -1,32 +1,56 @@
-import React, { PropTypes } from 'react';
-import { FlatButton } from '../Buttons';
+import React, { Component, PropTypes } from 'react';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import classnames from 'classnames';
 
-const PickerFooter = ({ okLabel, okPrimary, onOkClick, cancelLabel, cancelPrimary, onCancelClick }) => {
-  return (
-    <footer className="md-dialog-footer">
-      <FlatButton
-        primary={cancelPrimary}
-        secondary={!cancelPrimary}
-        label={cancelLabel}
-        onClick={onCancelClick}
-      />
-      <FlatButton
-        primary={okPrimary}
-        secondary={!okPrimary}
-        label={okLabel}
-        onClick={onOkClick}
-      />
-    </footer>
-  );
-};
+import FlatButton from '../Buttons/FlatButton';
 
-PickerFooter.propTypes = {
-  okLabel: PropTypes.string.isRequired,
-  okPrimary: PropTypes.bool.isRequired,
-  onOkClick: PropTypes.func.isRequired,
-  cancelLabel: PropTypes.string.isRequired,
-  cancelPrimary: PropTypes.bool.isRequired,
-  onCancelClick: PropTypes.func.isRequired,
-};
+/**
+ * The `PickerFooter` component is a dialog footer for the `Picker` components.
+ * It renders an ok and cancel button.
+ */
+export default class PickerFooter extends Component {
+  constructor(props) {
+    super(props);
 
-export default PickerFooter;
+    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
+  }
+
+  static propTypes = {
+    className: PropTypes.string,
+    okLabel: PropTypes.string.isRequired,
+    okPrimary: PropTypes.bool.isRequired,
+    onOkClick: PropTypes.func.isRequired,
+    cancelLabel: PropTypes.string.isRequired,
+    cancelPrimary: PropTypes.bool.isRequired,
+    onCancelClick: PropTypes.func.isRequired,
+  };
+
+  render() {
+    const {
+      className,
+      okLabel,
+      okPrimary,
+      onOkClick,
+      cancelLabel,
+      cancelPrimary,
+      onCancelClick,
+    } = this.props;
+
+    return (
+      <footer className={classnames('md-dialog-footer', className)}>
+        <FlatButton
+          primary={cancelPrimary}
+          secondary={!cancelPrimary}
+          label={cancelLabel}
+          onClick={onCancelClick}
+        />
+        <FlatButton
+          primary={okPrimary}
+          secondary={!okPrimary}
+          label={okLabel}
+          onClick={onOkClick}
+        />
+      </footer>
+    );
+  }
+}
diff --git a/src/js/Pickers/Year.js b/src/js/Pickers/Year.js
new file mode 100644
index 0000000000..1d71d223ae
--- /dev/null
+++ b/src/js/Pickers/Year.js
@@ -0,0 +1,39 @@
+import React, { Component, PropTypes } from 'react';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import classnames from 'classnames';
+
+/**
+ * This is a component for rendering a year in the Date Picker's Year picker
+ * list.
+ */
+export default class Year extends Component {
+  constructor(props) {
+    super(props);
+
+    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
+  }
+
+  static propTypes = {
+    className: PropTypes.string,
+    active: PropTypes.bool.isRequired,
+    onClick: PropTypes.func.isRequired,
+    year: PropTypes.number.isRequired,
+  };
+
+  handleClick = (e) => {
+    this.props.onClick(this.props.year, e);
+  };
+
+  render() {
+    const { active, className, year } = this.props;
+    return (
+      <button
+        type="button"
+        className={classnames('md-year', className, { active })}
+        onClick={this.handleClick}
+      >
+        {year}
+      </button>
+    );
+  }
+}
diff --git a/src/js/Pickers/YearPicker.js b/src/js/Pickers/YearPicker.js
index d2189736f4..c5b12369df 100644
--- a/src/js/Pickers/YearPicker.js
+++ b/src/js/Pickers/YearPicker.js
@@ -1,7 +1,12 @@
 import React, { Component, PropTypes } from 'react';
 import PureRenderMixin from 'react-addons-pure-render-mixin';
-import classnames from 'classnames';
 
+import Year from './Year';
+
+/**
+ * The `YearPicker` component is the Year view in a `DatePicker`. This
+ * will display a list of years to select from within the given range.
+ */
 export default class YearPicker extends Component {
   constructor(props) {
     super(props);
@@ -9,11 +14,26 @@ export default class YearPicker extends Component {
     this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
 
     const year = props.calendarTempDate.getFullYear();
-    const halfed = parseInt(props.initialYearsDisplayed / 2);
-    this.state = {
-      startYear: props.minDate ? props.minDate.getFullYear() : year - halfed,
-      endYear: props.maxDate ? props.maxDate.getFullYear() : year + halfed,
-    };
+    const range = !props.minDate && !props.maxDate ? parseInt(props.initialYearsDisplayed / 2) : props.initialYearsDisplayed;
+    let startYear, endYear;
+    if(props.minDate && props.maxDate) {
+      startYear = props.minDate.getFullYear();
+      endYear = props.maxDate.getFullYear();
+    } else if(!props.minDate && !props.maxDate) {
+      startYear = year - range;
+      endYear = year + range;
+      if(props.initialYearsDisplayed % 2 === 0) {
+        endYear -= 1;
+      }
+    } else if(!props.maxDate) {
+      startYear = props.minDate.getFullYear();
+      endYear = startYear + props.initialYearsDisplayed - 1;
+    } else {
+      endYear = props.maxDate.getFullYear();
+      startYear = endYear - props.initialYearsDisplayed + 1;
+    }
+
+    this.state = { startYear, endYear };
   }
 
   static propTypes = {
@@ -42,17 +62,15 @@ export default class YearPicker extends Component {
     const { startYear, endYear } = this.state;
 
     const currentYear = this.props.calendarTempDate.getFullYear();
-    let years = [];
+    const years = [];
     for(let year = startYear; year <= endYear; year++) {
       years.push(
-        <button
-          type="button"
-          key={`year-${year}`}
-          className={classnames('md-year', { 'active': year === currentYear })}
-          onClick={this.props.onCalendarYearClick.bind(this, year)}
-          >
-          {year}
-        </button>
+        <Year
+          key={year}
+          year={year}
+          active={year === currentYear}
+          onClick={this.props.onCalendarYearClick}
+        />
       );
     }
     return (
diff --git a/src/js/Pickers/__mocks__/DateTimeFormat.js b/src/js/Pickers/__mocks__/DateTimeFormat.js
new file mode 100644
index 0000000000..91126bfbb2
--- /dev/null
+++ b/src/js/Pickers/__mocks__/DateTimeFormat.js
@@ -0,0 +1,4 @@
+/*eslint-env jest*/
+export default jest.genMockFunction().mockImplementation(() => {
+  return { format: jest.genMockFunction().mockImplementation(() => '') };
+});
diff --git a/src/js/TextFields/TextField.js b/src/js/TextFields/TextField.js
index 1f28798d80..04bea0657f 100644
--- a/src/js/TextFields/TextField.js
+++ b/src/js/TextFields/TextField.js
@@ -175,6 +175,11 @@ export default class TextField extends Component {
      */
     onSelect: PropTypes.func,
 
+    /**
+     * An optional function to call when the text field is clicked.
+     */
+    onClick: PropTypes.func,
+
     /**
      * Optional style to apply to the text field container.
      */
@@ -314,6 +319,7 @@ export default class TextField extends Component {
       onKeyPress,
       onKeyUp,
       onSelect,
+      onClick,
       type,
       ...props,
     } = this.props;
@@ -376,6 +382,7 @@ export default class TextField extends Component {
       onBlur: this.handleBlur,
       onChange: this.handleChange,
       onFocus: this.handleFocus,
+      onClick,
       onInput,
       onInvalid,
       onKeyDown,
diff --git a/src/js/TextFields/__mocks__/index.js b/src/js/TextFields/__mocks__/index.js
new file mode 100644
index 0000000000..00a1116527
--- /dev/null
+++ b/src/js/TextFields/__mocks__/index.js
@@ -0,0 +1,4 @@
+/*eslint-env jest*/
+import React from 'react';
+
+export default jest.genMockFunction().mockImplementation(props => <input type="input" {...props} />);
diff --git a/src/js/utils/dates.js b/src/js/utils/dates.js
index 2046060fc2..b8d4a9154d 100644
--- a/src/js/utils/dates.js
+++ b/src/js/utils/dates.js
@@ -75,7 +75,9 @@ export const DateTimeFormat = (() => {
   }
 
   // (locales, options)
-  return () => date => date;
+  return {
+    format: () => date => date,
+  };
 })();
 
 /**


Test patch:
diff --git a/src/js/Pickers/__tests__/CalendarDate.js b/src/js/Pickers/__tests__/CalendarDate.js
new file mode 100644
index 0000000000..1b4e946d7e
--- /dev/null
+++ b/src/js/Pickers/__tests__/CalendarDate.js
@@ -0,0 +1,54 @@
+/*eslint-env jest*/
+jest.unmock('../CalendarDate');
+
+import React from 'react';
+import { findDOMNode } from 'react-dom';
+import {
+  Simulate,
+  renderIntoDocument,
+} from 'react-addons-test-utils';
+
+import CalendarDate from '../CalendarDate';
+
+describe('CalendarDate', () => {
+  it('calls the onClick prop with the date prop when not disabled', () => {
+    const DateTimeFormat = require('../__mocks__/DateTimeFormat');
+    const props = {
+      DateTimeFormat,
+      locales: 'en-US',
+      date: new Date(2016, 1, 1),
+      disabled: false,
+      onClick: jest.genMockFunction(),
+      index: 0,
+    };
+
+    let date = renderIntoDocument(<CalendarDate {...props} />);
+    let dateNode = findDOMNode(date);
+    Simulate.click(dateNode);
+    expect(props.onClick.mock.calls.length).toBe(1);
+
+    const newProps = Object.assign({}, props, { disabled: true });
+    date = renderIntoDocument(<CalendarDate {...newProps} />);
+    dateNode = findDOMNode(date);
+    Simulate.click(dateNode);
+    expect(props.onClick.mock.calls.length).toBe(1);
+  });
+
+  it('formats the date as the state', () => {
+    const DateTimeFormat = require('../__mocks__/DateTimeFormat');
+    const props = {
+      DateTimeFormat,
+      locales: 'en-US',
+      date: new Date(2016, 1, 1),
+      disabled: false,
+      onClick: jest.genMockFunction(),
+      index: 0,
+    };
+
+    const date = renderIntoDocument(<CalendarDate {...props} />);
+    expect(DateTimeFormat.mock.calls.length).toBe(1);
+    expect(DateTimeFormat.mock.calls[0][0]).toBe(props.locales);
+    expect(DateTimeFormat.mock.calls[0][1]).toEqual({ day: 'numeric' });
+    expect(date.state.date).toBe('');
+  });
+});
diff --git a/src/js/Pickers/__tests__/CalendarHeader.js b/src/js/Pickers/__tests__/CalendarHeader.js
new file mode 100644
index 0000000000..acaeabd5a2
--- /dev/null
+++ b/src/js/Pickers/__tests__/CalendarHeader.js
@@ -0,0 +1,74 @@
+/*eslint-env jest*/
+jest.unmock('../CalendarHeader');
+
+import React from 'react';
+import {
+  renderIntoDocument,
+  scryRenderedComponentsWithType,
+  findRenderedDOMComponentWithClass,
+} from 'react-addons-test-utils';
+
+import CalendarHeader from '../CalendarHeader';
+import IconButton from '../../Buttons/IconButton';
+
+describe('CalendarHeader', () => {
+  it('renders the day of week abbreviations', () => {
+    const DateTimeFormat = require('../__mocks__/DateTimeFormat');
+
+    const props = {
+      DateTimeFormat,
+      locales: 'en-US',
+      onPreviousClick: jest.genMockFunction(),
+      onNextClick: jest.genMockFunction(),
+      previousIcon: 'a',
+      nextIcon: 'a',
+      date: new Date(),
+    };
+
+    const header = renderIntoDocument(<CalendarHeader {...props} />);
+    const dows = findRenderedDOMComponentWithClass(header, 'md-dows');
+    expect(dows.childNodes.length).toBe(7);
+  });
+
+  it('formats the date with a month and year', () => {
+    const DateTimeFormat = require('../__mocks__/DateTimeFormat');
+
+    const props = {
+      DateTimeFormat,
+      locales: 'en-US',
+      onPreviousClick: jest.genMockFunction(),
+      onNextClick: jest.genMockFunction(),
+      previousIcon: 'a',
+      nextIcon: 'a',
+      date: new Date(),
+    };
+
+    renderIntoDocument(<CalendarHeader {...props} />);
+    expect(DateTimeFormat.mock.calls.length).toBe(2);
+    // first call is from generateDows
+    expect(DateTimeFormat.mock.calls[1][0]).toBe(props.locales);
+    expect(DateTimeFormat.mock.calls[1][1]).toEqual({ month: 'long', year: 'numeric' });
+  });
+
+  it('renders a next and previous icon button', () => {
+    const DateTimeFormat = require('../__mocks__/DateTimeFormat');
+
+    const props = {
+      DateTimeFormat,
+      locales: 'en-US',
+      onPreviousClick: jest.genMockFunction(),
+      onNextClick: jest.genMockFunction(),
+      previousIcon: 'a',
+      nextIcon: 'a',
+      date: new Date(),
+    };
+
+    const header = renderIntoDocument(<CalendarHeader {...props} />);
+    const iconBtns = scryRenderedComponentsWithType(header, IconButton);
+    expect(iconBtns.length).toBe(2);
+    expect(iconBtns[0].props.onClick).toBe(props.onPreviousClick);
+    expect(iconBtns[0].props.children).toBe(props.previousIcon);
+    expect(iconBtns[1].props.onClick).toBe(props.onNextClick);
+    expect(iconBtns[1].props.children).toBe(props.nextIcon);
+  });
+});
diff --git a/src/js/Pickers/__tests__/CalendarMonth.js b/src/js/Pickers/__tests__/CalendarMonth.js
new file mode 100644
index 0000000000..138f3b09d2
--- /dev/null
+++ b/src/js/Pickers/__tests__/CalendarMonth.js
@@ -0,0 +1,51 @@
+/*eslint-env jest*/
+jest.unmock('../CalendarMonth');
+jest.unmock('../../utils/dates');
+
+import React from 'react';
+import { findDOMNode } from 'react-dom';
+import {
+  renderIntoDocument,
+  scryRenderedComponentsWithType,
+} from 'react-addons-test-utils';
+
+import CalendarDate from '../CalendarDate';
+import CalendarMonth from '../CalendarMonth';
+
+describe('CalendarMonth', () => {
+  it('merges className and style', () => {
+    const DateTimeFormat = require('../__mocks__/DateTimeFormat');
+    const style = { display: 'block' };
+    const className = 'test';
+    const props = {
+      style,
+      className,
+      calendarDate: new Date(),
+      calendarTempDate: new Date(),
+      onCalendarDateClick: jest.genMockFunction(),
+      DateTimeFormat,
+      locales: 'en-US',
+    };
+    const calendarMonth = renderIntoDocument(<CalendarMonth {...props} />);
+
+    const calendarMonthNode = findDOMNode(calendarMonth);
+    expect(calendarMonthNode.style.display).toBe(style.display);
+    expect(calendarMonthNode.classList.contains(className)).toBe(true);
+  });
+
+  it('renders the number of days in a month', () => {
+    const DateTimeFormat = require('../__mocks__/DateTimeFormat');
+    const props = {
+      DateTimeFormat,
+      locales: 'en-US',
+      calendarDate: new Date(2016, 3, 12),
+      calendarTempDate: new Date(2016, 3, 12),
+      onCalendarDateClick: jest.genMockFunction(),
+    };
+
+    const calendarMonth = renderIntoDocument(<CalendarMonth {...props} />);
+    const days = scryRenderedComponentsWithType(calendarMonth, CalendarDate);
+    expect(days.length).toBe(30);
+    expect(days[11].props.className).toContain('active');
+  });
+});
diff --git a/src/js/Pickers/__tests__/DatePicker.js b/src/js/Pickers/__tests__/DatePicker.js
new file mode 100644
index 0000000000..a483a9cde7
--- /dev/null
+++ b/src/js/Pickers/__tests__/DatePicker.js
@@ -0,0 +1,95 @@
+/*eslint-env jest*/
+jest.unmock('../DatePicker');
+
+import React from 'react';
+import {
+  renderIntoDocument,
+  scryRenderedComponentsWithType,
+} from 'react-addons-test-utils';
+
+import DatePicker from '../DatePicker';
+import DatePickerHeader from '../DatePickerHeader';
+import PickerFooter from '../PickerFooter';
+import DatePickerCalendar from '../DatePickerCalendar';
+import YearPicker from '../YearPicker';
+
+describe('DatePicker', () => {
+  it('renders a DatePickerHeader and a PickerFooter', () => {
+    const DateTimeFormat = require('../__mocks__/DateTimeFormat');
+    const props = {
+      className: 'a',
+      okLabel: 'a',
+      okPrimary: false,
+      onOkClick: jest.genMockFunction(),
+      cancelLabel: 'a',
+      cancelPrimary: false,
+      onCancelClick: jest.genMockFunction(),
+      DateTimeFormat,
+      locales: 'en-US',
+      calendarDate: new Date(),
+      calendarTempDate: new Date(),
+      calendarMode: 'calendar',
+      changeCalendarMode: jest.genMockFunction(),
+      onSwipeChange: jest.genMockFunction(),
+    };
+
+    const picker = renderIntoDocument(<DatePicker {...props} />);
+    const headers = scryRenderedComponentsWithType(picker, DatePickerHeader);
+    expect(headers.length).toBe(1);
+
+    const footers = scryRenderedComponentsWithType(picker, PickerFooter);
+    expect(footers.length).toBe(1);
+  });
+
+  it('renders a calendar when the calendarMode is calendar', () => {
+    const DateTimeFormat = require('../__mocks__/DateTimeFormat');
+    const props = {
+      className: 'a',
+      okLabel: 'a',
+      okPrimary: false,
+      onOkClick: jest.genMockFunction(),
+      cancelLabel: 'a',
+      cancelPrimary: false,
+      onCancelClick: jest.genMockFunction(),
+      DateTimeFormat,
+      locales: 'en-US',
+      calendarDate: new Date(),
+      calendarTempDate: new Date(),
+      calendarMode: 'calendar',
+      changeCalendarMode: jest.genMockFunction(),
+      onSwipeChange: jest.genMockFunction(),
+    };
+
+    const picker = renderIntoDocument(<DatePicker {...props} />);
+    const calendars = scryRenderedComponentsWithType(picker, DatePickerCalendar);
+    const years = scryRenderedComponentsWithType(picker, YearPicker);
+    expect(calendars.length).toBe(1);
+    expect(years.length).toBe(0);
+  });
+
+  it('renders a year picker when the calendarMode is year', () => {
+    const DateTimeFormat = require('../__mocks__/DateTimeFormat');
+    const props = {
+      className: 'a',
+      okLabel: 'a',
+      okPrimary: false,
+      onOkClick: jest.genMockFunction(),
+      cancelLabel: 'a',
+      cancelPrimary: false,
+      onCancelClick: jest.genMockFunction(),
+      DateTimeFormat,
+      locales: 'en-US',
+      calendarDate: new Date(),
+      calendarTempDate: new Date(),
+      calendarMode: 'year',
+      changeCalendarMode: jest.genMockFunction(),
+      onSwipeChange: jest.genMockFunction(),
+    };
+
+    const picker = renderIntoDocument(<DatePicker {...props} />);
+    const calendars = scryRenderedComponentsWithType(picker, DatePickerCalendar);
+    const years = scryRenderedComponentsWithType(picker, YearPicker);
+    expect(calendars.length).toBe(0);
+    expect(years.length).toBe(1);
+  });
+});
diff --git a/src/js/Pickers/__tests__/DatePickerContainer.js b/src/js/Pickers/__tests__/DatePickerContainer.js
new file mode 100644
index 0000000000..6e98b3e789
--- /dev/null
+++ b/src/js/Pickers/__tests__/DatePickerContainer.js
@@ -0,0 +1,176 @@
+/*eslint-env jest*/
+jest.unmock('../DatePickerContainer');
+jest.unmock('../DatePicker');
+
+import React from 'react';
+import { findDOMNode } from 'react-dom';
+import {
+  Simulate,
+  renderIntoDocument,
+  scryRenderedComponentsWithType,
+  findRenderedDOMComponentWithTag,
+} from 'react-addons-test-utils';
+
+import DatePickerContainer from '../DatePickerContainer';
+import DatePicker from '../DatePicker';
+
+describe('DatePickerContainer', () => {
+  it('merges className and style', () => {
+    const style = { display: 'block' };
+    const className = 'test';
+    const pickerStyle = { background: 'black' };
+    const pickerClassName = 'picker-test';
+    let datePickerContainer = renderIntoDocument(
+      <DatePickerContainer
+        style={style}
+        className={className}
+        pickerStyle={pickerStyle}
+        pickerClassName={pickerClassName}
+        locales="en-US"
+      />
+    );
+
+    datePickerContainer.setState({ isOpen: true });
+
+    const datePickerContainerNode = findDOMNode(datePickerContainer);
+    expect(datePickerContainerNode.style.display).toBe(style.display);
+    expect(datePickerContainerNode.className).toContain(className);
+
+    const pickers = scryRenderedComponentsWithType(datePickerContainer, DatePicker);
+    expect(pickers.length).toBe(1);
+
+    const picker = pickers[0];
+    expect(picker.props.style).toEqual(pickerStyle);
+    expect(picker.props.className).toContain(pickerClassName);
+  });
+
+  it('renders a text field that opens the date picker when clicked', () => {
+    const container = renderIntoDocument(
+      <DatePickerContainer locales="en-US" />
+    );
+
+    const textField = findRenderedDOMComponentWithTag(container, 'input');
+    expect(container.state.isOpen).toBe(false);
+
+    Simulate.click(textField);
+    expect(container.state.isOpen).toBe(true);
+  });
+
+  it('allows for null, a Date object or a formatted string as the value prop', () => {
+    const DateTimeFormat = require('intl').DateTimeFormat;
+    const stringValue = '3/17/2017';
+    let container = renderIntoDocument(
+      <DatePickerContainer value={stringValue} locales="en-US" DateTimeFormat={DateTimeFormat} />
+    );
+
+    let textField = findRenderedDOMComponentWithTag(container, 'input');
+    expect(textField.value).toBe(stringValue);
+
+    const dateValue = new Date(2017, 2, 17);
+    container = renderIntoDocument(
+      <DatePickerContainer value={dateValue} locales="en-US" DateTimeFormat={DateTimeFormat} />
+    );
+
+    textField = findRenderedDOMComponentWithTag(container, 'input');
+    expect(textField.value).toBe(stringValue);
+
+    container = renderIntoDocument(
+      <DatePickerContainer value={null} locales="en-US" DateTimeFormat={DateTimeFormat} />
+    );
+
+    textField = findRenderedDOMComponentWithTag(container, 'input');
+    expect(textField.value).toBe('');
+  });
+
+  it('allows for null, a Date object or a formatted string as the defaultValue prop', () => {
+    const DateTimeFormat = require('intl').DateTimeFormat;
+    const stringValue = '3/17/2017';
+    let container = renderIntoDocument(
+      <DatePickerContainer defaultValue={stringValue} locales="en-US" DateTimeFormat={DateTimeFormat} />
+    );
+
+    let textField = findRenderedDOMComponentWithTag(container, 'input');
+    expect(textField.value).toBe(stringValue);
+
+    const dateValue = new Date(2017, 2, 17);
+    container = renderIntoDocument(
+      <DatePickerContainer defaultValue={dateValue} locales="en-US" DateTimeFormat={DateTimeFormat} />
+    );
+
+    textField = findRenderedDOMComponentWithTag(container, 'input');
+    expect(textField.value).toBe(stringValue);
+
+    container = renderIntoDocument(
+      <DatePickerContainer defaultValue={null} locales="en-US" DateTimeFormat={DateTimeFormat} />
+    );
+
+    textField = findRenderedDOMComponentWithTag(container, 'input');
+    expect(textField.value).toBe('');
+  });
+
+  it('when the ok button is clicked, the onChange prop is called with the formatted date string, the new date object, and the event', () => {
+    const defaultValue = '3/17/2016';
+    const DateTimeFormat = jest.genMockFunction().mockImplementation(() => {
+      return {
+        format: () => defaultValue,
+      };
+    });
+    const event = { target: 'a' };
+    const onChange = jest.genMockFunction();
+    const container = renderIntoDocument(
+      <DatePickerContainer
+        locales="en-US"
+        onChange={onChange}
+        defaultValue={defaultValue}
+        DateTimeFormat={DateTimeFormat}
+      />
+    );
+
+    container.handleOkClick(event);
+    expect(onChange.mock.calls.length).toBe(1);
+    expect(onChange.mock.calls[0][0]).toBe(defaultValue);
+    expect(onChange.mock.calls[0][1]).toEqual(new Date(defaultValue));
+    expect(onChange.mock.calls[0][2]).toEqual(event);
+  });
+
+  it('calls the onChange prop with the new formatted date string and the new date object when the autoOk prop is true and a new date is selected.', () => {
+    const onChange = jest.genMockFunction();
+    const defaultValue = '3/17/2016';
+    const DateTimeFormat = jest.genMockFunction().mockImplementation(() => {
+      return {
+        format: () => defaultValue,
+      };
+    });
+
+    let container = renderIntoDocument(
+      <DatePickerContainer
+        locales="en-US"
+        onChange={onChange}
+        defaultValue={defaultValue}
+        DateTimeFormat={DateTimeFormat}
+      />
+    );
+
+    let tempDate = new Date(2016, 2, 15);
+    container.setCalendarTempDate(tempDate);
+    expect(onChange.mock.calls.length).toBe(0);
+    expect(container.state.calendarTempDate).toEqual(tempDate);
+
+    container = renderIntoDocument(
+      <DatePickerContainer
+        locales="en-US"
+        onChange={onChange}
+        defaultValue={defaultValue}
+        DateTimeFormat={DateTimeFormat}
+        autoOk={true}
+      />
+    );
+
+    tempDate = new Date(2016, 2, 18);
+    container.setCalendarTempDate(tempDate);
+    expect(onChange.mock.calls.length).toBe(1);
+    expect(container.state.calendarTempDate).toEqual(tempDate);
+    expect(onChange.mock.calls[0][0]).toBe(defaultValue);
+    expect(onChange.mock.calls[0][1]).toEqual(tempDate);
+  });
+});
diff --git a/src/js/Pickers/__tests__/DatePickerHeader.js b/src/js/Pickers/__tests__/DatePickerHeader.js
new file mode 100644
index 0000000000..bd08388408
--- /dev/null
+++ b/src/js/Pickers/__tests__/DatePickerHeader.js
@@ -0,0 +1,61 @@
+/*eslint-env jest*/
+jest.unmock('../DatePickerHeader');
+
+import React from 'react';
+import {
+  renderIntoDocument,
+  scryRenderedComponentsWithType,
+} from 'react-addons-test-utils';
+
+import DatePickerHeader from '../DatePickerHeader';
+import PickerControl from '../PickerControl';
+
+describe('DatePickerHeader', () => {
+  it('displays a picker control for selecting the year and a picker control for selecting the calendar', () => {
+    const DateTimeFormat = require('../__mocks__/DateTimeFormat');
+
+    const props = {
+      changeCalendarMode: jest.genMockFunction(),
+      DateTimeFormat,
+      locales: 'en-US',
+      calendarTempDate: new Date(2016, 1, 1),
+      calendarMode: 'year',
+    };
+
+    const header = renderIntoDocument(<DatePickerHeader {...props} />);
+    const controls = scryRenderedComponentsWithType(header, PickerControl);
+    expect(controls.length).toBe(2);
+
+    const [year, calendar] = controls;
+    expect(year.props.onClick).toBe(header.selectYear);
+    expect(calendar.props.onClick).toBe(header.selectCalendar);
+  });
+
+  it('formats the calendar temp date', () => {
+    const DateTimeFormat = require('../__mocks__/DateTimeFormat');
+    const props = {
+      calendarTempDate: new Date(2016, 1, 1),
+      locales: 'en-US',
+      changeCalendarMode: jest.genMockFunction(),
+      calendarMode: 'year',
+      DateTimeFormat,
+    };
+
+    const header = renderIntoDocument(<DatePickerHeader {...props} />);
+    expect(DateTimeFormat.mock.calls.length).toBe(3);
+
+    const [time, weekday, date] = DateTimeFormat.mock.calls;
+    expect(time[0]).toBe(props.locales);
+    expect(time[1]).toEqual({ year: 'numeric' });
+
+    expect(weekday[0]).toBe(props.locales);
+    expect(weekday[1]).toEqual({ weekday: 'short' });
+
+    expect(date[0]).toBe(props.locales);
+    expect(date[1]).toEqual({ month: 'short', day: '2-digit' });
+
+    expect(header.state.year).toBe('');
+    expect(header.state.weekday).toBe('');
+    expect(header.state.date).toBe('');
+  });
+});
diff --git a/src/js/Pickers/__tests__/PickerControl.js b/src/js/Pickers/__tests__/PickerControl.js
new file mode 100644
index 0000000000..8ac59b8e69
--- /dev/null
+++ b/src/js/Pickers/__tests__/PickerControl.js
@@ -0,0 +1,46 @@
+/*eslint-env jest*/
+jest.unmock('../PickerControl');
+
+import React from 'react';
+import { findDOMNode } from 'react-dom';
+import {
+  Simulate,
+  renderIntoDocument,
+} from 'react-addons-test-utils';
+
+import PickerControl from '../PickerControl';
+
+describe('PickerControl', () => {
+  it('merges className and style', () => {
+    const style = { display: 'block' };
+    const className = 'test';
+    const pickerControl = renderIntoDocument(
+      <PickerControl
+        style={style}
+        className={className}
+        active={false}
+        onClick={jest.genMockFunction()}
+        children="A"
+      />
+    );
+
+    const pickerControlNode = findDOMNode(pickerControl);
+    expect(pickerControlNode.style.display).toBe(style.display);
+    expect(pickerControlNode.classList.contains(className)).toBe(true);
+  });
+
+  it('calls the onClick function when clicked', () => {
+    const onClick = jest.genMockFunction();
+    const pickerControl = renderIntoDocument(
+      <PickerControl
+        active={false}
+        onClick={onClick}
+        children="A"
+      />
+    );
+
+    const pickerControlNode = findDOMNode(pickerControl);
+    Simulate.click(pickerControlNode);
+    expect(onClick.mock.calls.length).toBe(1);
+  });
+});
diff --git a/src/js/Pickers/__tests__/PickerFooter.js b/src/js/Pickers/__tests__/PickerFooter.js
new file mode 100644
index 0000000000..a8936e34c9
--- /dev/null
+++ b/src/js/Pickers/__tests__/PickerFooter.js
@@ -0,0 +1,48 @@
+/*eslint-env jest*/
+jest.unmock('../PickerFooter');
+
+import React from 'react';
+import {
+  renderIntoDocument,
+  scryRenderedComponentsWithType,
+} from 'react-addons-test-utils';
+
+import PickerFooter from '../PickerFooter';
+import FlatButton from '../../Buttons/FlatButton';
+
+describe('PickerFooter', () => {
+  it('renders two flat buttons', () => {
+    const okLabel = 'Ok';
+    const okPrimary = true;
+    const onOkClick = jest.genMockFunction();
+    const cancelLabel = 'Cancel';
+    const cancelPrimary = false;
+    const onCancelClick = jest.genMockFunction();
+
+    const pickerFooter = renderIntoDocument(
+      <PickerFooter
+        okLabel={okLabel}
+        okPrimary={okPrimary}
+        onOkClick={onOkClick}
+        cancelLabel={cancelLabel}
+        cancelPrimary={cancelPrimary}
+        onCancelClick={onCancelClick}
+      />
+    );
+
+    const buttons = scryRenderedComponentsWithType(pickerFooter, FlatButton);
+    expect(buttons.length).toBe(2);
+
+    const [cancel, ok] = buttons;
+
+    expect(cancel.props.label).toBe(cancelLabel);
+    expect(cancel.props.primary).toBe(cancelPrimary);
+    expect(cancel.props.secondary).toBe(!cancelPrimary);
+    expect(cancel.props.onClick).toBe(onCancelClick);
+
+    expect(ok.props.label).toBe(okLabel);
+    expect(ok.props.primary).toBe(okPrimary);
+    expect(ok.props.secondary).toBe(!okPrimary);
+    expect(ok.props.onClick).toBe(onOkClick);
+  });
+});
diff --git a/src/js/Pickers/__tests__/Year.js b/src/js/Pickers/__tests__/Year.js
new file mode 100644
index 0000000000..f309ac2b2f
--- /dev/null
+++ b/src/js/Pickers/__tests__/Year.js
@@ -0,0 +1,33 @@
+/*eslint-env jest*/
+jest.unmock('../Year');
+
+import React from 'react';
+import { findDOMNode } from 'react-dom';
+import {
+  Simulate,
+  renderIntoDocument,
+} from 'react-addons-test-utils';
+
+import Year from '../Year';
+
+describe('Year', () => {
+  it('calls the onClick function when clicked', () => {
+    const onClick = jest.genMockFunction();
+    const year = renderIntoDocument(
+      <Year year={2000} onClick={onClick} active={false} />
+    );
+
+    const yearNode = findDOMNode(year);
+    Simulate.click(yearNode);
+    expect(onClick.mock.calls.length).toBe(1);
+  });
+
+  it('renders the year as children', () => {
+    const year = renderIntoDocument(
+      <Year year={2000} onClick={jest.genMockFunction()} active={false} />
+    );
+
+    const yearNode = findDOMNode(year);
+    expect(yearNode.textContent).toBe('2000');
+  });
+});
diff --git a/src/js/Pickers/__tests__/YearPicker.js b/src/js/Pickers/__tests__/YearPicker.js
new file mode 100644
index 0000000000..bebabdd397
--- /dev/null
+++ b/src/js/Pickers/__tests__/YearPicker.js
@@ -0,0 +1,87 @@
+/*eslint-env jest*/
+jest.unmock('../YearPicker');
+
+import React from 'react';
+import {
+  renderIntoDocument,
+  scryRenderedComponentsWithType,
+} from 'react-addons-test-utils';
+
+import YearPicker from '../YearPicker';
+import Year from '../Year';
+
+describe('YearPicker', () => {
+  it('renders the number of years from initialYearsDisplayed', () => {
+    const props = {
+      calendarTempDate: new Date(2016, 1, 1),
+      onCalendarYearClick: jest.genMockFunction(),
+      initialYearsDisplayed: 100,
+    };
+
+    YearPicker.prototype.componentDidMount = jest.genMockFunction();
+    let yearPicker = renderIntoDocument(<YearPicker {...props} />);
+    let years = scryRenderedComponentsWithType(yearPicker, Year);
+
+    expect(years.length).toBe(props.initialYearsDisplayed);
+
+    props.initialYearsDisplayed = 5;
+    yearPicker = renderIntoDocument(<YearPicker {...props} />);
+    years = scryRenderedComponentsWithType(yearPicker, Year);
+
+    expect(years.length).toBe(props.initialYearsDisplayed);
+  });
+
+  it('renders the number of years from initialYearsDisplayed starting with the minDate', () => {
+    const props = {
+      minDate: new Date(2016, 1, 15),
+      calendarTempDate: new Date(2016, 1, 1),
+      onCalendarYearClick: jest.genMockFunction(),
+      initialYearsDisplayed: 100,
+    };
+
+    YearPicker.prototype.componentDidMount = jest.genMockFunction();
+    const yearPicker = renderIntoDocument(<YearPicker {...props} />);
+    const years = scryRenderedComponentsWithType(yearPicker, Year);
+
+    // Even numbers have one more
+    expect(years.length).toBe(props.initialYearsDisplayed);
+    expect(years[0].props.year).toBe(props.minDate.getFullYear());
+    expect(years[props.initialYearsDisplayed - 1].props.year).toBe(props.minDate.getFullYear() + props.initialYearsDisplayed - 1);
+  });
+
+  it('renders the number of years from initialYearsDisplayed ending with the maxDate', () => {
+    const props = {
+      maxDate: new Date(2016, 1, 15),
+      calendarTempDate: new Date(2016, 1, 1),
+      onCalendarYearClick: jest.genMockFunction(),
+      initialYearsDisplayed: 100,
+    };
+
+    YearPicker.prototype.componentDidMount = jest.genMockFunction();
+    const yearPicker = renderIntoDocument(<YearPicker {...props} />);
+    const years = scryRenderedComponentsWithType(yearPicker, Year);
+
+    // Even numbers have one more
+    expect(years.length).toBe(props.initialYearsDisplayed);
+    expect(years[props.initialYearsDisplayed - 1].props.year).toBe(props.maxDate.getFullYear());
+    expect(years[0].props.year).toBe(props.maxDate.getFullYear() - props.initialYearsDisplayed + 1);
+  });
+
+  it('only renders years between the min and max dates', () => {
+    const props = {
+      minDate: new Date(2012),
+      maxDate: new Date(2015),
+      calendarTempDate: new Date(2012, 1, 1),
+      onCalendarYearClick: jest.genMockFunction(),
+      initialYearsDisplayed: 100,
+    };
+
+    YearPicker.prototype.componentDidMount = jest.genMockFunction();
+    const yearPicker = renderIntoDocument(<YearPicker {...props} />);
+    const years = scryRenderedComponentsWithType(yearPicker, Year);
+    expect(years.length).toBe(props.maxDate.getFullYear() - props.minDate.getFullYear() + 1);
+
+    expect(years[0].props.year).toBe(props.minDate.getFullYear());
+    expect(years[years.length -1].props.year).toBe(props.maxDate.getFullYear());
+  });
+});