Repository: mlaursen/react-md
Pull request #93 (2016-05-26 15:43:23)

Enhancement/#50 time picker tests
This should solve #50 


Patch:
diff --git a/.travis.yml b/.travis.yml
index 481c8cb08d..1874b7ec9f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,6 +3,10 @@ before_install:
   - gem install sass scss_lint
 node_js:
   - "4"
+sudo: false
+cache:
+  directories:
+    - node_modules
 script:
   - npm run lint
   - npm test
diff --git a/src/js/Dialogs/__mocks__/index.js b/src/js/Dialogs/__mocks__/index.js
index 52396a7267..87629e04de 100644
--- a/src/js/Dialogs/__mocks__/index.js
+++ b/src/js/Dialogs/__mocks__/index.js
@@ -1,7 +1,7 @@
 /*eslint-env jest*/
 import React from 'react';
 
-export default jest.genMockFunction().mockImplementation(({ isOpen, children }) => {
+export default jest.fn(({ isOpen, children }) => {
   return (
     <div>{isOpen && children}</div>
   );
diff --git a/src/js/Pickers/ClockFace.js b/src/js/Pickers/ClockFace.js
index 334167187c..f1a995b094 100644
--- a/src/js/Pickers/ClockFace.js
+++ b/src/js/Pickers/ClockFace.js
@@ -2,114 +2,113 @@ import React, { Component, PropTypes } from 'react';
 import PureRenderMixin from 'react-addons-pure-render-mixin';
 
 import { LEFT_MOUSE } from '../constants/keyCodes';
-import { getTouchOffset, isPointInCircle } from '../utils';
+import { getTouchOffset, isPointInCircle, isTouchDevice } from '../utils';
 import ClockTime from './ClockTime';
 import ClockHand from './ClockHand';
 
+/**
+ * The `ClockFace` component is used for rendering all the clock's times
+ * and the clock hand.
+ */
 export default class ClockFace extends Component {
   constructor(props) {
     super(props);
 
     this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
-    this.state = { radius: 136 };
+    this.state = { radius: 136, moving: false };
   }
 
   static propTypes = {
+    /**
+     * The current time for the clock.
+     */
     time: PropTypes.number.isRequired,
+
+    /**
+     * Boolean if the clock is on the minutes view.
+     */
     minutes: PropTypes.bool.isRequired,
+
+    /**
+     * A function to call when a new time is selected. It gives the
+     * new time value. If it is 12 o'clock, 0 will be given.
+     */
     onClick: PropTypes.func.isRequired,
+
+    /**
+     * An optional time period string. This should be either AM or PM
+     * if the locale uses them.
+     */
     timePeriod: PropTypes.string,
   };
 
   componentDidMount() {
-    this.updateRadius();
+    this.init();
   }
 
-  updateRadius = () => {
-    const { offsetWidth } = this.refs.clockFace;
+  init = () => {
+    const radius = this.refs.clockFace.offsetWidth / 2;
+    const touch = isTouchDevice();
 
-    this.setState({ radius: offsetWidth / 2 });
+    this.setState({ radius, touch });
   };
 
   handleMouseDown = (e) => {
-    if(e.button !== LEFT_MOUSE || e.ctrlKey) { return; }
-    this.refs.clockFace.addEventListener('mousemove', this.handleMouseMove);
+    if(e.button !== LEFT_MOUSE || e.ctrlKey || this.state.touch) { return; }
+    this.setState({ moving: true });
   };
 
   handleMouseMove = (e) => {
+    if(!this.state.moving || this.state.touch) { return; }
     e.preventDefault();
-    let { offsetX, offsetY } = e;
-    if(typeof offsetX === 'undefined' || typeof offsetY === 'undefined') {
-      const offset = getTouchOffset(e);
-
-      offsetX = offset.offsetX;
-      offsetY = offset.offsetY;
-    }
-
-    this.calcNewTime(offsetX, offsetY);
+    this.calcNewTime(e);
   };
 
   handleMouseUp = (e) => {
-    if(e.button !== LEFT_MOUSE || e.ctrlKey) { return; }
-    this.refs.clockFace.removeEventListener('mousemove', this.handleMouseMove);
-    let { offsetX, offsetY } = e;
-    if(typeof offsetX === 'undefined' || typeof offsetY === 'undefined') {
-      const offset = getTouchOffset(e);
-
-      offsetX = offset.offsetX;
-      offsetY = offset.offsetY;
-    }
+    if(e.button !== LEFT_MOUSE || e.ctrlKey || this.state.touch) { return; }
+    this.calcNewTime(e);
+    this.setState({ moving: false });
+  };
 
-    this.calcNewTime(offsetX, offsetY);
+  handleTouchStart = () => {
+    this.setState({ moving: true });
   };
 
   handleTouchMove = (e) => {
+    if(!this.state.moving) { return; }
     e.preventDefault();
-    let { offsetX, offsetY } = e.changedTouches[0];
-    if(typeof offsetX === 'undefined' || typeof offsetY === 'undefined') {
-      const offset = getTouchOffset(e);
-
-      offsetX = offset.offsetX;
-      offsetY = offset.offsetY;
-    }
-
-    this.calcNewTime(offsetX, offsetY);
+    this.calcNewTime(e);
   };
 
   handleTouchEnd = (e) => {
-    let { offsetX, offsetY } = e.changedTouches[0];
-    if(typeof offsetX === 'undefined' || typeof offsetY === 'undefined') {
-      const offset = getTouchOffset(e);
-
-      offsetX = offset.offsetX;
-      offsetY = offset.offsetY;
-    }
-
-    this.calcNewTime(offsetX, offsetY);
+    this.calcNewTime(e);
+    this.setState({ moving: false });
   };
 
-  calcNewTime = (x, y) => {
+  calcNewTime = (e) => {
+    const { offsetX, offsetY } = getTouchOffset(e);
     const { radius } = this.state;
-    const sectors = this.props.minutes ? 60 : 12;
+    const { minutes, timePeriod, onClick } = this.props;
+    const sectors = minutes ? 60 : 12;
     const sectorSize = 360 / sectors;
-    const atan = Math.atan2(y - radius, x - radius);
+    const atan = Math.atan2(offsetY - radius, offsetX - radius);
     const degrees = atan * (180 / Math.PI);
     let time = Math.round(degrees / sectorSize);
 
     // time will be a negative number if it is the top half of the circle
-    time += (this.props.minutes ? 15 : 3);
+    time += (minutes ? 15 : 3);
     if(time < 0) {
       time += sectors;
     }
 
-    if(!this.props.timePeriod) {
-      const isInCircle = isPointInCircle(radius, radius, radius - 48, x, y);
+    if(!timePeriod && !minutes) {
+      const isInCircle = isPointInCircle(radius, radius, radius - 48, offsetX, offsetY);
       if((isInCircle && time !== 0) || (!isInCircle && time === 0)) {
         time += 12;
       }
     }
 
-    this.props.onClick(time);
+    onClick(time);
   };
 
   render() {
@@ -131,18 +130,20 @@ export default class ClockFace extends Component {
           time={clockTime}
           active={clockTime === time}
           radius={radius}
-          minutes={minutes}
         />
       );
     });
+
     return (
       <div
         className="md-clock-face"
         ref="clockFace"
         onMouseDown={this.handleMouseDown}
+        onMouseMove={this.handleMouseMove}
         onMouseUp={this.handleMouseUp}
+        onTouchStart={this.handleTouchStart}
         onTouchMove={this.handleTouchMove}
-        onTouchEnd={this.handleTouchStart}
+        onTouchEnd={this.handleTouchEnd}
       >
         {times}
         <ClockHand time={time} coords={radius} minutes={minutes} />
diff --git a/src/js/Pickers/ClockHand.js b/src/js/Pickers/ClockHand.js
index 59a876bf25..d31bd2de2e 100644
--- a/src/js/Pickers/ClockHand.js
+++ b/src/js/Pickers/ClockHand.js
@@ -2,6 +2,10 @@ import React, { Component, PropTypes } from 'react';
 import PureRenderMixin from 'react-addons-pure-render-mixin';
 import classnames from 'classnames';
 
+/**
+ * The `ClockHand` component is just used to display the hand of the clock
+ * and a ball to surround the selected time.
+ */
 export default class ClockHand extends Component {
   constructor(props) {
     super(props);
@@ -11,8 +15,20 @@ export default class ClockHand extends Component {
   }
 
   static propTypes = {
+    /**
+     * This is the x and y coordinate to use for the center of the `ClockFace`.
+     * This should really be whatever the radius of the `ClockFace` is.
+     */
     coords: PropTypes.number,
+
+    /**
+     * The current time of the clock.
+     */
     time: PropTypes.number.isRequired,
+
+    /**
+     * Boolean if the clock is displaying minutes instead of hours.
+     */
     minutes: PropTypes.bool.isRequired,
   };
 
@@ -58,10 +74,10 @@ export default class ClockHand extends Component {
         style={{
           left: coords,
           top: coords,
-          transform: rotateTransform,
-          msTransform: rotateTransform,
           WebkitTransform: rotateTransform,
           MozTransform: rotateTransform,
+          msTransform: rotateTransform,
+          transform: rotateTransform,
         }}
       />
     );
diff --git a/src/js/Pickers/ClockTime.js b/src/js/Pickers/ClockTime.js
index 0bffaf17a2..0d73cd2445 100644
--- a/src/js/Pickers/ClockTime.js
+++ b/src/js/Pickers/ClockTime.js
@@ -4,6 +4,11 @@ import classnames from 'classnames';
 
 const CLOCK_PADDING = 4;
 
+/**
+ * The `ClockTime` component is used for positioning hours or minutes
+ * in a clock. The time will be positioned based on it's given index
+ * and the radius of the clock.
+ */
 export default class ClockTime extends Component {
   constructor(props) {
     super(props);
@@ -16,48 +21,63 @@ export default class ClockTime extends Component {
   }
 
   static propTypes = {
+    /**
+     * The index of the current time to be displayed. This
+     * should be a number between 1 and 24.
+     */
     index: PropTypes.number.isRequired,
+
+    /**
+     * The time number to display.
+     */
     time: PropTypes.number.isRequired,
+
+    /**
+     * Boolean if this time is currently selected.
+     */
     active: PropTypes.bool.isRequired,
+
+    /**
+     * The radius of the clock.
+     */
     radius: PropTypes.number.isRequired,
-    minutes: PropTypes.bool.isRequired,
   };
 
   componentDidMount() {
-    const { offsetWidth } = this.refs.time;
-    this.setState({ size: offsetWidth / 2 }); // eslint-disable-line react/no-did-mount-set-state
+    this.setPosition();
   }
 
-  calcPos = (r, inner, isTop) => {
-    const { radius } = this.props;
-    const { size } = this.state;
+  componentWillReceiveProps(nextProps) {
+    if(this.props.radius !== nextProps.radius || this.props.index !== nextProps.index) {
 
-    const outerR = radius - size;
-    let innerR = outerR - CLOCK_PADDING;
-    if(inner) {
-      innerR = outerR - size * 2 - CLOCK_PADDING;
+      this.setPosition(nextProps);
     }
+  }
 
-    if(isTop) {
-      return outerR - innerR * Math.sin(r);
-    } else {
-      return outerR + innerR * Math.cos(r);
-    }
+  setPosition = ({ radius, index } = this.props) => {
+    // 36 is default size for the time
+    const size = (this.refs.time.offsetWidth || 36) / 2;
+    const timeRadians = (Math.PI / 2) - index * (Math.PI / 6);
+    const innerCircle = index > 12;
+
+    const outerRadius = radius - size;
+    const innerRadius = outerRadius - CLOCK_PADDING - (innerCircle ? size * 2 : 0);
+
+    this.setState({
+      style: {
+        top: outerRadius - innerRadius * Math.sin(timeRadians),
+        left: outerRadius + innerRadius * Math.cos(timeRadians),
+      },
+    });
   };
 
   render() {
-    const { time, active, index } = this.props;
-
-    const r = (Math.PI / 2) - index * (Math.PI / 6);
-    const inner = index > 12;
+    const { time, active } = this.props;
     return (
       <div
         ref="time"
         className={classnames('md-clock-time', { active })}
-        style={{
-          top: this.calcPos(r, inner, true),
-          left: this.calcPos(r, inner, false),
-        }}
+        style={this.state.style}
       >
         <span className="md-clock-time-value">{time}</span>
       </div>
diff --git a/src/js/Pickers/DatePickerContainer.js b/src/js/Pickers/DatePickerContainer.js
index f47f3f31fc..23d88c8a97 100644
--- a/src/js/Pickers/DatePickerContainer.js
+++ b/src/js/Pickers/DatePickerContainer.js
@@ -360,7 +360,7 @@ export default class DatePickerContainer extends Component {
    */
   getValue = (props = this.props, state = this.state) => {
     const { DateTimeFormat, locales } = props;
-    const value = typeof this.props.value !== 'undefined' ? props.value : state.value;
+    const value = typeof props.value !== 'undefined' ? props.value : state.value;
     if(!value) {
       return '';
     } else if(value instanceof Date) {
@@ -387,23 +387,29 @@ export default class DatePickerContainer extends Component {
     delete props.value;
     delete props.onChange;
 
-    const pickerProps = {
-      ...state,
-      ...props,
-      style: pickerStyle,
-      className: classnames('md-picker', displayMode, pickerClassName, { inline, 'with-icon': inline && icon }),
-      onCancelClick: this.handleCancelClick,
-      onOkClick: this.handleOkClick,
-      changeCalendarMode: this.changeCalendarMode,
-      onPreviousClick: this.previousMonth,
-      onNextClick: this.nextMonth,
-      onCalendarDateClick: this.setCalendarTempDate,
-      onCalendarYearClick: this.setCalendarTempYear,
-      onSwipeChange: this.handleSwipeChange,
-    };
+    let picker, content;
+    if(isOpen) {
+      picker = (
+        <DatePicker
+          {...state}
+          {...props}
+          style={pickerStyle}
+          className={classnames('md-picker', displayMode, pickerClassName, {
+            inline,
+            'with-icon': inline && icon,
+          })}
+          onCancelClick={this.handleCancelClick}
+          onOkClick={this.handleOkClick}
+          changeCalendarMode={this.changeCalendarMode}
+          onPreviousClick={this.previousMonth}
+          onNextClick={this.nextMonth}
+          onCalendarDateClick={this.setCalendarTempDate}
+          onCalendarYearClick={this.setCalendarTempYear}
+          onSwipeChange={this.handleSwipeChange}
+        />
+      );
+    }
 
-    let picker = isOpen ? <DatePicker {...pickerProps} /> : null;
-    let content;
     if(inline) {
       picker = isOpen ? <Height transitionEnterTimeout={150} transitionLeaveTimeout={150}>{picker}</Height> : null;
       content = <TransitionGroup>{picker}</TransitionGroup>;
diff --git a/src/js/Pickers/DatePickerHeader.js b/src/js/Pickers/DatePickerHeader.js
index a909596b41..94531e760c 100644
--- a/src/js/Pickers/DatePickerHeader.js
+++ b/src/js/Pickers/DatePickerHeader.js
@@ -27,7 +27,7 @@ export default class DatePickerHeader extends Component {
     changeCalendarMode: PropTypes.func.isRequired,
   };
 
-  componentWillUpdate(nextProps) {
+  componentWillReceiveProps(nextProps) {
     const { DateTimeFormat, locales, calendarTempDate, changeCalendarMode } = this.props;
     if(DateTimeFormat !== nextProps.DateTimeFormat
       || locales !== nextProps.locales
@@ -62,7 +62,7 @@ export default class DatePickerHeader extends Component {
           <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">{weekday + ','}&nbsp;</h4>
           <h4 className="md-display-1">{date}</h4>
         </PickerControl>
       </header>
diff --git a/src/js/Pickers/TimePeriods.js b/src/js/Pickers/TimePeriods.js
new file mode 100644
index 0000000000..77f6d22232
--- /dev/null
+++ b/src/js/Pickers/TimePeriods.js
@@ -0,0 +1,60 @@
+import React, { Component, PropTypes } from 'react';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+
+import { addHours, subtractHours } from '../utils/dates';
+import PickerControl from './PickerControl';
+
+/**
+ * This component displays a section for switching between the AM
+ * and PM time periods.
+ */
+export default class TimePeriods extends Component {
+  constructor(props) {
+    super(props);
+
+    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
+  }
+
+  static propTypes = {
+    /**
+     * The current time for the time picker.
+     */
+    tempTime: PropTypes.instanceOf(Date).isRequired,
+
+    /**
+     * A function to update the time for the time picker.
+     */
+    setTempTime: PropTypes.func.isRequired,
+
+    /**
+     * The current time period.
+     */
+    timePeriod: PropTypes.string.isRequired,
+  };
+
+  setAM = () => {
+    if(this.props.timePeriod !== 'AM') {
+      this.props.setTempTime(addHours(this.props.tempTime, 12));
+    }
+  };
+
+  setPM = () => {
+    if(this.props.timePeriod !== 'PM') {
+      this.props.setTempTime(subtractHours(this.props.tempTime, 12));
+    }
+  };
+
+  render() {
+    const { timePeriod } = this.props;
+    return (
+      <div className="md-time-periods">
+        <PickerControl onClick={this.setAM} active={timePeriod === 'AM'}>
+          <h6 className="md-subtitle">AM</h6>
+        </PickerControl>
+        <PickerControl onClick={this.setPM} active={timePeriod === 'PM'}>
+          <h6 className="md-subtitle">PM</h6>
+        </PickerControl>
+      </div>
+    );
+  }
+}
diff --git a/src/js/Pickers/TimePicker.js b/src/js/Pickers/TimePicker.js
index 0ec0a1bf36..44a2bae621 100644
--- a/src/js/Pickers/TimePicker.js
+++ b/src/js/Pickers/TimePicker.js
@@ -5,6 +5,10 @@ import PickerFooter from './PickerFooter';
 import ClockFace from './ClockFace';
 import TimePickerHeader from './TimePickerHeader';
 
+/**
+ * The `TimePicker` component is used to display a time picker
+ * in the `TimePickerContainer` component.
+ */
 export default class TimePicker extends Component {
   constructor(props) {
     super(props);
@@ -25,12 +29,46 @@ export default class TimePicker extends Component {
       PropTypes.string,
       PropTypes.arrayOf(PropTypes.string),
     ]).isRequired,
+
+    /**
+     * A function that will switch the state from hour to minute.
+     */
     setTimeMode: PropTypes.func.isRequired,
+
+    /**
+     * A function that will update the time for the TimePicker before
+     * the user selects ok. This function will be given a new Date object
+     * with a modified time.
+     */
     setTempTime: PropTypes.func.isRequired,
+
+    /**
+     * The current display mode of the time picker.
+     */
     timeMode: PropTypes.oneOf(['hour', 'minute']).isRequired,
+
+    /**
+     * The current time as a date object that is being displayed in the
+     * time picker.
+     */
     tempTime: PropTypes.instanceOf(Date).isRequired,
+
+    /**
+     * A string that is a representation of the hours in the user's locale.
+     */
     hours: PropTypes.string.isRequired,
+
+    /**
+     * A string that is a representation of the minutes in the user's locale.
+     * This will also include any separator the locale uses.
+     *
+     * Example: ':15' in '3:15 PM' for 'en-US'
+     */
     minutes: PropTypes.string.isRequired,
+
+    /**
+     * An optional time period if a user's locale uses it.
+     */
     timePeriod: PropTypes.string,
   };
 
@@ -38,7 +76,11 @@ export default class TimePicker extends Component {
     const { tempTime, setTempTime, timeMode, timePeriod } = this.props;
     const time = new Date(tempTime);
     if(timeMode === 'hour') {
-      if(timePeriod && timePeriod === 'PM') {
+      const isAM = timePeriod === 'AM';
+      const is12 = timePart === 12;
+      if(timePeriod && isAM && is12) {
+        timePart = 0;
+      } else if(timePeriod && !isAM && !is12) {
         timePart += 12;
       }
 
diff --git a/src/js/Pickers/TimePickerContainer.js b/src/js/Pickers/TimePickerContainer.js
index 990a196668..5b7bd1ec60 100644
--- a/src/js/Pickers/TimePickerContainer.js
+++ b/src/js/Pickers/TimePickerContainer.js
@@ -8,7 +8,7 @@ import { onOutsideClick } from '../utils';
 import { DateTimeFormat, getTimeString, extractTimeParts } from '../utils/dates';
 import Dialog from '../Dialogs';
 import FontIcon from '../FontIcons';
-import { Height } from '../Transitions';
+import Height from '../Transitions/Height';
 import TextField from '../TextFields';
 import TimePicker from './TimePicker';
 
@@ -18,24 +18,46 @@ export default class TimePickerContainer extends Component {
 
     this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
 
-    const date = props.defaultValue ? new Date(props.defaultValue) : new Date();
+    let initialDate;
+    if(props.defaultValue) {
+      initialDate = new Date(props.defaultValue);
+    } else if(props.value) {
+      initialDate = new Date(props.value);
+    } else {
+      initialDate = new Date();
+    }
 
     this.state = {
-      ...this.getTimeParts(date, props),
+      ...this.getTimeParts(initialDate, props),
       value: props.defaultValue,
       isOpen: props.initiallyOpen,
-      time: date,
+      time: initialDate,
       timeMode: props.initialTimeMode,
-      tempTime: date,
+      tempTime: initialDate,
     };
   }
 
   static propTypes = {
     /**
-     * An optional className to apply to the year picker.
+     * An optional style to apply to the time picker's container.
+     */
+    style: PropTypes.object,
+
+    /**
+     * An optional className to apply to the time picker's container.
      */
     className: PropTypes.string,
 
+    /**
+     * An optional style to apply to the time picker.
+     */
+    pickerStyle: PropTypes.object,
+
+    /**
+     * An optional className to apply to the time picker.
+     */
+    pickerClassName: PropTypes.string,
+
     /**
      * An optional icon to display with the time picker.
      */
@@ -151,7 +173,9 @@ export default class TimePickerContainer extends Component {
     initialTimeMode: 'hour',
     icon: <FontIcon>access_time</FontIcon>,
     DateTimeFormat: DateTimeFormat,
-    locales: window.navigator.userLanguage || window.navigator.language,
+    locales: typeof window !== 'undefined'
+      ? window.navigator.userLanguage || window.navigator.language
+      : 'en-US',
     okLabel: 'Ok',
     okPrimary: true,
     cancelLabel: 'Cancel',
@@ -228,51 +252,90 @@ export default class TimePickerContainer extends Component {
     this.setState({ isOpen: false, tempTime: this.state.time });
   };
 
-  render() {
-    const { label, floatingLabel, value, onChange, icon, inline, displayMode, ...props } = this.props;
-    const { isOpen, ...state } = this.state;
+  getTextFieldValue = (props = this.props, state = this.state) => {
+    const { DateTimeFormat, locales } = props;
+    const value = this.getValue(props, state);
+    if(!value) {
+      return '';
+    } else if(value instanceof Date) {
+      return getTimeString(DateTimeFormat, locales, value);
+    } else {
+      // currently don't support value of string
+      return value;
+    }
+  };
 
-    const pickerProps = {
-      ...state,
+  render() {
+    const {
+      isOpen,
+      timeMode,
+      tempTime,
+      hours,
+      minutes,
+      timePeriod,
+    } = this.state;
+
+    const {
+      label,
+      floatingLabel,
+      icon,
+      inline,
+      displayMode,
+      style,
+      className,
+      pickerStyle,
+      pickerClassName,
       ...props,
-      className: classnames('md-picker', displayMode, { inline, 'with-icon': inline && icon }),
-      onOkClick: this.handleOkClick,
-      onCancelClick: this.handleCancelClick,
-      setTimeMode: this.setTimeMode,
-      setTempTime: this.setTempTime,
-    };
-
-    let textFieldValue = typeof value === 'undefined' ? state.value : value;
-    if(isOpen && inline) {
-      textFieldValue = this.getValue(this.props, this.state);
+    } = this.props;
+    delete props.value;
+    delete props.onChange;
+    delete props.defaultValue;
+
+    let picker, content;
+    if(isOpen) {
+      picker = (
+        <TimePicker
+          {...props}
+          tempTime={tempTime}
+          timeMode={timeMode}
+          hours={hours}
+          minutes={minutes}
+          timePeriod={timePeriod}
+          style={pickerStyle}
+          className={classnames('md-picker', displayMode, pickerClassName, {
+            inline,
+            'with-icon': inline && icon,
+          })}
+          onOkClick={this.handleOkClick}
+          onCancelClick={this.handleCancelClick}
+          setTimeMode={this.setTimeMode}
+          setTempTime={this.setTempTime}
+        />
+      );
     }
 
-    if(textFieldValue) {
-      textFieldValue = getTimeString(props.DateTimeFormat, props.locales, textFieldValue);
+    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
+        style={style}
+        className={classnames('md-picker-container', className)}
+        ref="container"
+      >
         <TextField
           icon={icon}
           onClick={this.toggleOpen}
           label={label}
           floatingLabel={floatingLabel}
-          value={textFieldValue}
-          onChange={onChange}
+          value={this.getTextFieldValue()}
+          readOnly={true}
         />
-        {inline ?
-          <TransitionGroup>
-            {isOpen &&
-              <Height transitionEnterTimeout={150} transitionLeaveTimeout={150}>
-                <TimePicker {...pickerProps} />
-              </Height>
-            }
-          </TransitionGroup> :
-          <Dialog isOpen={isOpen} close={this.close}>
-            {isOpen && <TimePicker {...pickerProps} />}
-          </Dialog>
-        }
+        {content}
       </div>
     );
   }
diff --git a/src/js/Pickers/TimePickerHeader.js b/src/js/Pickers/TimePickerHeader.js
index 10f741fbd0..38596c1b6f 100644
--- a/src/js/Pickers/TimePickerHeader.js
+++ b/src/js/Pickers/TimePickerHeader.js
@@ -1,43 +1,90 @@
-import React, { PropTypes } from 'react';
+import React, { Component, PropTypes } from 'react';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
 
+import TimePeriods from './TimePeriods';
 import PickerControl from './PickerControl';
-import { addHours, subtractHours } from '../utils/dates';
-
-const TimePickerHeader = ({ setTimeMode, setTempTime, timeMode, tempTime, hours, minutes, timePeriod }) => {
-  return (
-    <header className="md-picker-header">
-      <PickerControl onClick={setTimeMode.bind(this, 'hour')} active={timeMode === 'hour'}>
-        <h4 className="md-display-3">
-          {hours}
-        </h4>
-      </PickerControl>
-      <PickerControl onClick={setTimeMode.bind(this, 'minute')} active={timeMode === 'minute'}>
-        <h4 className="md-display-3">
-          {minutes}
-        </h4>
-      </PickerControl>
-      {timePeriod &&
-      <div className="md-time-periods">
-        <PickerControl onClick={() => setTempTime(addHours(tempTime, 12))} active={timePeriod === 'AM'}>
-          <h6 className="md-subtitle">AM</h6>
+
+/**
+ * The `TimePickerHeader` component is used for rendering the
+ * current time for the `TimePicker` as well as switching between
+ * the different views for the time picker.
+ */
+export default class TimePickerHeader extends Component {
+  constructor(props) {
+    super(props);
+
+    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
+  }
+
+  static propTypes = {
+    /**
+     * The current time of the time picker.
+     */
+    tempTime: PropTypes.instanceOf(Date).isRequired,
+
+    /**
+     * The current time type that is being changed.
+     */
+    timeMode: PropTypes.oneOf(['hour', 'minute']).isRequired,
+
+    /**
+     * A function to update the time mode.
+     */
+    setTimeMode: PropTypes.func.isRequired,
+
+    /**
+     * A function to update the time for the time picker.
+     */
+    setTempTime: PropTypes.func.isRequired,
+
+    /**
+     * A formatted hours string for the user's locale. This
+     * would be '3' for en-US if the time was '3:15'
+     */
+    hours: PropTypes.string.isRequired,
+
+    /**
+     * A formatted minutes string for the user's locale.
+     * This would be ':15' for en-US if the time was '3:15'.
+     */
+    minutes: PropTypes.string.isRequired,
+
+    /**
+     * An optional time period to use for locales that use
+     * 12 hour clocks and AM/PM.
+     */
+    timePeriod: PropTypes.string,
+  };
+
+  setHour = () => {
+    this.props.setTimeMode('hour');
+  };
+
+  setMinute = () => {
+    this.props.setTimeMode('minute');
+  };
+
+  render() {
+    const { timeMode, hours, minutes, timePeriod, setTempTime, tempTime } = this.props;
+    let timePeriods;
+    if(timePeriod) {
+      timePeriods = <TimePeriods tempTime={tempTime} setTempTime={setTempTime} timePeriod={timePeriod} />;
+    }
+
+    return (
+      <header className="md-picker-header">
+        <PickerControl onClick={this.setHour} active={timeMode === 'hour'}>
+          <h4 className="md-display-3">
+            {hours}
+          </h4>
         </PickerControl>
-        <PickerControl onClick={() => setTempTime(subtractHours(tempTime, 12))} active={timePeriod === 'PM'}>
-          <h6 className="md-subtitle">PM</h6>
+        <PickerControl onClick={this.setMinute} active={timeMode === 'minute'}>
+          <h4 className="md-display-3">
+            {minutes}
+          </h4>
         </PickerControl>
-      </div>
-      }
-    </header>
-  );
-};
-
-TimePickerHeader.propTypes = {
-  tempTime: PropTypes.instanceOf(Date).isRequired,
-  timeMode: PropTypes.oneOf(['hour', 'minute']).isRequired,
-  setTimeMode: PropTypes.func.isRequired,
-  setTempTime: PropTypes.func.isRequired,
-  hours: PropTypes.string.isRequired,
-  minutes: PropTypes.string.isRequired,
-  timePeriod: PropTypes.string,
-};
-
-export default TimePickerHeader;
+        {timePeriods}
+      </header>
+    );
+  }
+}
diff --git a/src/js/utils/__mocks__/index.js b/src/js/utils/__mocks__/index.js
index 208baffb43..55429732ad 100644
--- a/src/js/utils/__mocks__/index.js
+++ b/src/js/utils/__mocks__/index.js
@@ -9,3 +9,12 @@ export const getOffset = jest.genMockFunction().mockImplementation(() => {
 export const animate = jest.genMockFunction();
 export const isBetween = jest.genMockFunction().mockImplementation(() => true);
 export const isTouchDevice = jest.genMockFunction();
+export const getTouchOffset = jest.fn(e => {
+  if(!e) {
+    // position at 12 o'clock
+    return { offsetX: 136, offsetY: 12 };
+  } else {
+    return { offsetX: e.offsetX, offsetY: e.offsetY };
+  }
+});
+export const isPointInCircle = jest.genMockFunction().mockImplementation(() => true);
diff --git a/src/js/utils/index.js b/src/js/utils/index.js
index b49a86281d..7acffde593 100644
--- a/src/js/utils/index.js
+++ b/src/js/utils/index.js
@@ -45,15 +45,33 @@ export function getOffset(el) {
   };
 }
 
+/**
+ * Generates an object of an offsetX and offsetY from
+ * a mouse or touch event.
+ *
+ * @param {Object} event The event to extract data from
+ * @return {Object} an object holding the offsetX and offsetY of the event.
+ */
 export function getTouchOffset(event) {
   const el = event.target;
   const rect = el.getBoundingClientRect();
+  const { clientX, clientY } = event.changedTouches ? event.changedTouches[0] : event;
   return {
-    offsetX: event.clientX - rect.left,
-    offsetY: event.clientY - rect.top,
+    offsetX: clientX - rect.left,
+    offsetY: clientY - rect.top,
   };
 }
 
+/**
+ * Determines if a point is in a circle.
+ *
+ * @param {Number} cx the center X coordinate in the circle
+ * @param {Number} cy the center Y coordinate in the circle
+ * @param {Number} r the radius of the circle
+ * @param {Number} x the x coordinate to check
+ * @param {Number} y the y coordinate to check
+ * @return {bool} true if the given x and y coordinates are in the circle.
+ */
 export function isPointInCircle(cx, cy, r, x, y) {
   const distance = Math.pow(cx - x, 2) + Math.pow(cy - y, 2);
   return distance <= Math.pow(r, 2);
diff --git a/src/scss/helpers/mixins/_lists.scss b/src/scss/helpers/mixins/_lists.scss
index 86c42fab01..ec5d3a9be4 100644
--- a/src/scss/helpers/mixins/_lists.scss
+++ b/src/scss/helpers/mixins/_lists.scss
@@ -55,7 +55,7 @@
 
       .md-tile-secondary-text {
         // Allows two lines of text
-        height: $md-line-height * $md-list-mobile-font-size * 2;
+        height: 36px;
       }
     }
   }


Test patch:
diff --git a/src/js/Pickers/__tests__/ClockFace.js b/src/js/Pickers/__tests__/ClockFace.js
new file mode 100644
index 0000000000..1e4c63bdb3
--- /dev/null
+++ b/src/js/Pickers/__tests__/ClockFace.js
@@ -0,0 +1,190 @@
+/*eslint-env jest*/
+jest.unmock('../ClockFace');
+
+import React from 'react';
+import { findDOMNode } from 'react-dom';
+import {
+  Simulate,
+  renderIntoDocument,
+//  findRenderedComponentWithType,
+  scryRenderedComponentsWithType,
+//  scryRenderedDOMComponentsWithTag,
+} from 'react-addons-test-utils';
+
+import { LEFT_MOUSE, RIGHT_MOUSE } from '../../constants/keyCodes';
+import ClockFace from '../ClockFace';
+import ClockTime from '../ClockTime';
+import ClockHand from '../ClockHand';
+
+describe('ClockFace', () => {
+  it('renders a 12 hour clock if there is a time period prop and is not displayed in minutes', () => {
+    const onClick = jest.fn();
+    const face = renderIntoDocument(
+      <ClockFace time={3} minutes={false} onClick={onClick} timePeriod="AM" />
+    );
+
+    const times = scryRenderedComponentsWithType(face, ClockTime);
+    expect(times.length).toBe(12);
+  });
+
+  it('renders a 24 hour clock if there is not a time period prop and is not displayed in minutes', () => {
+    const onClick = jest.fn();
+    const face = renderIntoDocument(
+      <ClockFace time={3} minutes={false} onClick={onClick} />
+    );
+
+    const times = scryRenderedComponentsWithType(face, ClockTime);
+    expect(times.length).toBe(24);
+  });
+
+  it('renders minutes in 5 minute increments starting from 0', () => {
+    const onClick = jest.fn();
+    let face = renderIntoDocument(
+      <ClockFace time={3} minutes={true} onClick={onClick} />
+    );
+
+    let times = scryRenderedComponentsWithType(face, ClockTime);
+    expect(times.length).toBe(60 / 5);
+
+    face = renderIntoDocument(
+      <ClockFace time={3} minutes={true} onClick={onClick} timePeriod="AM" />
+    );
+
+    times = scryRenderedComponentsWithType(face, ClockTime);
+    expect(times.length).toBe(60 / 5);
+  });
+
+  it('sets the prop active for the correct time', () => {
+    const onClick = jest.fn();
+    let face = renderIntoDocument(
+      <ClockFace time={3} minutes={false} onClick={onClick} />
+    );
+
+    let times = scryRenderedComponentsWithType(face, ClockTime);
+    expect(times[2].props.active).toBe(true);
+
+    face = renderIntoDocument(
+      <ClockFace time={3} minutes={false} onClick={onClick} timePeriod="AM" />
+    );
+
+    times = scryRenderedComponentsWithType(face, ClockTime);
+    expect(times[2].props.active).toBe(true);
+  });
+
+  it('renders a clock hand component', () => {
+    const props = {
+      onClick: jest.fn(),
+      time: 3,
+      minutes: true,
+    };
+
+    const face = renderIntoDocument(<ClockFace {...props} />);
+    face.setState({ radius: 136 });
+
+    const hands = scryRenderedComponentsWithType(face, ClockHand);
+    expect(hands.length).toBe(1);
+    const [hand] = hands;
+    expect(hand.props.time).toBe(props.time);
+    expect(hand.props.coords).toBe(136);
+    expect(hand.props.minutes).toBe(props.minutes);
+  });
+
+  it('calls the on click prop with the selected time on mouse up', () => {
+    const onClick = jest.fn();
+    const face = renderIntoDocument(
+      <ClockFace time={3} minutes={false} onClick={onClick} timePeriod="A" />
+    );
+    face.setState({ radius: 136 });
+
+    const faceNode = findDOMNode(face);
+    // Click at 12 o'clock
+    Simulate.mouseUp(faceNode, { button: LEFT_MOUSE, offsetX: 136, offsetY: 15 });
+    expect(onClick.mock.calls.length).toBe(1);
+    expect(onClick.mock.calls[0][0]).toBe(0);
+
+    // Click at 3 o'clock
+    Simulate.mouseUp(faceNode, { button: LEFT_MOUSE, offsetX: 272, offsetY: 136 });
+    expect(onClick.mock.calls.length).toBe(2);
+    expect(onClick.mock.calls[1][0]).toBe(3);
+  });
+
+  it('calls the onClick prop with the selected time on mousemove until the mouse up event', () => {
+    const onClick = jest.fn();
+    const face = renderIntoDocument(
+      <ClockFace time={3} minutes={false} onClick={onClick} timePeriod="A" />
+    );
+    face.setState({ radius: 136 });
+
+    const faceNode = findDOMNode(face);
+
+    Simulate.mouseMove(faceNode, { offsetX: 136, offsetY: 15 });
+    expect(onClick.mock.calls.length).toBe(0);
+
+    Simulate.mouseDown(faceNode, { button: LEFT_MOUSE });
+    Simulate.mouseMove(faceNode, { offsetX: 136, offsetY: 15 });
+    expect(onClick.mock.calls.length).toBe(1);
+
+    Simulate.mouseMove(faceNode, { offsetX: 120, offsetY: 20 });
+    expect(onClick.mock.calls.length).toBe(2);
+
+    Simulate.mouseUp(faceNode, { button: LEFT_MOUSE, offsetX: 125, offsetY: 25 });
+    expect(onClick.mock.calls.length).toBe(3);
+
+    Simulate.mouseMove(faceNode, { offsetX: 130, offsetY: 30 });
+    expect(onClick.mock.calls.length).toBe(3);
+  });
+
+  it('does not start listening to mousemove events if the control key was pressed on mousedown', () => {
+    const onClick = jest.fn();
+    const face = renderIntoDocument(
+      <ClockFace time={3} minutes={false} onClick={onClick} timePeriod="A" />
+    );
+    face.setState({ radius: 136 });
+
+    const faceNode = findDOMNode(face);
+
+    Simulate.mouseDown(faceNode, { button: LEFT_MOUSE, ctrlKey: true });
+    Simulate.mouseMove(faceNode, { offsetX: 136, offsetY: 15 });
+    expect(onClick.mock.calls.length).toBe(0);
+  });
+
+  it('does not start listening to mousemove events if the right mouse key was pressed on mousedown', () => {
+    const onClick = jest.fn();
+    const face = renderIntoDocument(
+      <ClockFace time={3} minutes={false} onClick={onClick} timePeriod="A" />
+    );
+    face.setState({ radius: 136 });
+
+    const faceNode = findDOMNode(face);
+
+    Simulate.mouseDown(faceNode, { button: RIGHT_MOUSE });
+    Simulate.mouseMove(faceNode, { offsetX: 136, offsetY: 15 });
+    expect(onClick.mock.calls.length).toBe(0);
+  });
+
+  it('calls the onClick prop with the selected time on touchmove until the touchend event', () => {
+    const onClick = jest.fn();
+    const face = renderIntoDocument(
+      <ClockFace time={3} minutes={false} onClick={onClick} timePeriod="A" />
+    );
+    face.setState({ radius: 136 });
+
+    const faceNode = findDOMNode(face);
+
+    Simulate.touchMove(faceNode, { offsetX: 136, offsetY: 15 });
+    expect(onClick.mock.calls.length).toBe(0);
+
+    Simulate.touchStart(faceNode);
+    Simulate.touchMove(faceNode, { offsetX: 136, offsetY: 15 });
+    expect(onClick.mock.calls.length).toBe(1);
+
+    Simulate.touchMove(faceNode, { offsetX: 120, offsetY: 20 });
+    expect(onClick.mock.calls.length).toBe(2);
+
+    Simulate.touchEnd(faceNode, { offsetX: 125, offsetY: 25 });
+    expect(onClick.mock.calls.length).toBe(3);
+
+    Simulate.touchMove(faceNode, { offsetX: 130, offsetY: 30 });
+    expect(onClick.mock.calls.length).toBe(3);
+  });
+});
diff --git a/src/js/Pickers/__tests__/ClockHand.js b/src/js/Pickers/__tests__/ClockHand.js
new file mode 100644
index 0000000000..30a04740a1
--- /dev/null
+++ b/src/js/Pickers/__tests__/ClockHand.js
@@ -0,0 +1,100 @@
+/*eslint-env jest*/
+jest.unmock('../ClockHand');
+
+import React from 'react';
+import { findDOMNode } from 'react-dom';
+import { renderIntoDocument } from 'react-addons-test-utils';
+
+import ClockHand from '../ClockHand';
+
+describe('ClockHand', () => {
+  it('positions the hand from the given coordinates', () => {
+    const hand = renderIntoDocument(
+      <ClockHand coords={136} time={1} minutes={false} />
+    );
+
+    const { left, top } = findDOMNode(hand).style;
+    expect(left).toBe('136px');
+    expect(top).toBe('136px');
+  });
+
+  it('adds the className inner-hour if the time is on a 24 hour clock', () => {
+    let hand = renderIntoDocument(
+      <ClockHand coords={136} time={1} minutes={false} />
+    );
+
+    let className = findDOMNode(hand).className;
+    expect(className).toContain('md-clock-hand');
+    expect(className).not.toContain('inner-hour');
+
+    hand = renderIntoDocument(
+      <ClockHand coords={136} time={13} minutes={false} />
+    );
+
+    className = findDOMNode(hand).className;
+    expect(className).toContain('md-clock-hand');
+    expect(className).toContain('inner-hour');
+
+    hand = renderIntoDocument(
+      <ClockHand coords={136} time={24} minutes={false} />
+    );
+
+    className = findDOMNode(hand).className;
+    expect(className).toContain('md-clock-hand');
+    expect(className).toContain('inner-hour');
+
+    hand = renderIntoDocument(
+      <ClockHand coords={136} time={1} minutes={false} />
+    );
+
+    className = findDOMNode(hand).className;
+    expect(className).toContain('md-clock-hand');
+    expect(className).not.toContain('inner-hour');
+  });
+
+  it('adds the className invisible-minute when the hand is on a minute that is not divisible by 5', () => {
+    let hand = renderIntoDocument(
+      <ClockHand coords={136} time={0} minutes={true} />
+    );
+
+    let className = findDOMNode(hand).className;
+    expect(className).toContain('md-clock-hand');
+    expect(className).not.toContain('invisible-minute');
+
+    hand = renderIntoDocument(
+      <ClockHand coords={136} time={2} minutes={true} />
+    );
+
+    className = findDOMNode(hand).className;
+    expect(className).toContain('md-clock-hand');
+    expect(className).toContain('invisible-minute');
+
+    hand = renderIntoDocument(
+      <ClockHand coords={136} time={59} minutes={true} />
+    );
+
+    className = findDOMNode(hand).className;
+    expect(className).toContain('md-clock-hand');
+    expect(className).toContain('invisible-minute');
+
+    hand = renderIntoDocument(
+      <ClockHand coords={136} time={30} minutes={true} />
+    );
+
+    className = findDOMNode(hand).className;
+    expect(className).toContain('md-clock-hand');
+    expect(className).not.toContain('invisible-minute');
+  });
+
+  it('sets the transform style to the correct degrees for a time', () => {
+    let hand = renderIntoDocument(<ClockHand coords={136} time={3} minutes={false} />);
+    let { transform } = findDOMNode(hand).style;
+
+    expect(transform).toBe('rotate3d(0, 0, 1, 0deg)');
+
+    hand = renderIntoDocument(<ClockHand coords={136} time={2} minutes={false} />);
+    transform = findDOMNode(hand).style.transform;
+
+    expect(transform).toBe('rotate3d(0, 0, 1, -30deg)');
+  });
+});
diff --git a/src/js/Pickers/__tests__/ClockTime.js b/src/js/Pickers/__tests__/ClockTime.js
new file mode 100644
index 0000000000..a5d1ce70dc
--- /dev/null
+++ b/src/js/Pickers/__tests__/ClockTime.js
@@ -0,0 +1,30 @@
+/*eslint-env jest*/
+jest.unmock('../ClockTime');
+
+import React from 'react';
+import { findDOMNode } from 'react-dom';
+import { renderIntoDocument } from 'react-addons-test-utils';
+
+import ClockTime from '../ClockTime';
+
+describe('ClockTime', () => {
+  it('displays the time', () => {
+    const time = renderIntoDocument(
+      <ClockTime time={1} index={0} active={false} radius={120} />
+    );
+
+    const timeNode = findDOMNode(time);
+    expect(timeNode.textContent).toBe('1');
+  });
+
+  it('sets top and left style properties on mount', () => {
+    const time = renderIntoDocument(
+      <ClockTime time={1} index={0} active={false} radius={120} />
+    );
+
+    expect(time.state.style).toBeDefined();
+    expect(time.state.style).not.toBe(null);
+    expect(time.state.style.top).toBeDefined();
+    expect(time.state.style.left).toBeDefined();
+  });
+});
diff --git a/src/js/Pickers/__tests__/TimePeriods.js b/src/js/Pickers/__tests__/TimePeriods.js
new file mode 100644
index 0000000000..3051da5d8c
--- /dev/null
+++ b/src/js/Pickers/__tests__/TimePeriods.js
@@ -0,0 +1,45 @@
+/*eslint-env jest*/
+jest.unmock('../TimePeriods');
+jest.unmock('../PickerControl');
+
+import React from 'react';
+import { findDOMNode } from 'react-dom';
+import {
+  Simulate,
+  renderIntoDocument,
+  scryRenderedComponentsWithType,
+} from 'react-addons-test-utils';
+
+import TimePeriods from '../TimePeriods';
+import PickerControl from '../PickerControl';
+
+describe('TimePeriods', () => {
+  it('displays a picker control component for AM and PM', () => {
+    const setTempTime = jest.fn();
+    const periods = renderIntoDocument(
+      <TimePeriods tempTime={new Date()} setTempTime={setTempTime} timePeriod="AM" />
+    );
+
+    const controls = scryRenderedComponentsWithType(periods, PickerControl);
+    expect(controls.length).toBe(2);
+  });
+
+  it('calls the setTempTime prop with 12 hours added when the AM control is clicked if the time period is not active', () => {
+    const props = {
+      setTempTime: jest.fn(),
+      tempTime: new Date(2016, 3, 5, 12),
+      timePeriod: 'PM',
+    };
+
+    let periods = renderIntoDocument(<TimePeriods {...props} />);
+    let am = findDOMNode(scryRenderedComponentsWithType(periods, PickerControl)[0]);
+    Simulate.click(am);
+    expect(props.setTempTime.mock.calls.length).toBe(1);
+
+    props.timePeriod = 'AM';
+    periods = renderIntoDocument(<TimePeriods {...props} />);
+    am = findDOMNode(scryRenderedComponentsWithType(periods, PickerControl)[0]);
+    Simulate.click(am);
+    expect(props.setTempTime.mock.calls.length).toBe(1);
+  });
+});
diff --git a/src/js/Pickers/__tests__/TimePicker.js b/src/js/Pickers/__tests__/TimePicker.js
new file mode 100644
index 0000000000..f7134fd28c
--- /dev/null
+++ b/src/js/Pickers/__tests__/TimePicker.js
@@ -0,0 +1,142 @@
+/*eslint-env jest*/
+jest.unmock('../TimePicker');
+
+import React from 'react';
+import {
+  renderIntoDocument,
+  findRenderedComponentWithType,
+} from 'react-addons-test-utils';
+
+import TimePicker from '../TimePicker';
+import DateTimeFormat from '../__mocks__/DateTimeFormat';
+import TimePickerHeader from '../TimePickerHeader';
+import PickerFooter from '../PickerFooter';
+import ClockFace from '../ClockFace';
+
+const commonProps = {
+  className: 'a',
+  okLabel: 'Ok',
+  okPrimary: false,
+  onOkClick: jest.fn(),
+  cancelLabel: 'Cancel',
+  cancelPrimary: false,
+  onCancelClick: jest.fn(),
+  DateTimeFormat,
+  locales: 'en-US',
+  setTimeMode: jest.fn(),
+  setTempTime: jest.fn(),
+  timeMode: 'hour',
+  tempTime: new Date(2016, 3, 15, 3, 55),
+  hours: '3',
+  minutes: ':55',
+  timePeriod: 'AM',
+};
+
+describe('TimePicker', () => {
+  it('renders the TimePickerHeader component with the correct props', () => {
+    const props = Object.assign({}, commonProps);
+    const picker = renderIntoDocument(<TimePicker {...props} />);
+    const header = findRenderedComponentWithType(picker, TimePickerHeader);
+
+    expect(header.props.tempTime).toEqual(props.tempTime);
+    expect(header.props.timeMode).toBe(props.timeMode);
+    expect(header.props.setTimeMode).toBe(props.setTimeMode);
+    expect(header.props.setTempTime).toBe(props.setTempTime);
+    expect(header.props.hours).toBe(props.hours);
+    expect(header.props.minutes).toBe(props.minutes);
+    expect(header.props.timePeriod).toBe(props.timePeriod);
+  });
+
+  it('renders the PickerFooter component with the correct props', () => {
+    const props = Object.assign({}, commonProps);
+    const picker = renderIntoDocument(<TimePicker {...props} />);
+    const footer = findRenderedComponentWithType(picker, PickerFooter);
+
+    expect(footer.props.okLabel).toEqual(props.okLabel);
+    expect(footer.props.okPrimary).toBe(props.okPrimary);
+    expect(footer.props.onOkClick).toBe(props.onOkClick);
+    expect(footer.props.onOkClick).toBe(props.onOkClick);
+    expect(footer.props.cancelLabel).toBe(props.cancelLabel);
+    expect(footer.props.cancelPrimary).toBe(props.cancelPrimary);
+    expect(footer.props.onCancelClick).toBe(props.onCancelClick);
+  });
+
+  it('renders the ClockFace component with the correct props depending on the timeMode', () => {
+    let props = Object.assign({}, commonProps);
+    let picker = renderIntoDocument(<TimePicker {...props} />);
+    let face = findRenderedComponentWithType(picker, ClockFace);
+
+    expect(face.props.time).toBe(3);
+    expect(face.props.minutes).toBe(false);
+    expect(face.props.onClick).toBe(picker.updateTime);
+    expect(face.props.timePeriod).toBe(props.timePeriod);
+
+    props = Object.assign({}, commonProps, { timeMode: 'minute' });
+    picker = renderIntoDocument(<TimePicker {...props} />);
+    face = findRenderedComponentWithType(picker, ClockFace);
+
+    expect(face.props.time).toBe(55);
+    expect(face.props.minutes).toBe(true);
+    expect(face.props.onClick).toBe(picker.updateTime);
+    expect(face.props.timePeriod).toBe(props.timePeriod);
+  });
+
+  it('calls the setTempTime function with a new updated date object of the new time', () => {
+    const props = Object.assign({}, commonProps, { setTempTime: jest.fn() });
+    let picker = renderIntoDocument(<TimePicker {...props} />);
+
+    picker.updateTime(11);
+    expect(props.setTempTime.mock.calls.length).toBe(1);
+    expect(props.setTempTime.mock.calls[0][0]).toEqual(new Date(2016, 3, 15, 11, 55));
+
+    props.timeMode = 'minute';
+    picker = renderIntoDocument(<TimePicker {...props} />);
+
+    picker.updateTime(32);
+    expect(props.setTempTime.mock.calls.length).toBe(2);
+    expect(props.setTempTime.mock.calls[1][0]).toEqual(new Date(2016, 3, 15, 3, 32));
+  });
+
+  it('calls the setTempTime function with the correct hour to adjust for 12 hour clocks', () => {
+    const props = Object.assign({}, commonProps, {
+      setTempTime: jest.fn(),
+      // 3 PM
+      tempTime: new Date(2016, 3, 15, 15, 55),
+      timePeriod: 'PM',
+    });
+    let picker = renderIntoDocument(<TimePicker {...props} />);
+
+    picker.updateTime(12);
+    expect(props.setTempTime.mock.calls.length).toBe(1);
+    expect(props.setTempTime.mock.calls[0][0]).toEqual(new Date(2016, 3, 15, 12, 55));
+
+    picker.updateTime(5);
+    expect(props.setTempTime.mock.calls.length).toBe(2);
+    expect(props.setTempTime.mock.calls[1][0]).toEqual(new Date(2016, 3, 15, 17, 55));
+
+    props.timePeriod = 'AM';
+    props.tempTime = new Date(2016, 3, 15, 3, 55);
+    picker = renderIntoDocument(<TimePicker {...props} />);
+
+    picker.updateTime(12);
+    expect(props.setTempTime.mock.calls.length).toBe(3);
+    expect(props.setTempTime.mock.calls[2][0]).toEqual(new Date(2016, 3, 15, 0, 55));
+
+    picker.updateTime(5);
+    expect(props.setTempTime.mock.calls.length).toBe(4);
+    expect(props.setTempTime.mock.calls[3][0]).toEqual(new Date(2016, 3, 15, 5, 55));
+  });
+
+  it('calls the setTempTime function with the correct hour for a 24 hour clock', () => {
+    const props = Object.assign({}, commonProps, {
+      setTempTime: jest.fn(),
+      tempTime: new Date(2016, 3, 15, 15, 55),
+    });
+    delete props.timePeriod;
+
+    let picker = renderIntoDocument(<TimePicker {...props} />);
+    picker.updateTime(22);
+    expect(props.setTempTime.mock.calls.length).toBe(1);
+    expect(props.setTempTime.mock.calls[0][0]).toEqual(new Date(2016, 3, 15, 22, 55));
+  });
+});
diff --git a/src/js/Pickers/__tests__/TimePickerContainer.js b/src/js/Pickers/__tests__/TimePickerContainer.js
new file mode 100644
index 0000000000..34c3af3c6c
--- /dev/null
+++ b/src/js/Pickers/__tests__/TimePickerContainer.js
@@ -0,0 +1,100 @@
+/*eslint-env jest*/
+jest.unmock('../TimePickerContainer');
+jest.unmock('../../utils/dates');
+
+
+import React from 'react';
+import { findDOMNode } from 'react-dom';
+import {
+  renderIntoDocument,
+  findRenderedComponentWithType,
+  scryRenderedComponentsWithType,
+} from 'react-addons-test-utils';
+
+import TimePickerContainer from '../TimePickerContainer';
+import TimePicker from '../TimePicker';
+import TransitionGroup from 'react-addons-transition-group';
+
+describe('TimePickerContainer', () => {
+  it('merges className and style', () => {
+    const style = { display: 'block' };
+    const className = 'test';
+    const pickerStyle = { background: 'red' };
+    const pickerClassName = 'picker-test';
+    const container = renderIntoDocument(
+      <TimePickerContainer
+        style={style}
+        className={className}
+        pickerStyle={pickerStyle}
+        pickerClassName={pickerClassName}
+        locales="en-US"
+        initiallyOpen={true}
+      />
+    );
+
+    const containerNode = findDOMNode(container);
+    const picker = findRenderedComponentWithType(container, TimePicker);
+    expect(containerNode.style.display).toBe(style.display);
+    expect(containerNode.className).toContain(className);
+
+    expect(picker.props.style).toEqual(pickerStyle);
+    expect(picker.props.className).toContain(pickerClassName);
+  });
+
+  it('renders a transition group if displayed inline', () => {
+    let container = renderIntoDocument(
+      <TimePickerContainer inline={true} locales="en-US" />
+    );
+
+    let transitions = scryRenderedComponentsWithType(container, TransitionGroup);
+    expect(transitions.length).toBe(1);
+
+    container = renderIntoDocument(
+      <TimePickerContainer inline={false} locales="en-US" />
+    );
+
+    transitions = scryRenderedComponentsWithType(container, TransitionGroup);
+    expect(transitions.length).toBe(0);
+  });
+
+  it('passes the correct props to the TimePicker when open', () => {
+    let container = renderIntoDocument(<TimePickerContainer locales="en-US" />);
+    let pickers = scryRenderedComponentsWithType(container, TimePicker);
+    expect(pickers.length).toBe(0);
+
+    container = renderIntoDocument(<TimePickerContainer initiallyOpen={true} locales="en-US" />);
+    pickers = scryRenderedComponentsWithType(container, TimePicker);
+    expect(pickers.length).toBe(1);
+
+    const { props } = pickers[0];
+    expect(props.onOkClick).toBe(container.handleOkClick);
+    expect(props.okLabel).toBe(TimePickerContainer.defaultProps.okLabel);
+    expect(props.okPrimary).toBe(TimePickerContainer.defaultProps.okPrimary);
+    expect(props.onCancelClick).toBe(container.handleCancelClick);
+    expect(props.cancelLabel).toBe(TimePickerContainer.defaultProps.cancelLabel);
+    expect(props.cancelPrimary).toBe(TimePickerContainer.defaultProps.cancelPrimary);
+    expect(props.setTimeMode).toBe(container.setTimeMode);
+    expect(props.setTempTime).toBe(container.setTempTime);
+    expect(props.tempTime).toBe(container.state.tempTime);
+    expect(props.timeMode).toBe(container.state.timeMode);
+    expect(props.hours).toBe(container.state.hours);
+    expect(props.minutes).toBe(container.state.minutes);
+    expect(props.timePeriod).toBe(container.state.timePeriod);
+  });
+
+  it('can be a controlled component for the value', () => {
+    const props = {
+      onChange: jest.fn(),
+      value: new Date(2016, 3, 15, 3, 15),
+      locales: 'en-US',
+    };
+
+    const container = renderIntoDocument(<TimePickerContainer {...props} />);
+    const event = { button: 2 };
+    container.handleOkClick(event);
+    expect(props.onChange.mock.calls.length).toBe(1);
+    expect(props.onChange.mock.calls[0][0]).toBe('3:15 AM');
+    expect(props.onChange.mock.calls[0][1]).toEqual(new Date(2016, 3, 15, 3, 15));
+    expect(props.onChange.mock.calls[0][2]).toEqual(event);
+  });
+});
diff --git a/src/js/Pickers/__tests__/TimePickerHeader.js b/src/js/Pickers/__tests__/TimePickerHeader.js
new file mode 100644
index 0000000000..44defeed4d
--- /dev/null
+++ b/src/js/Pickers/__tests__/TimePickerHeader.js
@@ -0,0 +1,104 @@
+/*eslint-env jest*/
+jest.unmock('../TimePickerHeader');
+jest.unmock('../PickerControl');
+
+import React from 'react';
+import { findDOMNode } from 'react-dom';
+import {
+  Simulate,
+  renderIntoDocument,
+  scryRenderedComponentsWithType,
+} from 'react-addons-test-utils';
+
+import TimePickerHeader from '../TimePickerHeader';
+import PickerControl from '../PickerControl';
+import TimePeriods from '../TimePeriods';
+
+describe('TimePickerHeader', () => {
+  it('renders a picker control for the hours', () => {
+    const props = {
+      tempTime: new Date(2016, 3, 5, 5, 15),
+      timeMode: 'hour',
+      setTimeMode: jest.fn(),
+      setTempTime: jest.fn(),
+      hours: '5',
+      minutes: ':15',
+      timePeriod: 'AM',
+    };
+
+    const header = renderIntoDocument(<TimePickerHeader {...props} />);
+    const hour = scryRenderedComponentsWithType(header, PickerControl)[0];
+    const hourNode = findDOMNode(hour);
+
+    expect(hour.props.active).toBe(true);
+    expect(hourNode.textContent).toBe(props.hours);
+  });
+
+  it('renders a picker control for the minutes', () => {
+    const props = {
+      tempTime: new Date(2016, 3, 5, 5, 15),
+      timeMode: 'hour',
+      setTimeMode: jest.fn(),
+      setTempTime: jest.fn(),
+      hours: '5',
+      minutes: ':15',
+      timePeriod: 'AM',
+    };
+
+    const header = renderIntoDocument(<TimePickerHeader {...props} />);
+    const minute = scryRenderedComponentsWithType(header, PickerControl)[1];
+    const minuteNode = findDOMNode(minute);
+
+    expect(minute.props.active).toBe(false);
+    expect(minuteNode.textContent).toBe(props.minutes);
+  });
+
+  it('renders the TimePeriods component if there is a time period', () => {
+    const props = {
+      tempTime: new Date(2016, 3, 5, 5, 15),
+      timeMode: 'hour',
+      setTimeMode: jest.fn(),
+      setTempTime: jest.fn(),
+      hours: '5',
+      minutes: ':15',
+      timePeriod: 'AM',
+    };
+
+    let header = renderIntoDocument(<TimePickerHeader {...props} />);
+    let periods = scryRenderedComponentsWithType(header, TimePeriods);
+
+    expect(periods.length).toBe(1);
+    expect(periods[0].props.tempTime).toBe(props.tempTime);
+    expect(periods[0].props.setTempTime).toBe(props.setTempTime);
+    expect(periods[0].props.timePeriod).toBe(props.timePeriod);
+
+    delete props.timePeriod;
+    header = renderIntoDocument(<TimePickerHeader {...props} />);
+    periods = scryRenderedComponentsWithType(header, TimePeriods);
+
+    expect(periods.length).toBe(0);
+  });
+
+  it('calls the setTimeMode function when a picker control is clicked', () => {
+    const props = {
+      tempTime: new Date(2016, 3, 5, 5, 15),
+      timeMode: 'hour',
+      setTimeMode: jest.fn(),
+      setTempTime: jest.fn(),
+      hours: '5',
+      minutes: ':15',
+      timePeriod: 'AM',
+    };
+
+    const header = renderIntoDocument(<TimePickerHeader {...props} />);
+    let periods = scryRenderedComponentsWithType(header, PickerControl);
+    let am = findDOMNode(periods[0]);
+    let pm = findDOMNode(periods[1]);
+
+    Simulate.click(am);
+    expect(props.setTimeMode.mock.calls.length).toBe(1);
+
+    Simulate.click(pm);
+    expect(props.setTimeMode.mock.calls.length).toBe(2);
+  });
+});
diff --git a/src/js/utils/__tests__/index.js b/src/js/utils/__tests__/index.js
new file mode 100644
index 0000000000..b40069267d
--- /dev/null
+++ b/src/js/utils/__tests__/index.js
@@ -0,0 +1,16 @@
+ /*eslint-env jest*/
+jest.unmock('../index');
+
+import { isPointInCircle } from '../index';
+
+describe('isPointInCircle', () => {
+  it('returns true if an x and y coordinate are in a circle', () => {
+    // 4x4 circle
+    expect(isPointInCircle(2, 2, 2, 2, 2)).toBe(true);
+  });
+
+  it('returns false if an x and y coordinate are outside of a circle', () => {
+    // 4x4 circle
+    expect(isPointInCircle(2, 2, 2, 0, 0)).toBe(false);
+  });
+});