Repository: mlaursen/react-md
Pull request #112 (2016-06-17 17:32:55)

Release/0.3.x
Merging changes for 0.3.0 and 0.3.1 into master.


Patch:
diff --git a/.babelrc b/.babelrc
index 1b0e85a216..14cbd199cf 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,5 +1,12 @@
 {
+  "env": {
+    "build": {
+      "plugins": ["transform-es2015-modules-umd"]
+    },
+    "test": {
+      "plugins": ["add-module-exports"]
+    }
+  },
   "presets": ["es2015", "react", "stage-0"],
-  "plugins": ["add-module-exports"],
   "sourceMaps": true
 }
diff --git a/package.json b/package.json
index b8af1cb159..852268f2e0 100644
--- a/package.json
+++ b/package.json
@@ -1,13 +1,13 @@
 {
   "name": "react-md",
-  "version": "0.2.1",
+  "version": "0.3.1",
   "description": "React material design components",
   "main": "./lib",
   "scripts": {
-    "test": "jest",
-    "test:watch": "jest --watch",
-    "scripts": "babel --ignore=__tests__,__mocks__,beforeTest.js ./src/js --out-dir=./lib",
-    "scripts:watch": "babel --ignore=__tests__,__mocks__,beforeTest.js ./src/js --watch --out-dir=./lib",
+    "test": "BABEL_ENV=test jest",
+    "test:watch": "BABEL_ENV=test jest --watch",
+    "scripts": "BABEL_ENV=build babel --ignore=__tests__,__mocks__,beforeTest.js ./src/js --out-dir=./lib",
+    "scripts:watch": "BABEL_ENV=build babel --ignore=__tests__,__mocks__,beforeTest.js ./src/js --watch --out-dir=./lib",
     "styles": "sass --style expanded --line ./src/scss/react-md.scss ./lib/react-md.css && sass --style compact ./src/scss/react-md.scss ./lib/react-md.min.css",
     "prebuild": "npm test && rimraf lib",
     "lint:styles": "scss-lint src/scss",
@@ -40,6 +40,7 @@
     "babel-eslint": "^6.0.2",
     "babel-jest": "^12.0.2",
     "babel-plugin-add-module-exports": "^0.1.1",
+    "babel-plugin-transform-es2015-modules-umd": "^6.8.0",
     "babel-polyfill": "^6.3.14",
     "babel-preset-es2015": "^6.1.18",
     "babel-preset-react": "^6.1.18",
diff --git a/src/js/FileInputs/FileInput.js b/src/js/FileInputs/FileInput.js
new file mode 100644
index 0000000000..3e04a860ae
--- /dev/null
+++ b/src/js/FileInputs/FileInput.js
@@ -0,0 +1,159 @@
+import React, { Component, PropTypes } from 'react';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import classnames from 'classnames';
+
+import injectInk from '../Inks';
+import FontIcon from '../FontIcons';
+
+/**
+ * The `FileInput` component is used as simple styling for the `<input type="file" />`.
+ * It will style the input as a raised button by default.
+ */
+class FileInput extends Component {
+  constructor(props) {
+    super(props);
+
+    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
+  }
+
+  static propTypes = {
+    /**
+     * An optional style to apply.
+     */
+    style: PropTypes.object,
+
+    /**
+     * An optional className to apply.
+     */
+    className: PropTypes.string,
+
+    /**
+     * Boolean if the `FileInput` should be styled with the primary color.
+     */
+    primary: PropTypes.bool,
+
+    /**
+     * Boolean if the `FileInput` should be styled with the secondary color.
+     */
+    secondary: PropTypes.bool,
+
+    /**
+     * Boolean if the `FileInput` should be styled as a flat button instead of a
+     * raised button.
+     */
+    flat: PropTypes.bool,
+
+    /**
+     * This should be a comma separated list of Media Types that the `FileInput` can
+     * accept. If this prop is left blank, any file will be accepted.
+     *
+     * The values can either be:
+     * - A file extension
+     * - audio/*
+     * - video/*
+     * - image/*
+     * - any valid [IANA Media Type](http://www.iana.org/assignments/media-types/media-types.xhtml)
+     */
+    accept: PropTypes.string,
+
+    /**
+     * Boolean if multiple files will be accepted.
+     */
+    multiple: PropTypes.bool,
+
+    /**
+     * A label to display on the `FileInput` when no files have been selected.
+     */
+    label: PropTypes.string.isRequired,
+
+    /**
+     * The icon children to use for the upload icon.
+     */
+    iconChildren: PropTypes.node,
+
+    /**
+     * The icon className to use for the upload icon.
+     */
+    iconClassName: PropTypes.string,
+
+    /**
+     * A function to call when the value of the input changes. This will
+     * be triggered when the user selects a new file or cancels the new file selection.
+     *
+     * This function will be given the new [FileList](https://developer.mozilla.org/en-US/docs/Web/API/FileList)
+     * as an array and the change event. If this is not a multiple file input, only the newly selected File
+     * will be given instead of a list of one file. Since this is an `input` tag,
+     * the user will not be able to select the same file multiple times unless
+     * you manually clear the input's value.
+     *
+     * > NOTE: If the user hits cancel, null will be given for a single file input.
+     *
+     * ```js
+     * onChange(files, e);
+     * ```
+     */
+    onChange: PropTypes.func.isRequired,
+
+    /**
+     * Injected from `injectInk`
+     */
+    ink: PropTypes.node,
+  };
+
+  static defaultProps = {
+    label: 'Select a file from your computer',
+    iconChildren: 'file_upload',
+  };
+
+  _handleChange = (e) => {
+    const { multiple, onChange } = this.props;
+    const { files } = e.target;
+    if(!multiple) {
+      onChange(files[0] || null, e);
+    } else {
+      onChange(Array.prototype.slice.call(files), e);
+    }
+  };
+
+  render() {
+    const {
+      style,
+      className,
+      label,
+      iconChildren,
+      iconClassName,
+      primary,
+      secondary,
+      flat,
+      ink,
+      ...props,
+    } = this.props;
+    delete props.onChange;
+
+    return (
+      <label
+        style={style}
+        className={classnames(`md-btn md-${flat ? 'flat' : 'raised'}-btn md-file-input-btn`, className, {
+          'md-primary': primary,
+          'md-secondary': secondary,
+        })}
+        disabled={props.disabled}
+      >
+        {ink}
+        <input
+          {...props}
+          aria-hidden="true"
+          type="file"
+          className="md-file-input"
+          onChange={this._handleChange}
+        />
+        <div className="icon-separator">
+          <FontIcon iconClassName={iconClassName} children={iconChildren} />
+          <span className="text">{label}</span>
+        </div>
+      </label>
+    );
+  }
+}
+
+export default injectInk(FileInput);
diff --git a/src/js/FileInputs/FileUpload.js b/src/js/FileInputs/FileUpload.js
new file mode 100644
index 0000000000..04ff3e0752
--- /dev/null
+++ b/src/js/FileInputs/FileUpload.js
@@ -0,0 +1,339 @@
+import React, { Component, PropTypes } from 'react';
+import { findDOMNode } from 'react-dom';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import FileInput from './FileInput';
+
+/**
+ * The `FileUpload` component is used to upload files locally. If you want
+ * to upload files to a server, use [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData)
+ * by attaching the `File`.
+ *
+ * Quick example:
+ *
+ * ```js
+ * function upload(file) {
+ *   fetch('/api/images', {
+ *     method: 'POST',
+ *     body: new FormData().append('file', file),
+ *   });
+ * }
+ * ```
+ *
+ * An upload can be aborted by calling the `abort(file || fileName)` function. If
+ * the file or fileName are ommitted, it will *attempt* to abort the current
+ * file that is uploading. Unreliable for multiselect.
+ *
+ * ```js
+ * <FileUpload ref="upload" />
+ * <RaisedButton onClick={() => this.refs.upload.abort()} label="Abort! Abort!" />
+ * ```
+ */
+export default class FileUpload extends Component {
+  constructor(props) {
+    super(props);
+
+    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
+    this.state = {};
+  }
+
+  static propTypes = {
+    /**
+     * An optional style to apply.
+     */
+    style: PropTypes.object,
+
+    /**
+     * An optional className to apply.
+     */
+    className: PropTypes.string,
+
+    /**
+     * Boolean if the `FileInput` should be styled with the primary color.
+     */
+    primary: PropTypes.bool,
+
+    /**
+     * Boolean if the `FileInput` should be styled with the secondary color.
+     */
+    secondary: PropTypes.bool,
+
+    /**
+     * Boolean if the `FileInput` should be styled as a flat button instead of a
+     * raised button.
+     */
+    flat: PropTypes.bool,
+
+    /**
+     * This should be a comma separated list of Media Types that the `FileInput` can
+     * accept. If this prop is left blank, any file will be accepted.
+     *
+     * The values can either be:
+     * - A file extension
+     * - audio/*
+     * - video/*
+     * - image/*
+     * - any valid [IANA Media Type](http://www.iana.org/assignments/media-types/media-types.xhtml)
+     */
+    accept: PropTypes.string,
+
+    /**
+     * Boolean if multiple files will be accepted.
+     */
+    multiple: PropTypes.bool,
+
+    /**
+     * A label to display on the `FileInput`.
+     */
+    label: PropTypes.string,
+
+    /**
+     * The icon children to use for the upload icon.
+     */
+    iconChildren: PropTypes.node,
+
+    /**
+     * The icon className to use for the upload icon.
+     */
+    iconClassName: PropTypes.string,
+
+    /**
+     * An optional max size for the file. If the file is greater than
+     * this limit, the file will not be uploaded.
+     */
+    maxSize: PropTypes.number,
+
+    /**
+     * A required function to call when the `maxSize` prop is set. It will
+     * be given a list of files that were too big.
+     */
+    onSizeError: (props, propName, component) => {
+      if(typeof props.maxSize === 'number') {
+        return PropTypes.func.isRequired(props, propName, component);
+      }
+    },
+
+    /**
+     * You can force the `FileReader` to read the file as a specific type
+     * if you do not trust the *amazing* regex I have for choosing the correct
+     * one.
+     *
+     * ```js
+     * if(type.match(/image|video|audio/)) {
+     *   fr.readAsDataURL(file);
+     * } else if(type.match(/application|model|multipart/)) {
+     *   fr.readAsArrayBuffer(file);
+     * } else {
+     *   fr.readAsText(file);
+     * }
+     * ```
+     *
+     * > `.yml` and `.js` both are considered `application`, so it definitely fails there.
+     *
+     * If this prop is a function, you will be given the file's type, the file object, and
+     * the file reader. You will then need to call `fileReader.readAsYOUR_CORRECT_TYPE(file)`.
+     */
+    readAs: PropTypes.oneOfType([
+      PropTypes.oneOf(['DataURL', 'ArrayBuffer', 'Text']),
+      PropTypes.func,
+    ]),
+
+    /**
+     * An optional function to call when the `FileUpload` aborts. The current
+     * file and the abort event are given. This might not be the most useful
+     * function to use since you will need to manually call abort yourself anyways.
+     */
+    onAbort: PropTypes.func,
+
+    /**
+     * An optional function to call when the `FileUpload` errors. The current
+     * file, the error, and the error event are given.
+     *
+     * ```js
+     * onError(file, event.target.error, event);
+     * ```
+     */
+    onError: PropTypes.func,
+
+    /**
+     * An optional function to call when the `FileUpload` loads. The current
+     * file, the load result, and the load event are given.
+     *
+     * ```js
+     * onLoad(file, event.target.result, event);
+     * ```
+     *
+     * The load result will either be:
+     * - a data url
+     * - a plain text string
+     * - an array buffer
+     *
+     * depending on what type the file is.
+     */
+    onLoad: PropTypes.func,
+
+    /**
+     * An optional function to call when the `FileUpload` starts loading. The current
+     * file and the load start event are given.
+     */
+    onLoadStart: PropTypes.func,
+
+    /**
+     * An optional function to call when the `FileUpload` finishes loading. The
+     * current file and the load end event are given.
+     */
+    onLoadEnd: PropTypes.func,
+
+    /**
+     * An optional function to call when the `FileUpload` progress. The current
+     * file, upload progress, and the progress event are given. The progress
+     * will be a number between 0 and 100 that has not been rounded.
+     *
+     * ```js
+     * onProgress(file, progress, event);
+     * ```
+     */
+    onProgress: PropTypes.func,
+
+    /**
+     * An optional function to call when a file selects or unselects a file.
+     * This will be called before any local uploading occurs.
+     */
+    onChange: PropTypes.func,
+  };
+
+  /**
+   * Aborts the upload of a file. This either takes a file or a fileName and calls the
+   * `abort` function for that file's `FileReader`.
+   *
+   * @param {Object|String} file? The file or the file name to use to find the correct `FileReader`.
+   */
+  abort = (file) => {
+    let fileName = file;
+    if(!file) {
+      // Attempt to remove first file added...
+      fileName = Object.keys(this.state)[0];
+    } else if(typeof file.name === 'string') {
+      fileName = file.name;
+    }
+
+    const reader = this.state[fileName];
+    if(reader) {
+      reader.abort();
+      const state = this.state;
+      delete state[fileName];
+
+      findDOMNode(this).querySelector('.md-file-input').value = '';
+
+      this.setState(state);
+    }
+  };
+
+  _uploadFile = (file) => {
+    const {
+      onAbort,
+      onError,
+      onLoad,
+      onLoadStart,
+      onLoadEnd,
+      onProgress,
+      readAs,
+    } = this.props;
+
+    const { name, type } = file;
+
+    const fr = new FileReader();
+    if(onError) {
+      fr.onerror = e => {
+        onError(file, e.target.error, e);
+      };
+    }
+
+    if(onAbort) {
+      fr.onabort = e => {
+        onAbort(file, e);
+      };
+    }
+
+    if(onLoadStart) {
+      fr.onloadstart = e => {
+        onLoadStart(file, e);
+      };
+    }
+
+    if(onLoadEnd) {
+      fr.onloadend = e => {
+        onLoadEnd(file, e);
+      };
+    }
+
+    fr.onload = e => {
+      if(onLoad) {
+        onLoad(file, e.target.result, e);
+      }
+
+      const state = Object.assign({}, this.state);
+      delete state[name];
+      this.setState(state);
+    };
+
+    if(onProgress) {
+      fr.onprogress = e => {
+        if(e.lengthComputable) {
+          onProgress(file, (e.loaded / e.total) * 100, e);
+        }
+      };
+    }
+
+    if(readAs) {
+      if(typeof readAs === 'function') {
+        readAs(file.type, file, fr);
+      } else {
+        fr['readAs' + readAs](file);
+      }
+    } else if(type.match(/image|video|audio/)) {
+      fr.readAsDataURL(file);
+    } else if(type.match(/application|model|multipart/)) {
+      fr.readAsArrayBuffer(file);
+    } else {
+      fr.readAsText(file);
+    }
+
+    return fr;
+  };
+
+  _handleUpload = (files, e) => {
+    this.props.onChange && this.props.onChange(files, e);
+
+    if(!files) { return; }
+    const { maxSize, onSizeError } = this.props;
+    files = Array.isArray(files) ? files : [files];
+
+    let errorFiles = [];
+    if(maxSize) {
+      errorFiles = files.filter(file => file.size > maxSize);
+      files = files.filter(file => file.size <= maxSize);
+    }
+
+    if(errorFiles.length) {
+      onSizeError(errorFiles);
+    }
+
+    if(!files.length) {
+      return;
+    }
+
+    const nextState = {};
+    files.forEach(file => {
+      const fileReader = this._uploadFile(file);
+      nextState[file.name] = fileReader;
+    });
+
+    this.setState(nextState);
+  };
+
+  render() {
+    return (
+      <FileInput {...this.props} onChange={this._handleUpload} />
+    );
+  }
+}
diff --git a/src/js/FileInputs/index.js b/src/js/FileInputs/index.js
new file mode 100644
index 0000000000..82ddae34d6
--- /dev/null
+++ b/src/js/FileInputs/index.js
@@ -0,0 +1,7 @@
+import FileInput from './FileInput';
+export default FileInput;
+
+import FileUpload from './FileUpload';
+
+export { FileInput };
+export { FileUpload };
diff --git a/src/js/NavigationDrawers/NavigationDrawer.js b/src/js/NavigationDrawers/NavigationDrawer.js
index aab97bafb2..a49964b729 100644
--- a/src/js/NavigationDrawers/NavigationDrawer.js
+++ b/src/js/NavigationDrawers/NavigationDrawer.js
@@ -1,49 +1,35 @@
 import React, { Component, PropTypes } from 'react';
 import PureRenderMixin from 'react-addons-pure-render-mixin';
+import CSSTransitionGroup from 'react-addons-css-transition-group';
 import classnames from 'classnames';
 
-import { setOverflow, isTouchDevice } from '../utils';
-import Divider from '../Dividers';
-import { List, ListItem } from '../Lists';
-import Subheader from '../Subheaders';
+import Drawer from './Drawer';
+import DrawerToolbar from './DrawerToolbar';
 import Overlay from '../Transitions/Overlay';
 
-import NavigationDrawerHeader from './NavigationDrawerHeader';
-import NavigationDrawerToolbar from './NavigationDrawerToolbar';
-
-const deprecate = (props, propName, component, to) => {
-  if(typeof props[propName] !== 'undefined') {
-    console.error(
-      `The prop '${propName}' has been renamed to '${to}' ` +
-      'for the next release. Check the upgrade docs at ' +
-      'https://github.com/mlaursen/react-md/tree/master/docs/UpgradeGuide.md ' +
-      'for more information.'
-    );
-  }
-};
-
-const removed = (props, propName) => {
-  if(typeof props[propName] !== 'undefined') {
-    console.error(
-      `The prop '${propName}' has been removed for the next release. Check ` +
-      'the upgrade docs at ' +
-      'https://github.com/mlaursen/react-md/tree/master/docs/UpgradeGuide.md ' +
-      'for more information.'
-    );
-  }
-};
-
 /**
- * Navigation drawers are an excellent component to use to set up the initial
- * layout of your application. This component combines a Navigation drawer
+ * Navigation Drawers are an excellent component to use to set up the initial
+ * layout of your application. This component combines a Drawer
  * (a sidebar of nav items), an app bar, and displays any additional content.
+ *
+ * The `NavigationDrawer` component is customizable to have different
+ * display states for mobile, tablet, and desktop displays.
  */
 export default class NavigationDrawer extends Component {
   constructor(props) {
     super(props);
 
     this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
-    this.state = { touch: false };
+    const { initiallyOpen, initialDrawerType } = props;
+    this.state = {
+      isOpen: typeof initiallyOpen !== 'undefined'
+        ? initiallyOpen
+        : initialDrawerType !== 'mobile',
+      mobile: initialDrawerType === 'mobile',
+      tablet: initialDrawerType === 'tablet',
+      desktop: initialDrawerType === 'desktop',
+      drawerType: props[initialDrawerType + 'DrawerType'],
+    };
   }
 
   static DrawerType = {
@@ -59,36 +45,24 @@ export default class NavigationDrawer extends Component {
 
   static propTypes = {
     /**
-     * An optional style to apply to the navigation drawer itself.
+     * An optional style to apply to the entire container.
      */
-    style: (props, propName, component) => {
-      deprecate(props, propName, component, 'drawerStyle');
-      return PropTypes.object(props, propName, component);
-    },
+    style: PropTypes.object,
 
     /**
-     * An optional className to apply to the navigation drawer iteself.
+     * An optional className to apply to the entire container.
      */
-    className: (props, propName, component) => {
-      deprecate(props, propName, component, 'drawerClassName');
-      return PropTypes.string(props, propName, component);
-    },
+    className: PropTypes.string,
 
     /**
-     * An optional style to apply to the navigation drawer container.
+     * An optional style to apply to the drawer.
      */
-    containerStyle: (props, propName, component) => {
-      deprecate(props, propName, component, 'style');
-      return PropTypes.object(props, propName, component);
-    },
+    drawerStyle: PropTypes.object,
 
     /**
-     * An optional className to apply to the navigation drawer container.
+     * An optional className to apply to the drawer.
      */
-    containerClassName: (props, propName, component) => {
-      deprecate(props, propName, component, 'className');
-      return PropTypes.string(props, propName, component);
-    },
+    drawerClassName: PropTypes.string,
 
     /**
      * An optional style to apply to the main content.
@@ -111,26 +85,97 @@ export default class NavigationDrawer extends Component {
     toolbarClassName: PropTypes.string,
 
     /**
-     * Boolean if the navigation drawer is currently open.
+     * An optional title to display in the navigation drawer header.
+     */
+    drawerTitle: PropTypes.string,
+
+    /**
+     * Any additional children you want to display in the navigation drawer header after
+     * the optional title.
      */
-    isOpen: (props, propName, component) => {
-      removed(props, propName);
-      return PropTypes.bool.isRequired(props, propName, component);
-    },
+    drawerChildren: PropTypes.node,
 
     /**
-     * An optional title to display in the navigation drawer header.
+     * An optional title to display in the toolbar.
      */
-    title: (props, propName, component) => {
-      deprecate(props, propName, component, 'drawerTitle');
-      return PropTypes.string(props, propName, component);
-    },
+    toolbarTitle: PropTypes.string,
+
+    /**
+     * Any additional children you want to display in the main toolbar after the menu
+     * button (for persistent drawers) and the optional title.
+     */
+    toolbarChildren: PropTypes.node,
 
     /**
      * The main content to display.
      */
     children: PropTypes.node,
 
+    /**
+     * An optional boolean if the drawer is open for persistent or temporary
+     * drawers.
+     */
+    initiallyOpen: PropTypes.bool,
+
+    /**
+     * The initial drawer type to render. This is used for Server Side Rendering.
+     */
+    initialDrawerType: PropTypes.oneOf(['mobile', 'tablet', 'desktop']).isRequired,
+
+    /**
+     * The `DrawerType` to use for mobile devices. If the `mobileMinWidth` prop
+     * matches or the `tabletMinWidth` prop matches and the orientation is portrait,
+     * this drawer type will be used.
+     */
+    mobileDrawerType: PropTypes.oneOf([
+      NavigationDrawer.DrawerType.TEMPORARY,
+      NavigationDrawer.DrawerType.TEMPORARY_MINI,
+    ]).isRequired,
+
+    /**
+     * The `DrawerType` to use for landscape oriented tablets. If the
+     * `tabletMinWidth` prop matches and the orientation is landscape, this drawer
+     * type will be used.
+     */
+    tabletDrawerType: PropTypes.oneOf([
+      NavigationDrawer.DrawerType.FULL_HEIGHT,
+      NavigationDrawer.DrawerType.CLIPPED,
+      NavigationDrawer.DrawerType.FLOATING,
+      NavigationDrawer.DrawerType.PERSISTENT,
+      NavigationDrawer.DrawerType.PERSISTENT_MINI,
+      NavigationDrawer.DrawerType.TEMPORARY,
+      NavigationDrawer.DrawerType.TEMPORARY_MINI,
+    ]).isRequired,
+
+    /**
+     * The `DrawerType` to use for desktop displays. If the `desktopMinWidth`
+     * prop matches, this drawer type will be used.
+     */
+    desktopDrawerType: PropTypes.oneOf([
+      NavigationDrawer.DrawerType.FULL_HEIGHT,
+      NavigationDrawer.DrawerType.CLIPPED,
+      NavigationDrawer.DrawerType.FLOATING,
+      NavigationDrawer.DrawerType.PERSISTENT,
+      NavigationDrawer.DrawerType.PERSISTENT_MINI,
+      NavigationDrawer.DrawerType.TEMPORARY,
+      NavigationDrawer.DrawerType.TEMPORARY_MINI,
+    ]).isRequired,
+
+    /**
+     * The min width to use for a mobile media query.
+     */
+    mobileMinWidth: PropTypes.number.isRequired,
+
+    /**
+     * The min width to use for a tablet media query.
+     */
+    tabletMinWidth: PropTypes.number.isRequired,
+
+    /**
+     * The min width to use for a desktop media query.
+     */
+    desktopMinWidth: PropTypes.number.isRequired,
+
     /**
      * The icon className to use for the main menu icon.
      */
@@ -152,31 +197,20 @@ export default class NavigationDrawer extends Component {
     closeIconChildren: PropTypes.node,
 
     /**
-     * A function to call that will open the navigation drawer. This will
-     * be applied to the main menu button.
+     * Boolean if the sliding drawer should automatically close when a nav item
+     * is clicked for a temporary or temporary mini drawer.
      */
-    openDrawer: (props, propName, component) => {
-      removed(props, propName);
-      return PropTypes.func(props, propName, component);
-    },
+    autoclose: PropTypes.bool.isRequired,
 
     /**
-     * A function to call that will close the navigation drawer. If the drawer
-     * is persistent or persistent mini, this function is required.
+     * An optional function to call when the sliding drawer's open state changes.
+     * This can happen from rotating a tablet, resizing the browser window,
+     * or toggling the drawer for persistent and temporary versions. The
+     * `onDrawerChange` prop will be given the new open state.
+     *
+     * `onDrawerChange(isOpen)`
      */
-    closeDrawer: (props, propName, component) => {
-      removed(props, propName);
-
-      const { PERSISTENT, PERSISTENT_MINI } = NavigationDrawer.DrawerType;
-      const { drawerType } = props;
-      if(drawerType !== PERSISTENT && drawerType !== PERSISTENT_MINI) {
-        return;
-      }
-
-      return PropTypes.func.isRequired(props, propName, component)
-        || PropTypes.string(props, 'closeIconClassName', component)
-        || PropTypes.node(props, 'closeIconChildren', component);
-    },
+    onDrawerChange: PropTypes.func,
 
     /**
      * A list of items to render in the navigation drawer. If an item
@@ -186,7 +220,17 @@ export default class NavigationDrawer extends Component {
      * ##### Additional Info
      */
     navItems: PropTypes.arrayOf(PropTypes.oneOfType([
-      PropTypes.node,
+      /**
+       * Any react component you want to render instead of using props
+       * to generate a component.
+       */
+      PropTypes.element,
+
+      /**
+       * An object of props to use that will generate either a `ListItem`, `Divider`,
+       * or a `Subheader` component. Any props not listed will be passed
+       * to the component.
+       */
       PropTypes.shape({
         /**
          * Boolean if this item should be rendered as a divider.
@@ -214,183 +258,268 @@ export default class NavigationDrawer extends Component {
     ])).isRequired,
 
     /**
-     * The drawer type to use.
+     * The transition name to use for the drawer sliding in and out of view.
      */
-    drawerType: (props, propName, component) => {
-      removed(props, propName);
-      return PropTypes.oneOf(Object.keys(NavigationDrawer.DrawerType).map(k => NavigationDrawer.DrawerType[k]))(props, propName, component);
-    },
+    transitionName: PropTypes.string.isRequired,
 
     /**
-     * Any additional children to display in the main toolbar.
+     * The transition enter duration for the drawer sliding in to view.
      */
-    toolbarChildren: PropTypes.node,
+    transitionEnterTimeout: PropTypes.number.isRequired,
 
     /**
-     * An optional title to display in the main toolbar.
+     * The transition leave duration for the drawer sliding out of view.
      */
-    toolbarTitle: PropTypes.string,
+    transitionLeaveTimeout: PropTypes.number.isRequired,
+
+    /**
+     * An optional transition to use when the content changes. To disable this transition,
+     * set the enter and leave timeouts to `0`, `null`, or `undefined`. Make sure your
+     * child has a new key as well.
+     */
+    contentTransitionName: PropTypes.string.isRequired,
 
     /**
-     * An optional header to display in the navigation drawer. This will be rendered
-     * before the `navItems`. This will replace the generated header if you gave the
-     * `title` prop.
+     * The transition enter timeout for when the main content changes. Settings this value
+     * to `0`, `null`, or `undefined` will disable the enter transition.
      */
-    navHeader: (props, propName, component) => {
-      removed(props, propName);
-      return PropTypes.node(props, propName, component);
-    },
+    contentTransitionEnterTimeout: PropTypes.number,
 
     /**
-     * Any additional children to display in the navigation drawer's header after the
-     * `title`.
+     * The transition leave timeout for when the main content changes. Settings this value
+     * to `0`, `null`, or `undefined` will disable the leave transition.
      */
-    navHeaderChildren: (props, propName, component) => {
-      deprecate(props, propName, component, 'drawerChildren');
-      return PropTypes.node(props, propName, component);
-    },
+    contentTransitionLeaveTimeout: PropTypes.number,
   };
 
   static defaultProps = {
-    drawerType: NavigationDrawer.DrawerType.FULL_HEIGHT,
+    initialDrawerType: 'desktop',
+    mobileDrawerType: NavigationDrawer.DrawerType.TEMPORARY,
+    tabletDrawerType: NavigationDrawer.DrawerType.PERSISTENT,
+    desktopDrawerType: NavigationDrawer.DrawerType.FULL_HEIGHT,
+    mobileMinWidth: 320,
+    tabletMinWidth: 768,
+    desktopMinWidth: 1025,
     menuIconChildren: 'menu',
+    autoclose: true,
     closeIconChildren: 'keyboard_arrow_left',
+    transitionName: 'drawer',
+    transitionEnterTimeout: 300,
+    transitionLeaveTimeout: 300,
+    contentTransitionName: 'cross-fade',
+    contentTransitionEnterTimeout: 300,
   };
 
   componentDidMount() {
-    this.setState({ touch: isTouchDevice() }); // eslint-disable-line
+    window.addEventListener('resize', this._updateMedia);
+    this._updateMedia();
   }
 
-  componentWillUpdate(nextProps) {
-    if(nextProps.isOpen === this.props.isOpen && nextProps.drawerType === this.props.drawerType) {
-      return;
-    }
+  componentWillReceiveProps(nextProps) {
+    const {
+      mobileMinWidth,
+      mobileDrawerType,
+      tabletMinWidth,
+      tabletDrawerType,
+      desktopMinWidth,
+      desktopDrawerType,
+    } = this.props;
 
-    const temps = [
-      NavigationDrawer.DrawerType.TEMPORARY,
-      NavigationDrawer.DrawerType.TEMPORARY_MINI,
-    ];
+    if(nextProps.mobileMinWidth !== mobileMinWidth || nextProps.mobileDrawerType !== mobileDrawerType
+      || nextProps.tabletMinWidth !== tabletMinWidth || nextProps.tabletDrawerType !== tabletDrawerType
+      || nextProps.desktopMinWidth !== desktopMinWidth || nextProps.desktopDrawerType !== desktopDrawerType) {
+      this._updateDrawerType(nextProps);
+    }
+  }
 
-    const isNextTemp = temps.indexOf(nextProps.drawerType) !== -1;
-    const isCurrTemp = temps.indexOf(this.props.drawerType) !== -1;
-    if(!isNextTemp && !isCurrTemp) { return; }
-    if((isCurrTemp && !isNextTemp) || (!isCurrTemp && !isNextTemp)) {
-      setOverflow(false);
-    } else {
-      setOverflow(nextProps.isOpen);
+  componentWillUpdate(nextProps, nextState) {
+    if(this.state.isOpen !== nextState.isOpen && nextProps.onDrawerChange) {
+      nextProps.onDrawerChange(nextState.isOpen);
     }
   }
 
-  mapItemsToComponents = (mini, active, item, key) => {
-    if(React.isValidElement(item)) {
-      return item;
+  componentWillUnmount() {
+    window.removeEventListener('resize', this._updateMedia);
+  }
+
+  _matches = (min, max, orientation) => {
+    let media = 'only screen';
+    if(orientation) {
+      media += ` and (orientation: ${orientation})`;
+    }
+
+    media += ` and (min-width: ${min}px)`;
+
+    if(max) {
+      media += ` and (max-width: ${max}px)`;
     }
 
-    const { divider, subheader, primaryText, nestedItems, ...itemProps } = item;
-    let component;
-    if(divider) {
-      component = Divider;
-    } else if(subheader) {
-      component = Subheader;
+    return window.matchMedia(media).matches;
+  };
+
+  _updateDrawerType = (props) => {
+    const {
+      mobileMinWidth,
+      mobileDrawerType,
+      tabletMinWidth,
+      tabletDrawerType,
+      desktopMinWidth,
+      desktopDrawerType,
+    } = props;
+
+    const nextState = {
+      mobile: this._matches(mobileMinWidth, desktopMinWidth - 1),
+      tablet: this._matches(tabletMinWidth, desktopMinWidth - 1, 'landscape'),
+      desktop: this._matches(desktopMinWidth),
+    };
+
+    if(nextState.tablet) {
+      nextState.drawerType = tabletDrawerType;
+    } else if(nextState.mobile) {
+      nextState.drawerType = mobileDrawerType;
     } else {
-      component = ListItem;
+      nextState.drawerType = desktopDrawerType;
     }
 
-    const props = Object.assign({}, itemProps, {
-      key: item.key || key,
-      nestedItems: nestedItems && nestedItems.map(this.mapItemsToComponents.bind(this, mini, active)),
-    });
-    if(!mini || (mini && active)) {
-      props.primaryText = primaryText;
+    if(this._isFullHeight(nextState.drawerType) !== this._isFullHeight(this.state.drawerType)) {
+      nextState.isOpen = this._isFullHeight(nextState.drawerType);
     }
 
-    return React.createElement(component, props);
+    this.setState(nextState);
+  };
+
+  _updateMedia = () => {
+    this._updateDrawerType(this.props);
+  };
+
+  openDrawer = () => {
+    this.setState({ isOpen: true });
+  };
+
+  closeDrawer = () => {
+    this.setState({ isOpen: false });
+  };
+
+  _isFullHeight = (drawerType) => {
+    const { FULL_HEIGHT, CLIPPED, FLOATING } = NavigationDrawer.DrawerType;
+    return [FULL_HEIGHT, CLIPPED, FLOATING].indexOf(drawerType) !== -1;
+  };
+
+  _isPersistent = (drawerType) => {
+    const { PERSISTENT, PERSISTENT_MINI } = NavigationDrawer.DrawerType;
+    return [PERSISTENT, PERSISTENT_MINI].indexOf(drawerType) !== -1;
+  };
+
+  _isTemporary = (drawerType) => {
+    const { TEMPORARY, TEMPORARY_MINI } = NavigationDrawer.DrawerType;
+    return [TEMPORARY, TEMPORARY_MINI].indexOf(drawerType) !== -1;
+  };
+
+  _isMini = (drawerType) => {
+    const { PERSISTENT_MINI, TEMPORARY_MINI } = NavigationDrawer.DrawerType;
+    return [PERSISTENT_MINI, TEMPORARY_MINI].indexOf(drawerType) !== -1;
   };
 
   render() {
     const {
-      isOpen,
-      title,
-      toolbarTitle,
       style,
       className,
-      containerStyle,
-      containerClassName,
+      drawerStyle,
+      drawerClassName,
       contentStyle,
       contentClassName,
       toolbarStyle,
       toolbarClassName,
-      menuIconClassName,
+      drawerTitle,
+      drawerChildren,
       menuIconChildren,
-      children,
-      openDrawer,
-      closeDrawer,
-      closeIconClassName,
+      menuIconClassName,
       closeIconChildren,
-      navItems,
-      drawerType,
+      closeIconClassName,
+      toolbarTitle,
       toolbarChildren,
-      navHeader,
-      navHeaderChildren,
+      autoclose,
+      navItems,
+      children,
+      transitionName,
+      transitionEnterTimeout,
+      transitionLeaveTimeout,
+      contentTransitionName,
+      contentTransitionEnterTimeout,
+      contentTransitionLeaveTimeout,
     } = this.props;
 
-    const { touch } = this.state;
-    const { PERSISTENT, PERSISTENT_MINI, TEMPORARY, TEMPORARY_MINI } = NavigationDrawer.DrawerType;
-
-    const mini = drawerType === PERSISTENT_MINI || drawerType === TEMPORARY_MINI;
-    const persistent = drawerType === PERSISTENT_MINI || drawerType === PERSISTENT;
-    const temporary = drawerType === TEMPORARY || drawerType === TEMPORARY_MINI;
-    const active = isOpen || (!temporary && (!persistent && !touch));
-
-    let nav;
-    if(active || mini) {
-      const navigationItems = navItems.map(this.mapItemsToComponents.bind(this, mini, active));
-
-      nav = <List>{navigationItems}</List>;
-    }
-
-    let header;
-    if(navHeader) {
-      header = React.cloneElement(navHeader, { persistent });
-    } else if((active && mini) || !mini) {
-      header = (
-        <NavigationDrawerHeader
-          title={title}
-          closeDrawer={closeDrawer}
+    const { isOpen, drawerType } = this.state;
+    const persistent = this._isPersistent(drawerType);
+    const temporary = this._isTemporary(drawerType);
+    const mini = this._isMini(drawerType);
+
+    let drawer, overlay;
+    if(isOpen || mini) {
+      drawer = (
+        <Drawer
+          key="drawer"
+          style={drawerStyle}
+          className={drawerClassName}
+          autoclose={autoclose}
+          title={drawerTitle}
+          closeDrawer={this.closeDrawer}
           closeIconChildren={closeIconChildren}
           closeIconClassName={closeIconClassName}
+          temporary={temporary}
           persistent={persistent}
-          children={navHeaderChildren}
+          navItems={navItems}
+          drawerType={drawerType}
+          children={drawerChildren}
+          mini={mini}
+          isOpen={isOpen}
         />
       );
     }
 
-    const conditionalClassNames = { active };
+    if(temporary) {
+      overlay = <Overlay isOpen={isOpen} onClick={this.closeDrawer} />;
+    }
 
     return (
-      <div className={classnames('md-navigation-drawer-container', containerClassName, conditionalClassNames)} style={containerStyle}>
-        <nav className={classnames('md-navigation-drawer', className, drawerType, conditionalClassNames)} style={style}>
-          {header}
-          {nav}
-        </nav>
-        <div className={classnames('md-navigation-drawer-content', contentClassName, drawerType, conditionalClassNames)} style={contentStyle}>
-          <NavigationDrawerToolbar
-            className={classnames(toolbarClassName, drawerType, conditionalClassNames)}
-            temporary={temporary}
+      <CSSTransitionGroup
+        style={style}
+        className={classnames('md-navigation-drawer-container', className)}
+        component="div"
+        transitionName={transitionName}
+        transitionEnterTimeout={transitionEnterTimeout}
+        transitionLeaveTimeout={transitionLeaveTimeout}
+      >
+        {drawer}
+        <CSSTransitionGroup
+          style={contentStyle}
+          className={classnames('md-navigation-drawer-content', contentClassName, drawerType, {
+            'active': isOpen && !temporary,
+          })}
+          component="div"
+          transitionName={contentTransitionName}
+          transitionEnter={!!contentTransitionEnterTimeout}
+          transitionEnterTimeout={contentTransitionEnterTimeout}
+          transitionLeave={!!contentTransitionLeaveTimeout}
+          transitionLeaveTimeout={contentTransitionLeaveTimeout}
+        >
+          <DrawerToolbar
             isOpen={isOpen}
+            drawerType={drawerType}
+            style={toolbarStyle}
+            className={toolbarClassName}
+            temporary={temporary}
             persistent={persistent}
-            openDrawer={openDrawer}
-            menuIconClassName={menuIconClassName}
+            openDrawer={this.openDrawer}
             menuIconChildren={menuIconChildren}
+            menuIconClassName={menuIconClassName}
             title={toolbarTitle}
             children={toolbarChildren}
-            style={toolbarStyle}
-            touch={touch}
           />
           {children}
-          <Overlay isOpen={isOpen && (!persistent || persistent && touch)} onClick={closeDrawer} />
-        </div>
-      </div>
+          {overlay}
+        </CSSTransitionGroup>
+      </CSSTransitionGroup>
     );
   }
 }
diff --git a/src/js/NavigationDrawers/NavigationDrawerHeader.js b/src/js/NavigationDrawers/NavigationDrawerHeader.js
deleted file mode 100644
index 0b69203e2e..0000000000
--- a/src/js/NavigationDrawers/NavigationDrawerHeader.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import React, { PropTypes } from 'react';
-import classnames from 'classnames';
-
-import { IconButton } from '../Buttons';
-import Divider from '../Dividers';
-
-const NavigationDrawerHeader = (props) => {
-  const {
-    className,
-    children,
-    persistent,
-    title,
-    closeDrawer,
-    closeIconChildren,
-    closeIconClassName,
-  } = props;
-
-  let closeButton;
-  if(persistent && closeDrawer && (closeIconChildren || closeIconClassName)) {
-    closeButton = (
-      <IconButton
-        className="md-navigation-drawer-btn md-navigation-drawer-toggle"
-        onClick={closeDrawer}
-        iconClassName={closeIconClassName}
-        children={closeIconChildren}
-      />
-    );
-  }
-  return (
-    <header className={classnames('md-drawer-header', className)}>
-      {title && <h3 className="md-title">{title}</h3>}
-      {children}
-      {closeButton}
-      <Divider />
-    </header>
-  );
-};
-
-NavigationDrawerHeader.propTypes = {
-  className: PropTypes.string,
-  children: PropTypes.node,
-  persistent: PropTypes.bool.isRequired,
-  title: PropTypes.string,
-  closeDrawer: PropTypes.func,
-  closeIconClassName: PropTypes.string,
-  closeIconChildren: PropTypes.node,
-};
-
-export default NavigationDrawerHeader;
diff --git a/src/js/NavigationDrawers/NavigationDrawerToolbar.js b/src/js/NavigationDrawers/NavigationDrawerToolbar.js
deleted file mode 100644
index 7cf827af3c..0000000000
--- a/src/js/NavigationDrawers/NavigationDrawerToolbar.js
+++ /dev/null
@@ -1,65 +0,0 @@
-import React, { Component, PropTypes } from 'react';
-import PureRenderMixin from 'react-addons-pure-render-mixin';
-import classnames from 'classnames';
-import IconButton from '../Buttons/IconButton';
-
-export default class NavigationDrawerToolbar extends Component {
-  constructor(props) {
-    super(props);
-
-    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
-  }
-
-  static propTypes = {
-    style: PropTypes.object,
-    className: PropTypes.string,
-    children: PropTypes.node,
-    title: PropTypes.string,
-    isOpen: PropTypes.bool,
-    temporary: PropTypes.bool,
-    persistent: PropTypes.bool,
-    openDrawer: PropTypes.func,
-    menuIconChildren: PropTypes.node,
-    menuIconClassName: PropTypes.string,
-    touch: PropTypes.bool,
-  };
-
-  render() {
-    const {
-      style,
-      className,
-      children,
-      title,
-      temporary,
-      isOpen,
-      persistent,
-      openDrawer,
-      touch,
-      menuIconChildren,
-      menuIconClassName,
-    } = this.props;
-
-    let menuBtn;
-    if(temporary || touch || (persistent && !isOpen)) {
-      menuBtn = (
-        <IconButton
-          key="toggle"
-          onClick={openDrawer}
-          children={menuIconChildren}
-          iconClassName={menuIconClassName}
-        />
-      );
-    }
-
-    return (
-      <header
-        className={classnames('md-navigation-drawer-toolbar', className)}
-        style={style}
-      >
-        {menuBtn}
-        {title && <h3 className="md-title">{title}</h3>}
-        {children}
-      </header>
-    );
-  }
-}
diff --git a/src/js/NavigationDrawers/_NavigationDrawer.js b/src/js/NavigationDrawers/_NavigationDrawer.js
deleted file mode 100644
index 980bb9757f..0000000000
--- a/src/js/NavigationDrawers/_NavigationDrawer.js
+++ /dev/null
@@ -1,516 +0,0 @@
-import React, { Component, PropTypes } from 'react';
-import PureRenderMixin from 'react-addons-pure-render-mixin';
-import CSSTransitionGroup from 'react-addons-css-transition-group';
-import classnames from 'classnames';
-
-import Drawer from './Drawer';
-import DrawerToolbar from './DrawerToolbar';
-import Overlay from '../Transitions/Overlay';
-
-/**
- * Navigation Drawers are an excellent component to use to set up the initial
- * layout of your application. This component combines a Drawer
- * (a sidebar of nav items), an app bar, and displays any additional content.
- *
- * The `NavigationDrawer` component is customizable to have different
- * display states for mobile, tablet, and desktop displays.
- */
-export default class NavigationDrawer extends Component {
-  constructor(props) {
-    super(props);
-
-    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
-    this.state = {
-      isOpen: typeof props.initiallyOpen !== 'undefined' ? props.initiallyOpen : true,
-      mobile: false,
-      tablet: false,
-      desktop: true,
-      drawerType: props.desktopDrawerType,
-    };
-  }
-
-  static DrawerType = {
-    FULL_HEIGHT: 'full-height',
-    CLIPPED: 'clipped',
-    FLOATING: 'floating',
-    PERSISTENT: 'persistent',
-    PERSISTENT_MINI: 'mini',
-    TEMPORARY: 'temporary',
-    // want styles of temporary and mini. Little hacky.
-    TEMPORARY_MINI: 'temporary mini',
-  };
-
-  static propTypes = {
-    /**
-     * An optional style to apply to the entire container.
-     */
-    style: PropTypes.object,
-
-    /**
-     * An optional className to apply to the entire container.
-     */
-    className: PropTypes.string,
-
-    /**
-     * An optional style to apply to the drawer.
-     */
-    drawerStyle: PropTypes.object,
-
-    /**
-     * An optional className to apply to the drawer.
-     */
-    drawerClassName: PropTypes.string,
-
-    /**
-     * An optional style to apply to the main content.
-     */
-    contentStyle: PropTypes.object,
-
-    /**
-     * An optional className to apply to the main content.
-     */
-    contentClassName: PropTypes.string,
-
-    /**
-     * An optional style to apply to the main toolbar.
-     */
-    toolbarStyle: PropTypes.object,
-
-    /**
-     * An optional className to applt to the main toolbar.
-     */
-    toolbarClassName: PropTypes.string,
-
-    /**
-     * An optional title to display in the navigation drawer header.
-     */
-    drawerTitle: PropTypes.string,
-
-    /**
-     * Any additional children you want to display in the navigation drawer header after
-     * the optional title.
-     */
-    drawerChildren: PropTypes.node,
-
-    /**
-     * An optional title to display in the toolbar.
-     */
-    toolbarTitle: PropTypes.string,
-
-    /**
-     * Any additional children you want to display in the main toolbar after the menu
-     * button (for persistent drawers) and the optional title.
-     */
-    toolbarChildren: PropTypes.node,
-
-    /**
-     * The main content to display.
-     */
-    children: PropTypes.node,
-
-    /**
-     * An optional boolean if the drawer is open for persistent or temporary
-     * drawers.
-     */
-    initiallyOpen: PropTypes.bool,
-
-    /**
-     * The `DrawerType` to use for mobile devices. If the `mobileMinWidth` prop
-     * matches or the `tabletMinWidth` prop matches and the orientation is portrait,
-     * this drawer type will be used.
-     */
-    mobileDrawerType: PropTypes.oneOf([
-      NavigationDrawer.DrawerType.TEMPORARY,
-      NavigationDrawer.DrawerType.TEMPORARY_MINI,
-    ]).isRequired,
-
-    /**
-     * The `DrawerType` to use for landscape oriented tablets. If the
-     * `tabletMinWidth` prop matches and the orientation is landscape, this drawer
-     * type will be used.
-     */
-    tabletDrawerType: PropTypes.oneOf([
-      NavigationDrawer.DrawerType.FULL_HEIGHT,
-      NavigationDrawer.DrawerType.CLIPPED,
-      NavigationDrawer.DrawerType.FLOATING,
-      NavigationDrawer.DrawerType.PERSISTENT,
-      NavigationDrawer.DrawerType.PERSISTENT_MINI,
-      NavigationDrawer.DrawerType.TEMPORARY,
-      NavigationDrawer.DrawerType.TEMPORARY_MINI,
-    ]).isRequired,
-
-    /**
-     * The `DrawerType` to use for desktop displays. If the `desktopMinWidth`
-     * prop matches, this drawer type will be used.
-     */
-    desktopDrawerType: PropTypes.oneOf([
-      NavigationDrawer.DrawerType.FULL_HEIGHT,
-      NavigationDrawer.DrawerType.CLIPPED,
-      NavigationDrawer.DrawerType.FLOATING,
-      NavigationDrawer.DrawerType.PERSISTENT,
-      NavigationDrawer.DrawerType.PERSISTENT_MINI,
-      NavigationDrawer.DrawerType.TEMPORARY,
-      NavigationDrawer.DrawerType.TEMPORARY_MINI,
-    ]).isRequired,
-
-    /**
-     * The min width to use for a mobile media query.
-     */
-    mobileMinWidth: PropTypes.number.isRequired,
-
-    /**
-     * The min width to use for a tablet media query.
-     */
-    tabletMinWidth: PropTypes.number.isRequired,
-
-    /**
-     * The min width to use for a desktop media query.
-     */
-    desktopMinWidth: PropTypes.number.isRequired,
-
-    /**
-     * The icon className to use for the main menu icon.
-     */
-    menuIconClassName: PropTypes.string,
-
-    /**
-     * Any children required to render the main menu icon.
-     */
-    menuIconChildren: PropTypes.node,
-
-    /**
-     * The icon className to use for closing a persistent navigation drawer.
-     */
-    closeIconClassName: PropTypes.string,
-
-    /**
-     * Any children required to render the close icon for a persistent navigation drawer.
-     */
-    closeIconChildren: PropTypes.node,
-
-    /**
-     * Boolean if the sliding drawer should automatically close when a nav item
-     * is clicked for a temporary or temporary mini drawer.
-     */
-    autoclose: PropTypes.bool.isRequired,
-
-    /**
-     * An optional function to call when the sliding drawer's open state changes.
-     * This can happen from rotating a tablet, resizing the browser window,
-     * or toggling the drawer for persistent and temporary versions. The
-     * `onDrawerChange` prop will be given the new open state.
-     *
-     * `onDrawerChange(isOpen)`
-     */
-    onDrawerChange: PropTypes.func,
-
-    /**
-     * A list of items to render in the navigation drawer. If an item
-     * is a prop object, all props will be passed to either a `ListItem`,
-     * `Divider`, or `Subheader` component.
-     *
-     * ##### Additional Info
-     */
-    navItems: PropTypes.arrayOf(PropTypes.oneOfType([
-      /**
-       * Any react component you want to render instead of using props
-       * to generate a component.
-       */
-      PropTypes.element,
-
-      /**
-       * An object of props to use that will generate either a `ListItem`, `Divider`,
-       * or a `Subheader` component. Any props not listed will be passed
-       * to the component.
-       */
-      PropTypes.shape({
-        /**
-         * Boolean if this item should be rendered as a divider.
-         */
-        divider: PropTypes.bool,
-
-        /**
-         * Boolean if this item should be rendered as a subheader.
-         */
-        subheader: PropTypes.bool,
-
-        /**
-         * An optional component to render the `ListItem` as.
-         */
-        component: PropTypes.oneOfType([
-          PropTypes.string,
-          PropTypes.func,
-        ]),
-
-        /**
-         * The main text to be displayed in a `Subheader` or a `ListItem`.
-         */
-        primaryText: PropTypes.string,
-      }),
-    ])).isRequired,
-
-    /**
-     * The transition name to use for the drawer sliding in and out of view.
-     */
-    transitionName: PropTypes.string.isRequired,
-
-    /**
-     * The transition enter duration for the drawer sliding in to view.
-     */
-    transitionEnterTimeout: PropTypes.number.isRequired,
-
-    /**
-     * The transition leave duration for the drawer sliding out of view.
-     */
-    transitionLeaveTimeout: PropTypes.number.isRequired,
-
-    /**
-     * An optional transition to use when the content changes. To disable this transition,
-     * set the enter and leave timeouts to `0`, `null`, or `undefined`. Make sure your
-     * child has a new key as well.
-     */
-    contentTransitionName: PropTypes.string.isRequired,
-
-    /**
-     * The transition enter timeout for when the main content changes. Settings this value
-     * to `0`, `null`, or `undefined` will disable the enter transition.
-     */
-    contentTransitionEnterTimeout: PropTypes.number,
-
-    /**
-     * The transition leave timeout for when the main content changes. Settings this value
-     * to `0`, `null`, or `undefined` will disable the leave transition.
-     */
-    contentTransitionLeaveTimeout: PropTypes.number,
-  };
-
-  static defaultProps = {
-    mobileDrawerType: NavigationDrawer.DrawerType.TEMPORARY,
-    tabletDrawerType: NavigationDrawer.DrawerType.PERSISTENT,
-    desktopDrawerType: NavigationDrawer.DrawerType.FULL_HEIGHT,
-    mobileMinWidth: 320,
-    tabletMinWidth: 768,
-    desktopMinWidth: 1025,
-    menuIconChildren: 'menu',
-    autoclose: true,
-    closeIconChildren: 'keyboard_arrow_left',
-    transitionName: 'drawer',
-    transitionEnterTimeout: 300,
-    transitionLeaveTimeout: 300,
-    contentTransitionName: 'cross-fade',
-    contentTransitionEnterTimeout: 300,
-  };
-
-  componentDidMount() {
-    window.addEventListener('resize', this._updateMedia);
-    this._updateMedia();
-  }
-
-  componentWillReceiveProps(nextProps) {
-    const {
-      mobileMinWidth,
-      mobileDrawerType,
-      tabletMinWidth,
-      tabletDrawerType,
-      desktopMinWidth,
-      desktopDrawerType,
-    } = this.props;
-
-    if(nextProps.mobileMinWidth !== mobileMinWidth || nextProps.mobileDrawerType !== mobileDrawerType
-      || nextProps.tabletMinWidth !== tabletMinWidth || nextProps.tabletDrawerType !== tabletDrawerType
-      || nextProps.desktopMinWidth !== desktopMinWidth || nextProps.desktopDrawerType !== desktopDrawerType) {
-      this._updateDrawerType(nextProps);
-    }
-  }
-
-  componentWillUpdate(nextProps, nextState) {
-    if(this.state.isOpen !== nextState.isOpen && nextProps.onDrawerChange) {
-      nextProps.onDrawerChange(nextState.isOpen);
-    }
-  }
-
-  componentWillUnmount() {
-    window.removeEventListener('resize', this._updateMedia);
-  }
-
-  _matches = (min, max, orientation) => {
-    let media = 'only screen';
-    if(orientation) {
-      media += ` and (orientation: ${orientation})`;
-    }
-
-    media += ` and (min-width: ${min}px)`;
-
-    if(max) {
-      media += ` and (max-width: ${max}px)`;
-    }
-
-    return window.matchMedia(media).matches;
-  };
-
-  _updateDrawerType = (props) => {
-    const {
-      mobileMinWidth,
-      mobileDrawerType,
-      tabletMinWidth,
-      tabletDrawerType,
-      desktopMinWidth,
-      desktopDrawerType,
-    } = props;
-
-    const nextState = {
-      mobile: this._matches(mobileMinWidth, desktopMinWidth - 1),
-      tablet: this._matches(tabletMinWidth, desktopMinWidth - 1, 'landscape'),
-      desktop: this._matches(desktopMinWidth),
-    };
-
-    if(nextState.tablet) {
-      nextState.drawerType = tabletDrawerType;
-    } else if(nextState.mobile) {
-      nextState.drawerType = mobileDrawerType;
-    } else {
-      nextState.drawerType = desktopDrawerType;
-    }
-
-    if(this._isFullHeight(nextState.drawerType) !== this._isFullHeight(this.state.drawerType)) {
-      nextState.isOpen = this._isFullHeight(nextState.drawerType);
-    }
-
-    this.setState(nextState);
-  };
-
-  _updateMedia = () => {
-    this._updateDrawerType(this.props);
-  };
-
-  openDrawer = () => {
-    this.setState({ isOpen: true });
-  };
-
-  closeDrawer = () => {
-    this.setState({ isOpen: false });
-  };
-
-  _isFullHeight = (drawerType) => {
-    const { FULL_HEIGHT, CLIPPED, FLOATING } = NavigationDrawer.DrawerType;
-    return [FULL_HEIGHT, CLIPPED, FLOATING].indexOf(drawerType) !== -1;
-  };
-
-  _isPersistent = (drawerType) => {
-    const { PERSISTENT, PERSISTENT_MINI } = NavigationDrawer.DrawerType;
-    return [PERSISTENT, PERSISTENT_MINI].indexOf(drawerType) !== -1;
-  };
-
-  _isTemporary = (drawerType) => {
-    const { TEMPORARY, TEMPORARY_MINI } = NavigationDrawer.DrawerType;
-    return [TEMPORARY, TEMPORARY_MINI].indexOf(drawerType) !== -1;
-  };
-
-  _isMini = (drawerType) => {
-    const { PERSISTENT_MINI, TEMPORARY_MINI } = NavigationDrawer.DrawerType;
-    return [PERSISTENT_MINI, TEMPORARY_MINI].indexOf(drawerType) !== -1;
-  };
-
-  render() {
-    const {
-      style,
-      className,
-      drawerStyle,
-      drawerClassName,
-      contentStyle,
-      contentClassName,
-      toolbarStyle,
-      toolbarClassName,
-      drawerTitle,
-      drawerChildren,
-      menuIconChildren,
-      menuIconClassName,
-      closeIconChildren,
-      closeIconClassName,
-      toolbarTitle,
-      toolbarChildren,
-      autoclose,
-      navItems,
-      children,
-      transitionName,
-      transitionEnterTimeout,
-      transitionLeaveTimeout,
-      contentTransitionName,
-      contentTransitionEnterTimeout,
-      contentTransitionLeaveTimeout,
-    } = this.props;
-
-    const { isOpen, drawerType } = this.state;
-    const persistent = this._isPersistent(drawerType);
-    const temporary = this._isTemporary(drawerType);
-    const mini = this._isMini(drawerType);
-
-    let drawer, overlay;
-    if(isOpen || mini) {
-      drawer = (
-        <Drawer
-          key="drawer"
-          style={drawerStyle}
-          className={drawerClassName}
-          autoclose={autoclose}
-          title={drawerTitle}
-          closeDrawer={this.closeDrawer}
-          closeIconChildren={closeIconChildren}
-          closeIconClassName={closeIconClassName}
-          temporary={temporary}
-          persistent={persistent}
-          navItems={navItems}
-          drawerType={drawerType}
-          children={drawerChildren}
-          mini={mini}
-          isOpen={isOpen}
-        />
-      );
-    }
-
-    if(temporary) {
-      overlay = <Overlay isOpen={isOpen} onClick={this.closeDrawer} />;
-    }
-
-    return (
-      <CSSTransitionGroup
-        style={style}
-        className={classnames('md-navigation-drawer-container', className)}
-        component="div"
-        transitionName={transitionName}
-        transitionEnterTimeout={transitionEnterTimeout}
-        transitionLeaveTimeout={transitionLeaveTimeout}
-      >
-        {drawer}
-        <CSSTransitionGroup
-          style={contentStyle}
-          className={classnames('md-navigation-drawer-content', contentClassName, drawerType, {
-            'active': isOpen && !temporary,
-          })}
-          component="div"
-          transitionName={contentTransitionName}
-          transitionEnter={!!contentTransitionEnterTimeout}
-          transitionEnterTimeout={contentTransitionEnterTimeout}
-          transitionLeave={!!contentTransitionLeaveTimeout}
-          transitionLeaveTimeout={contentTransitionLeaveTimeout}
-        >
-          <DrawerToolbar
-            isOpen={isOpen}
-            drawerType={drawerType}
-            style={toolbarStyle}
-            className={toolbarClassName}
-            temporary={temporary}
-            persistent={persistent}
-            openDrawer={this.openDrawer}
-            menuIconChildren={menuIconChildren}
-            menuIconClassName={menuIconClassName}
-            title={toolbarTitle}
-            children={toolbarChildren}
-          />
-          {children}
-          {overlay}
-        </CSSTransitionGroup>
-      </CSSTransitionGroup>
-    );
-  }
-}
diff --git a/src/js/SelectFields/SelectField.js b/src/js/SelectFields/SelectField.js
index 7dbdaf4a78..450ff7ad37 100644
--- a/src/js/SelectFields/SelectField.js
+++ b/src/js/SelectFields/SelectField.js
@@ -518,13 +518,13 @@ export default class SelectField extends Component {
    */
   handleContainerClick = (e) => {
     let node = e.target;
-    while(node && node.classList) {
-      let classList = node.classList;
-      if(classList.contains('md-text-field')) {
-        e.stopPropagation();
+    while(node && node.parentNode) {
+      let { className } = node;
+      if(className.match(/md-text-field/)) {
+        e.preventDefault(); // stops a double click from being triggered. No idea why
         this.toggle();
         return;
-      } else if(classList.contains('md-list-tile')) {
+      } else if(className.match(/md-list-tile/)) {
         const tiles = Array.prototype.slice.call(ReactDOM.findDOMNode(this).querySelectorAll('.md-list-tile'));
         this.handleItemClick(tiles.indexOf(node), e);
         return;
@@ -594,6 +594,7 @@ export default class SelectField extends Component {
       }),
       listClassName: classnames('md-select-field-menu', listClassName, {
         'single-line': !floatingLabel,
+        'full-width': fullWidth,
       }),
       toggle,
       listStyle,
diff --git a/src/js/TextFields/FloatingLabel.js b/src/js/TextFields/FloatingLabel.js
index 64f70408d8..8f850c832e 100644
--- a/src/js/TextFields/FloatingLabel.js
+++ b/src/js/TextFields/FloatingLabel.js
@@ -1,33 +1,43 @@
-import React, { PropTypes } from 'react';
+import React, { Component, PropTypes } from 'react';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
 import classnames from 'classnames';
 
-const FloatingLabel = ({ active, error, label, required, value }) => {
-  if(required && label.indexOf('*') === -1) {
-    label = label.trim() + ' *';
+/**
+ * This component is used for rendering the floating label for a text field.
+ */
+export default class FloatingLabel extends Component {
+  constructor(props) {
+    super(props);
+
+    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
   }
 
-  return (
-    <span
-      className={classnames('md-floating-label', {
-        'active': active || !!value,
-        'focus': active,
-        error,
-      })}
-    >
-      {label}
-    </span>
-  );
-};
+  static propTypes = {
+    active: PropTypes.bool.isRequired,
+    error: PropTypes.bool.isRequired,
+    label: PropTypes.string.isRequired,
+    required: PropTypes.bool,
+    disabled: PropTypes.bool,
+    value: PropTypes.oneOfType([
+      PropTypes.string,
+      PropTypes.number,
+    ]),
+  };
+
+  render() {
+    const { active, error, required, value, disabled } = this.props;
+    let { label } = this.props;
+    if(required && label.indexOf('*') === -1) {
+      label = label.trim() + ' *';
+    }
 
-FloatingLabel.propTypes = {
-  active: PropTypes.bool.isRequired,
-  error: PropTypes.bool.isRequired,
-  label: PropTypes.string.isRequired,
-  required: PropTypes.bool,
-  value: PropTypes.oneOfType([
-    PropTypes.string,
-    PropTypes.number,
-  ]),
-};
+    const className= classnames('md-floating-label', {
+      error,
+      disabled,
+      'focus': active,
+      'active': active || !!value,
+    });
 
-export default FloatingLabel;
+    return <span className={className}>{label}</span>;
+  }
+}
diff --git a/src/js/TextFields/TextDivider.js b/src/js/TextFields/TextDivider.js
index f422b662ff..c761f2b72d 100644
--- a/src/js/TextFields/TextDivider.js
+++ b/src/js/TextFields/TextDivider.js
@@ -1,23 +1,33 @@
-import React, { PropTypes } from 'react';
+import React, { Component, PropTypes } from 'react';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
 import classnames from 'classnames';
 
-const TextDivider = ({ lineDirection, active, error, icon }) => {
-  return (
-    <div
-      className={classnames('md-text-divider', `from-${lineDirection}`, {
-        active,
-        error,
-        'icon-offset': icon,
-      })}
-    />
-  );
-};
+/**
+ * The `TextDivider` component renders the divider below the `TextField` component.
+ */
+export default class TextDivider extends Component {
+  constructor(props) {
+    super(props);
 
-TextDivider.propTypes = {
-  lineDirection: PropTypes.oneOf(['left', 'center', 'right']).isRequired,
-  active: PropTypes.bool.isRequired,
-  error: PropTypes.bool.isRequired,
-  icon: PropTypes.bool.isRequired,
-};
+    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
+  }
 
-export default TextDivider;
+  static propTypes = {
+    lineDirection: PropTypes.oneOf(['left', 'center', 'right']).isRequired,
+    active: PropTypes.bool.isRequired,
+    error: PropTypes.bool.isRequired,
+    icon: PropTypes.bool.isRequired,
+  };
+
+  render() {
+    const { lineDirection, active, error, icon } = this.props;
+    const className = classnames('md-text-divider', `from-${lineDirection}`, {
+      active,
+      error,
+      'icon-offset': icon,
+    });
+    return (
+      <div className={className} />
+    );
+  }
+}
diff --git a/src/js/TextFields/TextField.js b/src/js/TextFields/TextField.js
index 04bea0657f..5d2b179dba 100644
--- a/src/js/TextFields/TextField.js
+++ b/src/js/TextFields/TextField.js
@@ -1,10 +1,12 @@
 import React, { Component, PropTypes } from 'react';
+import { findDOMNode } from 'react-dom';
 import PureRenderMixin from 'react-addons-pure-render-mixin';
 import classnames from 'classnames';
 
 import FloatingLabel from './FloatingLabel';
 import TextDivider from './TextDivider';
 import TextFieldMessage from './TextFieldMessage';
+import FontIcon from '../FontIcons';
 
 const valueType = PropTypes.oneOfType([
   PropTypes.string,
@@ -18,6 +20,15 @@ const valueType = PropTypes.oneOfType([
  * Text Fields display as `inline-block` by default so that their size does not span `100%`. If
  * you want a text field per-line, wrap them in a div, or set them to display block (will make their width
  * expand as well though).
+ *
+ * There is a publically accessible `focus()` function to simplify the focusing of the text field.
+ *
+ * It can be used as follows:
+ *
+ * ```js
+ * <TextField ref="textField" />
+ * <button onClick={() => this.refs.textField.focus()}>Focus Text Field</button>
+ * ```
  */
 export default class TextField extends Component {
   constructor(props) {
@@ -29,6 +40,7 @@ export default class TextField extends Component {
       currentRows: props.rows,
       areaHeight: 'auto',
       value: props.defaultValue,
+      passwordVisible: false,
     };
   }
 
@@ -226,6 +238,16 @@ export default class TextField extends Component {
      * Boolean if the this text field should span the full width of a parent.
      */
     fullWidth: PropTypes.bool,
+
+    /**
+     * Any children used to render the password icon button.
+     */
+    passwordIconChildren: PropTypes.node,
+
+    /**
+     * Any icon className to use to render the password icon button.
+     */
+    passwordIconClassName: PropTypes.string,
   };
 
   static defaultProps = {
@@ -233,6 +255,7 @@ export default class TextField extends Component {
     defaultValue: '',
     floatingLabel: true,
     lineDirection: 'left',
+    passwordIconChildren: 'remove_red_eye',
   };
 
   getValue = () => {
@@ -289,8 +312,24 @@ export default class TextField extends Component {
     this.setState(state);
   };
 
+  /**
+   * A publicly accessibly API for focusing the text field.
+   */
+  focus = () => {
+    if(!this.textField) {
+      this.textField = findDOMNode(this.refs.textField || this.refs.textarea);
+    }
+
+
+    this.textField.focus();
+  };
+
+  _togglePasswordField = () => {
+    this.setState({ passwordVisible: !this.state.passwordVisible });
+  };
+
   render() {
-    const { active, currentRows, areaHeight } = this.state;
+    const { active, currentRows, areaHeight, passwordVisible } = this.state;
     const {
       className,
       inputClassName,
@@ -302,6 +341,8 @@ export default class TextField extends Component {
       floatingLabel,
       icon,
       rightIcon,
+      passwordIconChildren,
+      passwordIconClassName,
       lineDirection,
       rows,
       maxRows,
@@ -338,6 +379,7 @@ export default class TextField extends Component {
     if(icon) {
       fontIcon = React.cloneElement(icon, {
         className: classnames('md-text-field-icon', {
+          disabled,
           active,
           error,
           'with-floating-label': useFloatingLabel,
@@ -352,6 +394,20 @@ export default class TextField extends Component {
           'single-line': !useFloatingLabel,
         }),
       });
+    } else if(type === 'password') {
+      indIcon = (
+        <button
+          type="button"
+          onClick={this._togglePasswordField}
+          className={classnames('md-password-btn', {
+            'active': passwordVisible,
+            'multi-line': useFloatingLabel,
+            'single-line': !useFloatingLabel,
+          })}
+        >
+          <FontIcon iconClassName={passwordIconClassName} children={passwordIconChildren} />
+        </button>
+      );
     }
 
     if(errorText || maxLength || helpText) {
@@ -391,7 +447,6 @@ export default class TextField extends Component {
       onSelect,
       readOnly,
       size,
-      type,
       value,
     };
 
@@ -408,21 +463,43 @@ export default class TextField extends Component {
         }
       }
 
+      let visiblePlaceholder;
+      if(active || !useFloatingLabel || block) {
+        visiblePlaceholder = placeholder || label;
+
+        if(required && visiblePlaceholder.indexOf('*') === -1) {
+          visiblePlaceholder = visiblePlaceholder.trim() + ' *';
+        }
+      }
+
       textField = (
         <textarea
           {...textFieldProps}
-          placeholder={active || !useFloatingLabel || block ? (placeholder || label) : null}
+          placeholder={visiblePlaceholder}
           ref="textarea"
           rows={rows}
           style={areaStyle}
         />
       );
     } else {
+      let visiblePlaceholder;
+      if(!useFloatingLabel) {
+        visiblePlaceholder = placeholder || label;
+
+        if(required && visiblePlaceholder.indexOf('*') === -1) {
+          visiblePlaceholder = visiblePlaceholder.trim() + ' *';
+        }
+      } else {
+        visiblePlaceholder = placeholder;
+      }
+
       textField = (
         <input
           {...textFieldProps}
+          ref="textField"
+          type={passwordVisible ? 'text': type}
           style={inputStyle}
-          placeholder={!useFloatingLabel ? (placeholder || label) : placeholder}
+          placeholder={visiblePlaceholder}
         />
       );
     }
@@ -431,6 +508,7 @@ export default class TextField extends Component {
       <div
         {...props}
         className={classnames('md-text-field-container', className, {
+          disabled,
           'multi-line': multiline,
           'block': block,
           'full-width': fullWidth,
@@ -446,6 +524,7 @@ export default class TextField extends Component {
             error={error}
             required={required}
             value={value}
+            disabled={disabled}
           />
           }
           {textField}
diff --git a/src/js/TextFields/TextFieldMessage.js b/src/js/TextFields/TextFieldMessage.js
index 03fb3305af..bc37f62006 100644
--- a/src/js/TextFields/TextFieldMessage.js
+++ b/src/js/TextFields/TextFieldMessage.js
@@ -1,41 +1,54 @@
-import React, { PropTypes } from 'react';
+import React, { Component, PropTypes } from 'react';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
 import CSSTransitionGroup from 'react-addons-css-transition-group';
 import classnames from 'classnames';
 
-const TextFieldMessage = ({ value, message, maxLength, error, active, helpOnFocus, className }) => {
-  const isMessageVisible = !!message && (!helpOnFocus || active);
-  return (
-    <CSSTransitionGroup
-      component="div"
-      transitionName="opacity"
-      transitionEnterTimeout={150}
-      transitionLeaveTimeout={150}
-      className={classnames('md-text-field-message', className, {
-        error,
-        'count-only': !message || !isMessageVisible,
-      })}
-    >
-      {isMessageVisible && <span key="message">{message}</span>}
-      {maxLength &&
-      <span className="md-text-field-counter">
-        {`${value.length} / ${maxLength}`}
-      </span>
-      }
-    </CSSTransitionGroup>
-  );
-};
+/**
+ * The `TextFieldMessage` component is used for rendering a help or error text message
+ * under a `TextField`. It can also be used to display a counter of remaining characters.
+ */
+export default class TextFieldMessage extends Component {
+  constructor(props) {
+    super(props);
 
-TextFieldMessage.propTypes = {
-  value: PropTypes.oneOfType([
-    PropTypes.string,
-    PropTypes.number,
-  ]),
-  message: PropTypes.string,
-  maxLength: PropTypes.number,
-  error: PropTypes.bool.isRequired,
-  active: PropTypes.bool.isRequired,
-  helpOnFocus: PropTypes.bool,
-  className: PropTypes.string,
-};
+    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
+  }
 
-export default TextFieldMessage;
+  static propTypes = {
+    value: PropTypes.oneOfType([
+      PropTypes.string,
+      PropTypes.number,
+    ]).isRequired,
+    message: PropTypes.string,
+    maxLength: PropTypes.number,
+    error: PropTypes.bool.isRequired,
+    active: PropTypes.bool.isRequired,
+    helpOnFocus: PropTypes.bool,
+    className: PropTypes.string,
+  };
+
+  render() {
+    const { value, message, maxLength, error, active, helpOnFocus, className } = this.props;
+    const isMessageVisible = !!message && (!helpOnFocus || active);
+
+    return (
+      <CSSTransitionGroup
+        component="div"
+        transitionName="opacity"
+        transitionEnterTimeout={150}
+        transitionLeaveTimeout={150}
+        className={classnames('md-text-field-message', className, {
+          error,
+          'count-only': !message || !isMessageVisible,
+        })}
+      >
+        {isMessageVisible && <span key="message">{message}</span>}
+        {maxLength &&
+        <span className="md-text-field-counter">
+          {`${value.length} / ${maxLength}`}
+        </span>
+        }
+      </CSSTransitionGroup>
+    );
+  }
+}
diff --git a/src/js/index.js b/src/js/index.js
index 436fb17292..350d854b94 100644
--- a/src/js/index.js
+++ b/src/js/index.js
@@ -1,4 +1,5 @@
 import Avatar from './Avatars';
+import BottomNavigation from './BottomNavigations';
 import { FlatButton, FloatingButton, IconButton, RaisedButton } from './Buttons';
 import { Card, CardActions, CardActionOverlay, CardHeader, CardMedia, CardText, CardTitle } from './Cards';
 import Chip from './Chips';
@@ -6,9 +7,11 @@ import { DataTable, TableHeader, TableBody, TableRow, TableColumn, EditDialogCol
 import Dialog from './Dialogs';
 import Divider from './Dividers';
 import { SpeedDial } from './FABTransitions';
+import FileInput, { FileUpload } from './FileInput';
 import FontIcon from './FontIcons';
 import Ink from './Inks';
 import { List, ListItem } from './Lists';
+import Menu from './Menus';
 import NavigationDrawer from './NavigationDrawers';
 import Paper from './Papers';
 import { DatePicker, TimePicker } from './Pickers';
@@ -25,6 +28,7 @@ import Toolbar from './Toolbars';
 import Tooltip from './Tooltips';
 
 export { Avatar };
+export { BottomNavigation };
 export { Card };
 export { CardActions };
 export { CardActionOverlay };
@@ -42,12 +46,15 @@ export { Divider };
 export { EditDialogColumn };
 export { FlatButton };
 export { FloatingButton };
+export { FileInput };
+export { FileUpload };
 export { FontIcon };
 export { IconButton };
 export { Ink };
 export { LinearProgress };
 export { List };
 export { ListItem };
+export { Menu };
 export { NavigationDrawer };
 export { Paper };
 export { Radio };
@@ -72,6 +79,7 @@ export { Tooltip };
 
 export default {
   Avatar,
+  BottomNavigation,
   FlatButton,
   RaisedButton,
   FloatingButton,
@@ -99,7 +107,10 @@ export default {
   Chip,
   Dialog,
   Divider,
+  FileInput,
+  FileUpload,
   FontIcon,
+  Menu,
   NavigationDrawer,
   Ink,
   Paper,
diff --git a/src/scss/_media-queries.scss b/src/scss/_media-queries.scss
index 6419acf966..885d06b794 100644
--- a/src/scss/_media-queries.scss
+++ b/src/scss/_media-queries.scss
@@ -48,3 +48,7 @@ $md-mobile-max-width: $md-desktop-min-width - 1;
     @include md-pickers-landscape();
   }
 }
+
+@media only screen and (orientation: landscape) and (min-width: #{$md-tablet-min-width}) and (max-width: #{$md-mobile-max-width}) {
+  @include md-navigation-drawer-tablet-landscape();
+}
diff --git a/src/scss/components/_buttons.scss b/src/scss/components/_buttons.scss
index 204e29e3e8..4912484bcb 100644
--- a/src/scss/components/_buttons.scss
+++ b/src/scss/components/_buttons.scss
@@ -92,3 +92,12 @@ a.md-btn {
     color: inherit;
   }
 }
+
+.md-file-input {
+  height: 0;
+  opacity: 0;
+  overflow: hidden;
+  position: absolute;
+  width: 0;
+  z-index: -1;
+}
diff --git a/src/scss/components/_cards.scss b/src/scss/components/_cards.scss
index 074db5ec43..74d557b275 100644
--- a/src/scss/components/_cards.scss
+++ b/src/scss/components/_cards.scss
@@ -138,6 +138,7 @@
 }
 
 .md-card-media {
+  overflow: hidden;
   position: relative;
 }
 
diff --git a/src/scss/components/_menus.scss b/src/scss/components/_menus.scss
index 0abf56a9b4..3fb8e55374 100644
--- a/src/scss/components/_menus.scss
+++ b/src/scss/components/_menus.scss
@@ -41,6 +41,10 @@
     }
   }
 
+  &.full-width {
+    @extend %full-width;
+  }
+
   .md-menu-container {
     position: absolute;
   }
diff --git a/src/scss/components/_navigation-drawers.scss b/src/scss/components/_navigation-drawers.scss
index 9278866e50..27c15337f8 100644
--- a/src/scss/components/_navigation-drawers.scss
+++ b/src/scss/components/_navigation-drawers.scss
@@ -2,24 +2,30 @@
   @include md-box-shadow(1);
 
   height: 100vh;
+  left: 0;
   overflow-y: auto;
   position: fixed;
-  transition-duration: $md-transition-time;
-  transition-property: box-shadow, left, width;
   z-index: $md-navigation-drawer-z-index;
 
   &.active,
   &.mini {
-    left: 0;
     z-index: $md-navigation-drawer-z-index - 5;
   }
 
-  &.mini:not(.active) {
+  &.mini {
+    overflow-x: hidden;
+    transition-duration: $md-navigation-drawer-transition-time;
+    transition-property: width;
     width: $md-navigation-drawer-mini-width;
 
     .md-list-tile {
       justify-content: center;
     }
+
+    &:not(.active) .md-tile-content {
+      display: none;
+      visibility: hidden;
+    }
   }
 
   &.temporary.active {
@@ -30,6 +36,9 @@
 }
 
 .md-navigation-drawer-content {
+  padding-left: 0;
+  transition: padding-left $md-navigation-drawer-transition-time;
+
   &.mini {
     padding-left: $md-navigation-drawer-mini-width;
     position: relative;
@@ -64,7 +73,7 @@
   left: 0;
   position: fixed;
   top: 0;
-  transition-duration: $md-transition-time;
+  transition-duration: $md-navigation-drawer-transition-time;
   transition-property: box-shadow, left, width;
   width: 100%;
   z-index: $md-navigation-drawer-toolbar-z-index;
@@ -91,3 +100,26 @@
 .md-navigation-drawer-toggle {
   float: right;
 }
+
+
+.drawer-leave,
+.drawer-enter.drawer-enter-active {
+  left: 0;
+}
+
+.drawer-enter-active,
+.drawer-leave-active {
+  transition: left $md-navigation-drawer-transition-time;
+}
+
+.cross-fade-enter {
+  opacity: .01;
+  transform: translate3d(0, $md-cross-fade-distance, 0);
+
+  &.cross-fade-enter-active {
+    opacity: 1;
+    transform: translate3d(0, 0, 0) $md-cross-fade-transition-time;
+    transition-duration: $md-cross-fade-transition-time;
+    transition-property: transform, opacity;
+  }
+}
diff --git a/src/scss/components/_text-fields.scss b/src/scss/components/_text-fields.scss
index cb1fe8b867..f244b67ae3 100644
--- a/src/scss/components/_text-fields.scss
+++ b/src/scss/components/_text-fields.scss
@@ -1,6 +1,5 @@
 .md-text-field-container {
   @include md-theme-placeholders($md-secondary-text-color);
-
   display: inline-block;
   vertical-align: top;
 
@@ -39,6 +38,28 @@
   &.full-width {
     width: 100%;
   }
+
+  &.disabled {
+    @extend %md-disabled-text;
+    @include md-theme-placeholders($md-disabled-color);
+  }
+
+  // scss-lint:disable VendorPrefix, PseudoElement
+  :-moz-placeholder {
+    opacity: 1;
+  }
+
+  ::-moz-placeholder {
+    opacity: 1;
+  }
+
+  :-ms-input-placeholder {
+    opacity: 1;
+  }
+
+  ::-webkit-input-placeholder {
+    opacity: 1;
+  }
 }
 
 .md-text-field-label {
@@ -96,12 +117,9 @@
   position: absolute;
 }
 
-.md-floating-label,
-.md-text-field-icon {
-  color: $md-hint-text-color;
-}
-
 .md-floating-label {
+  @extend %md-secondary-text;
+
   cursor: text;
   pointer-events: none;
   top: $md-text-field-floating-label-top;
@@ -111,6 +129,10 @@
   &.active {
     font-size: $md-text-field-active-font-size;
   }
+
+  &.disabled {
+    @extend %md-disabled-text;
+  }
 }
 
 .md-text-divider {
@@ -146,12 +168,20 @@
 }
 
 .md-text-field-icon {
+  @extend %md-secondary-text;
+
   margin-right: $md-text-field-icon-margin;
   transition-duration: $md-transition-time;
   transition-property: color;
+
+  &.disabled {
+    @extend %md-disabled-text;
+  }
 }
 
 .md-text-field-message {
+  @extend %md-hint-text;
+
   display: flex;
   font-size: $md-text-field-info-font-size;
   justify-content: space-between;
@@ -180,3 +210,21 @@
 .md-text-field-ind {
   color: $md-icon-color;
 }
+
+.md-password-btn {
+  @extend %button-unstyled;
+  @extend %clickable;
+  @extend %md-hint-text;
+
+  height: $md-font-icon-size;
+  padding-left: $md-default-padding;
+  vertical-align: bottom;
+
+  &.active {
+    @extend %md-secondary-text;
+  }
+
+  .md-icon {
+    color: inherit;
+  }
+}
diff --git a/src/scss/helpers/_variables.scss b/src/scss/helpers/_variables.scss
index 280a43f78c..706dd44fab 100644
--- a/src/scss/helpers/_variables.scss
+++ b/src/scss/helpers/_variables.scss
@@ -6,6 +6,12 @@
 /// @group media-queries
 $md-mobile-min-width: 320px !default;
 
+/// The min width for the tablet media query.
+///
+/// @type String
+/// @group media-queries
+$md-tablet-min-width: 768px !default;
+
 
 /// The min width for the desktop media query.
 ///
@@ -796,6 +802,67 @@ $md-toolbar-title-keyline: 72px;
 
 
 
+// ===================================================================================
+// Navigation Drawers
+// ===================================================================================
+
+/// The width for the navigation drawer on mobile devices.
+///
+/// @type String
+/// @group navigation-drawers
+$md-navigation-drawer-mobile-left: calc(-100% - 56px) !default;
+
+/// The width for the navigation drawer on mobile devices.
+///
+/// @type String
+/// @group navigation-drawers
+$md-navigation-drawer-mobile-width: calc(100% - 56px) !default;
+
+/// The width for the navigation drawer on desktops.
+///
+/// @type String
+/// @group navigation-drawers
+$md-navigation-drawer-desktop-width: 256px !default;
+
+/// The width for a mini navigation drawer that has not been opened.
+///
+/// @type String
+/// @group navigation-drawers
+$md-navigation-drawer-mini-width: 72px !default;
+
+/// The max width for a navigation drawer on mobile devices.
+///
+/// @type String
+/// @group navigation-drawers
+$md-navigation-drawer-mobile-max-width: 320px;
+
+/// the max width for a navigation drawer on desktop devices.
+///
+/// @type String
+/// @group navigation-drawers
+$md-navigation-drawer-desktop-max-width: 400px;
+
+
+/// The transition time for the navigation drawer to slide in and out of view.
+///
+/// @type String
+/// @group navigation-drawers
+$md-navigation-drawer-transition-time: $md-transition-time * 2 !default;
+
+/// The distance that the cross-fade transition should move.
+///
+/// @type String
+/// @group navigation-drawers
+$md-cross-fade-distance: 10px !default;
+
+/// The transition time for the cross-fade transition.
+///
+/// @type Time
+/// @group navigation-drawers
+$md-cross-fade-transition-time: $md-transition-time * 2 !default;
+
+
+
 // ===================================================================================
 // Sidebars
 // ===================================================================================
@@ -810,13 +877,15 @@ $md-sidebar-open-width: 256px !default;
 ///
 /// @type String
 /// @group sidebars
-$md-sidebar-mobile-max-width: 320px;
+/// @alias md-navigation-drawer-mobile-max-width
+$md-sidebar-mobile-max-width: $md-navigation-drawer-mobile-max-width;
 
 /// The max open width for the sidebar that is docked to the left.
 ///
 /// @type String
 /// @group sidebars
-$md-sidebar-desktop-max-width: 400px;
+/// @alias md-navigation-drawer-desktop-max-width
+$md-sidebar-desktop-max-width: $md-navigation-drawer-desktop-max-width;
 
 
 
@@ -855,7 +924,7 @@ $md-snackbar-lr-padding: 24px;
 /// @group snackbars
 $md-snackbar-tb-padding: 14px;
 
-/// The margin left for the action button in a sidebar.
+/// The margin left for the action button in a snackbar.
 ///
 /// @type String
 /// @group snackbars
@@ -1569,37 +1638,6 @@ $md-tooltip-desktop-lr-padding: 8px;
 $md-tooltip-desktop-height: 22px;
 
 
-
-// ===================================================================================
-// Navigation Drawers
-// ===================================================================================
-
-/// The width for the navigation drawer on mobile devices.
-///
-/// @type String
-/// @group navigation-drawers
-$md-navigation-drawer-mobile-left: calc(-100% - 56px) !default;
-
-/// The width for the navigation drawer on mobile devices.
-///
-/// @type String
-/// @group navigation-drawers
-$md-navigation-drawer-mobile-width: calc(100% - 56px) !default;
-
-/// The width for the navigation drawer on desktops.
-///
-/// @type String
-/// @group navigation-drawers
-$md-navigation-drawer-desktop-width: 256px !default;
-
-///
-///
-/// @type String
-/// @group navigation-drawers
-$md-navigation-drawer-mini-width: 72px !default;
-
-
-
 // ===================================================================================
 // Data Tables
 // ===================================================================================
diff --git a/src/scss/helpers/mixins/_navigation-drawers.scss b/src/scss/helpers/mixins/_navigation-drawers.scss
index 2db831ce4e..16b550777b 100644
--- a/src/scss/helpers/mixins/_navigation-drawers.scss
+++ b/src/scss/helpers/mixins/_navigation-drawers.scss
@@ -1,9 +1,31 @@
+/// Styles the navigation drawer component to use a specific toolbar height at a
+/// media screen size.
+///
+/// @access private
+/// @param {String} $height - The current height of the toolbar for this media size
+/// @group navigation-drawers
 @mixin md-navigation-drawer-media($height) {
-  .md-navigation-drawer-content,
-  .md-navigation-drawer.mini:not(.active) {
+  .md-navigation-drawer-content {
     padding-top: $height;
   }
 
+  .md-navigation-drawer {
+    &.clipped,
+    &.floating,
+    &.mini:not(.active) {
+      max-height: calc(100vh - #{$height});
+    }
+
+    &.mini:not(.active) {
+      top: $height;
+    }
+
+    &.clipped,
+    &.floating {
+      margin-top: $height;
+    }
+  }
+
   .md-drawer-header,
   .md-navigation-drawer-toolbar {
     height: $height;
@@ -19,34 +41,59 @@
   }
 }
 
+/// Styles the navigation drawer component for a portait mobile device
+///
+/// @group navigation-drawers
 @mixin md-navigation-drawer-mobile-portrait() {
   @include md-navigation-drawer-media($md-toolbar-mobile-portrait-height);
 }
 
+/// Styles the navigation drawer component for a landscape mobile device
+///
+/// @group navigation-drawers
 @mixin md-navigation-drawer-mobile-landscape() {
   @include md-navigation-drawer-media($md-toolbar-mobile-landscape-height);
 }
 
+/// Styles the navigation drawer component for a landscape tablet device
+///
+/// @group navigation-drawers
+@mixin md-navigation-drawer-tablet-landscape() {
+  @include md-navigation-drawer-landscape-devices();
+}
+
+/// Styles the navigation drawer component for a mobile device. This updates
+/// the drawer so it will always be temporary and updates the width.
+///
+/// @group navigation-drawers
 @mixin md-navigation-drawer-mobile() {
   .md-navigation-drawer {
-    left: $md-navigation-drawer-mobile-left;
-    max-width: $md-sidebar-mobile-max-width;
-    width: $md-navigation-drawer-mobile-width;
+    @include md-box-shadow(3);
 
-    &.active {
-      @include md-box-shadow(3);
+    max-width: $md-navigation-drawer-mobile-max-width;
+    width: $md-navigation-drawer-mobile-width;
+    z-index: $md-navigation-drawer-temporary-z-index;
 
-      z-index: $md-navigation-drawer-temporary-z-index;
+    &.mini.active {
+      width: $md-navigation-drawer-mobile-width;
     }
   }
+
+  .drawer-enter,
+  .drawer-leave.drawer-leave-active {
+    left: $md-navigation-drawer-mobile-left;
+  }
 }
 
-@mixin md-navigation-drawer-desktop() {
-  @include md-navigation-drawer-media($md-toolbar-desktop-height);
 
+/// Styles the navigation drawer component for larger screened landscape
+/// devices. This should be used for landscape tablets and desktop screens.
+///
+/// @access private
+/// @group navigation-drawers
+@mixin md-navigation-drawer-landscape-devices() {
   .md-navigation-drawer {
-    left: -$md-navigation-drawer-desktop-width;
-    max-width: $md-sidebar-desktop-max-width;
+    max-width: $md-navigation-drawer-desktop-max-width;
     width: $md-navigation-drawer-desktop-width;
 
     &.floating {
@@ -55,10 +102,12 @@
 
     &.floating,
     &.clipped {
-      height: calc(100vh - #{$md-toolbar-desktop-height});
-      margin-top: $md-toolbar-desktop-height;
       z-index: $md-navigation-drawer-z-index - 5;
     }
+
+    &.mini.active {
+      width: $md-navigation-drawer-desktop-width;
+    }
   }
 
   .md-navigation-drawer-toolbar {
@@ -70,16 +119,25 @@
     }
   }
 
-  .md-navigation-drawer-content {
-    &:not(.temporary) {
-      &.active {
-        padding-left: $md-navigation-drawer-desktop-width;
-        width: 100%;
-      }
-    }
+  .md-navigation-drawer-content.active {
+    padding-left: $md-navigation-drawer-desktop-width;
+    width: 100%;
+  }
+
+  .drawer-enter,
+  .drawer-leave.drawer-leave-active {
+    left: -$md-navigation-drawer-desktop-width;
   }
 }
 
+/// Styles the navigation drawer component for desktop displays.
+///
+/// @group navigation-drawers
+@mixin md-navigation-drawer-desktop() {
+  @include md-navigation-drawer-media($md-toolbar-desktop-height);
+  @include md-navigation-drawer-landscape-devices();
+}
+
 /// Themes the navigation drawers with the given color
 ///
 /// @example scss - Basic Usage SASS
diff --git a/src/scss/helpers/mixins/_text-fields.scss b/src/scss/helpers/mixins/_text-fields.scss
index 4d5bd6bd39..b623cc5575 100644
--- a/src/scss/helpers/mixins/_text-fields.scss
+++ b/src/scss/helpers/mixins/_text-fields.scss
@@ -135,6 +135,10 @@
       padding-top: $ind-padding;
     }
   }
+
+  .md-password-btn {
+    margin-bottom: $md-text-field-mobile-divider-padding * 2;
+  }
 }
 
 /// Generates css for a text field on desktop
@@ -165,6 +169,10 @@
       padding-top: $ind-padding;
     }
   }
+
+  .md-password-btn {
+    margin-bottom: $md-text-field-desktop-divider-padding * 2;
+  }
 }
 
 /// Themes the text fields for the given color. This theme mixin does not use the "&." syntax.
@@ -205,12 +213,10 @@
 /// @param {Color} $error-color [$md-red-500] - The error color to use for the text field
 ///        underlines, icons, floating labels and error message text when there is an error.
 /// @group text-fields
-@mixin md-theme-text-fields($color: $md-primary-color, $hint-color: $md-secondary-text-color, $error-color: $md-red-500) {
+@mixin md-theme-text-fields($color: $md-primary-color, $error-color: $md-red-500) {
   .md-floating-label,
   .md-text-field-message,
   .md-text-field-icon {
-    color: $hint-color;
-
     &.error {
       color: $error-color;
     }
@@ -259,6 +265,7 @@
 ///   }
 ///
 /// @param {Color} $color - The placeholder color to use
+/// @group text-fields
 @mixin md-theme-placeholders($color) {
   // scss-lint:disable VendorPrefix, PseudoElement
 


Test patch:
diff --git a/src/js/FileInputs/__tests__/FileInput.js b/src/js/FileInputs/__tests__/FileInput.js
new file mode 100644
index 0000000000..58f0c269f9
--- /dev/null
+++ b/src/js/FileInputs/__tests__/FileInput.js
@@ -0,0 +1,92 @@
+/*eslint-env jest*/
+jest.unmock('../FileInput');
+
+import React from 'react';
+import { findDOMNode } from 'react-dom';
+import {
+  Simulate,
+  renderIntoDocument,
+  findRenderedDOMComponentWithTag,
+} from 'react-addons-test-utils';
+
+import FileInput from '../FileInput';
+
+const File = jest.fn(name => ({
+  name: name || 'Test.jpg',
+  lastModifiedDate: new Date(),
+  isClosed: true,
+  size: 39202,
+  type: 'image/jpg',
+}));
+
+describe('FileInput', () => {
+  it('merges className and style', () => {
+    const style = { display: 'block' };
+    const className = 'test';
+    const fileInput = renderIntoDocument(
+      <FileInput style={style} className={className} onChange={jest.fn()} />
+    );
+
+    const fileInputNode = findDOMNode(fileInput);
+    expect(fileInputNode.style.display).toBe(style.display);
+    expect(fileInputNode.className).toContain(className);
+  });
+
+  it('returns a single file when multiple is false onChange', () => {
+    const onChange = jest.fn();
+    const fileInput = renderIntoDocument(<FileInput onChange={onChange} />);
+
+    const input = findRenderedDOMComponentWithTag(fileInput, 'input');
+
+    // can't instantiate fully, so we can make an almost exact reference
+    const files = [new File()];
+
+    Simulate.change(input, { target: { files } });
+
+    expect(onChange.mock.calls.length).toBe(1);
+    expect(onChange.mock.calls[0][0]).toEqual(files[0]);
+  });
+
+  it('returns null when the user cancel\'s a file selection when multiple is false onChange', () => {
+    const onChange = jest.fn();
+    const fileInput = renderIntoDocument(<FileInput onChange={onChange} />);
+
+    const input = findRenderedDOMComponentWithTag(fileInput, 'input');
+    // Cancel returns empty list
+    const files = [];
+
+    Simulate.change(input, { target: { files } });
+
+    expect(onChange.mock.calls.length).toBe(1);
+    expect(onChange.mock.calls[0][0]).toBe(null);
+  });
+
+  it('returns a list of files when multiple is true onChange', () => {
+    const onChange = jest.fn();
+    const fileInput = renderIntoDocument(<FileInput onChange={onChange} multiple={true} />);
+
+    const input = findRenderedDOMComponentWithTag(fileInput, 'input');
+
+    // can't instantiate fully, so we can make an almost exact reference
+    const files = [new File(), new File('Test2.jpg')];
+
+    Simulate.change(input, { target: { files } });
+
+    expect(onChange.mock.calls.length).toBe(1);
+    expect(onChange.mock.calls[0][0]).toEqual(files);
+  });
+
+  it('returns an empty list of files when the user cancels an upload and multiple is true onChange', () => {
+    const onChange = jest.fn();
+    const fileInput = renderIntoDocument(<FileInput onChange={onChange} multiple={true} />);
+
+    const input = findRenderedDOMComponentWithTag(fileInput, 'input');
+    const files = [];
+
+    Simulate.change(input, { target: { files } });
+
+    expect(onChange.mock.calls.length).toBe(1);
+    expect(onChange.mock.calls[0][0].length).toBe(0);
+    expect(onChange.mock.calls[0][0]).toEqual(files);
+  });
+});
diff --git a/src/js/FileInputs/__tests__/FileUpload.js b/src/js/FileInputs/__tests__/FileUpload.js
new file mode 100644
index 0000000000..0b12986be5
--- /dev/null
+++ b/src/js/FileInputs/__tests__/FileUpload.js
@@ -0,0 +1,380 @@
+/*eslint-env jest, jasmine*/
+jest.unmock('../FileUpload');
+jest.unmock('../FileInput');
+
+import React from 'react';
+import { findDOMNode } from 'react-dom';
+import {
+  Simulate,
+  renderIntoDocument,
+  findRenderedDOMComponentWithTag,
+} from 'react-addons-test-utils';
+
+import FileUpload from '../FileUpload';
+
+const File = jest.fn((size, type) => ({
+  name: 'Test.jpg',
+  lastModifiedDate: new Date(),
+  isClosed: true,
+  size: size || 1024,
+  type: typeof type === 'string' ? type : 'image/jpg',
+}));
+
+describe('FileUpload', () => {
+  it('merges className and style', () => {
+    const style = { display: 'block' };
+    const className = 'test';
+    const fileUpload = renderIntoDocument(
+      <FileUpload style={style} className={className} onChange={jest.fn()} />
+    );
+
+    const fileUploadNode = findDOMNode(fileUpload);
+    expect(fileUploadNode.style.display).toBe(style.display);
+    expect(fileUploadNode.className).toContain(className);
+  });
+
+  it('calls the onChange function still', () => {
+    const onChange = jest.fn();
+    const fileUpload = renderIntoDocument(<FileUpload onChange={onChange} />);
+
+    const input = findRenderedDOMComponentWithTag(fileUpload, 'input');
+    const files = [new File()];
+
+    Simulate.change(input, { target: { files }});
+    expect(onChange.mock.calls.length).toBe(1);
+    expect(onChange.mock.calls[0][0]).toEqual(files[0]);
+  });
+
+  it('prevents any files with a size greater than the maxSize', () => {
+    const onSizeError = jest.fn();
+    const fileUpload = renderIntoDocument(<FileUpload maxSize={1024} onSizeError={onSizeError} />);
+
+    const input = findRenderedDOMComponentWithTag(fileUpload, 'input');
+    let files = [new File()];
+
+    Simulate.change(input, { target: { files }});
+    expect(onSizeError.mock.calls.length).toBe(0);
+
+    files = [new File(1025)];
+
+    Simulate.change(input, { target: { files }});
+    expect(onSizeError.mock.calls.length).toBe(1);
+    expect(onSizeError.mock.calls[0][0]).toEqual(files);
+  });
+
+  it('calls the onError prop with the current file, error, and error event', () => {
+    const eventListener = jasmine.createSpy();
+
+    const frMock = {
+      onerror: jest.fn(e => e),
+      readAsDataURL: jest.fn(function() {
+        this.onerror({ target: { error: new Error('Something went wrong') } });
+      }),
+      addEventListener: eventListener,
+    };
+
+    spyOn(window, 'FileReader').and.returnValue(frMock);
+
+    const onError = jest.fn();
+    const fileUpload = renderIntoDocument(<FileUpload onError={onError} />);
+    const input = findRenderedDOMComponentWithTag(fileUpload, 'input');
+    const files = [new File()];
+
+    Simulate.change(input, { target: { files }});
+    expect(onError.mock.calls.length).toBe(1);
+    expect(onError.mock.calls[0][0]).toEqual(files[0]);
+    expect(onError.mock.calls[0][1]).toEqual(new Error('Something went wrong'));
+    expect(onError.mock.calls[0][2]).toBeDefined();
+  });
+
+  it('calls the onAbort prop with the current file and the abort event', () => {
+    const eventListener = jasmine.createSpy();
+
+    const frMock = {
+      onabort: jest.fn(e => e),
+      readAsDataURL: jest.fn(function() {
+        this.onabort({});
+      }),
+      addEventListener: eventListener,
+    };
+
+    spyOn(window, 'FileReader').and.returnValue(frMock);
+
+    const onAbort = jest.fn();
+    const fileUpload = renderIntoDocument(<FileUpload onAbort={onAbort} />);
+    const input = findRenderedDOMComponentWithTag(fileUpload, 'input');
+    const files = [new File()];
+
+    Simulate.change(input, { target: { files }});
+    expect(onAbort.mock.calls.length).toBe(1);
+    expect(onAbort.mock.calls[0][0]).toEqual(files[0]);
+    expect(onAbort.mock.calls[0][1]).toBeDefined();
+  });
+
+  it('calls the onLoadStart prop with the current file and the load start event', () => {
+    const eventListener = jasmine.createSpy();
+
+    const frMock = {
+      onloadstart: jest.fn(e => e),
+      readAsDataURL: jest.fn(function() {
+        this.onloadstart({});
+      }),
+      addEventListener: eventListener,
+    };
+
+    spyOn(window, 'FileReader').and.returnValue(frMock);
+
+    const onLoadStart = jest.fn();
+    const fileUpload = renderIntoDocument(<FileUpload onLoadStart={onLoadStart} />);
+    const input = findRenderedDOMComponentWithTag(fileUpload, 'input');
+    const files = [new File()];
+
+    Simulate.change(input, { target: { files }});
+    expect(onLoadStart.mock.calls.length).toBe(1);
+    expect(onLoadStart.mock.calls[0][0]).toEqual(files[0]);
+    expect(onLoadStart.mock.calls[0][1]).toBeDefined();
+  });
+
+  it('calls the onLoadEnd prop with the current file and the load end event', () => {
+    const eventListener = jasmine.createSpy();
+
+    const frMock = {
+      onloadend: jest.fn(e => e),
+      readAsDataURL: jest.fn(function() {
+        this.onloadend({});
+      }),
+      addEventListener: eventListener,
+    };
+
+    spyOn(window, 'FileReader').and.returnValue(frMock);
+
+    const onLoadEnd = jest.fn();
+    const fileUpload = renderIntoDocument(<FileUpload onLoadEnd={onLoadEnd} />);
+    const input = findRenderedDOMComponentWithTag(fileUpload, 'input');
+    const files = [new File()];
+
+    Simulate.change(input, { target: { files }});
+    expect(onLoadEnd.mock.calls.length).toBe(1);
+    expect(onLoadEnd.mock.calls[0][0]).toEqual(files[0]);
+    expect(onLoadEnd.mock.calls[0][1]).toBeDefined();
+  });
+
+  it('calls the onLoad function with the file, the load result, and the load event', () => {
+    const eventListener = jasmine.createSpy();
+
+    const result = 'data:image/png;base64;hfuasdhfjawf';
+    const frMock = {
+      onload: jest.fn(e => e),
+      readAsDataURL: jest.fn(function() {
+        this.onload(({ target: { result }}));
+      }),
+      addEventListener: eventListener,
+    };
+
+    spyOn(window, 'FileReader').and.returnValue(frMock);
+
+    const onLoad = jest.fn();
+    const fileUpload = renderIntoDocument(<FileUpload onLoad={onLoad} />);
+    const input = findRenderedDOMComponentWithTag(fileUpload, 'input');
+    const files = [new File()];
+
+    Simulate.change(input, { target: { files }});
+    expect(onLoad.mock.calls.length).toBe(1);
+    expect(onLoad.mock.calls[0][0]).toBe(files[0]);
+    expect(onLoad.mock.calls[0][1]).toBe(result);
+    expect(onLoad.mock.calls[0][2]).toBeDefined();
+  });
+
+  it('calls the onProgress function with the file, percentage complete, and the progress event', () => {
+    const eventListener = jasmine.createSpy();
+
+    const frMock = {
+      onprogress: jest.fn(e => e),
+      readAsDataURL: jest.fn(function(file) {
+        this.onprogress(({ lengthComputable: true, loaded: 22, total: file.size }));
+      }),
+      addEventListener: eventListener,
+    };
+
+    spyOn(window, 'FileReader').and.returnValue(frMock);
+
+    const onProgress = jest.fn();
+    const fileUpload = renderIntoDocument(<FileUpload onProgress={onProgress} />);
+    const input = findRenderedDOMComponentWithTag(fileUpload, 'input');
+    const files = [new File()];
+
+    Simulate.change(input, { target: { files }});
+    expect(onProgress.mock.calls.length).toBe(1);
+    expect(onProgress.mock.calls[0][0]).toBe(files[0]);
+    expect(onProgress.mock.calls[0][1]).toBe((22 / files[0].size) * 100);
+    expect(onProgress.mock.calls[0][2]).toBeDefined();
+  });
+
+  it('it matches image, video, and audio to the readAsDataURL FileReader function', () => {
+    const eventListener = jasmine.createSpy();
+
+    const frMock = {
+      readAsDataURL: jest.fn(),
+      readAsArrayBuffer: jest.fn(),
+      readAsText: jest.fn(),
+      addEventListener: eventListener,
+    };
+    spyOn(window, 'FileReader').and.returnValue(frMock);
+
+    const fileUpload = renderIntoDocument(<FileUpload />);
+    const input = findRenderedDOMComponentWithTag(fileUpload, 'input');
+    let files = [new File()];
+
+    Simulate.change(input, { target: { files }});
+
+    expect(frMock.readAsDataURL.mock.calls.length).toBe(1);
+    expect(frMock.readAsArrayBuffer.mock.calls.length).toBe(0);
+    expect(frMock.readAsText.mock.calls.length).toBe(0);
+
+    files = [new File(1024, 'video/mp4')];
+
+    Simulate.change(input, { target: { files }});
+
+    expect(frMock.readAsDataURL.mock.calls.length).toBe(2);
+    expect(frMock.readAsArrayBuffer.mock.calls.length).toBe(0);
+    expect(frMock.readAsText.mock.calls.length).toBe(0);
+
+    files = [new File(1024, 'audio/mp3')];
+
+    Simulate.change(input, { target: { files }});
+
+    expect(frMock.readAsDataURL.mock.calls.length).toBe(3);
+    expect(frMock.readAsArrayBuffer.mock.calls.length).toBe(0);
+    expect(frMock.readAsText.mock.calls.length).toBe(0);
+  });
+
+  it('matches application, model, and multipart to the readAsArrayBuffer FileReader function', () => {
+    const eventListener = jasmine.createSpy();
+
+    const frMock = {
+      readAsDataURL: jest.fn(),
+      readAsArrayBuffer: jest.fn(),
+      readAsText: jest.fn(),
+      addEventListener: eventListener,
+    };
+    spyOn(window, 'FileReader').and.returnValue(frMock);
+
+    const fileUpload = renderIntoDocument(<FileUpload />);
+    const input = findRenderedDOMComponentWithTag(fileUpload, 'input');
+    let files = [new File(1024, 'application/gzip')];
+
+    Simulate.change(input, { target: { files }});
+
+    expect(frMock.readAsDataURL.mock.calls.length).toBe(0);
+    expect(frMock.readAsArrayBuffer.mock.calls.length).toBe(1);
+    expect(frMock.readAsText.mock.calls.length).toBe(0);
+
+    // no idea what real model would be
+    files = [new File(1024, 'model/airoplaine')];
+
+    Simulate.change(input, { target: { files }});
+
+    expect(frMock.readAsDataURL.mock.calls.length).toBe(0);
+    expect(frMock.readAsArrayBuffer.mock.calls.length).toBe(2);
+    expect(frMock.readAsText.mock.calls.length).toBe(0);
+
+    // not sure what real multipart would be.
+    files = [new File(1024, 'multipart/form-data')];
+
+    Simulate.change(input, { target: { files }});
+
+    expect(frMock.readAsDataURL.mock.calls.length).toBe(0);
+    expect(frMock.readAsArrayBuffer.mock.calls.length).toBe(3);
+    expect(frMock.readAsText.mock.calls.length).toBe(0);
+  });
+
+  it('matches any remaining type to text', () => {
+    const eventListener = jasmine.createSpy();
+
+    const frMock = {
+      readAsDataURL: jest.fn(),
+      readAsArrayBuffer: jest.fn(),
+      readAsText: jest.fn(),
+      addEventListener: eventListener,
+    };
+    spyOn(window, 'FileReader').and.returnValue(frMock);
+
+    const fileUpload = renderIntoDocument(<FileUpload />);
+    const input = findRenderedDOMComponentWithTag(fileUpload, 'input');
+    let files = [new File(1024, 'text/x-java')];
+
+    Simulate.change(input, { target: { files }});
+
+    expect(frMock.readAsDataURL.mock.calls.length).toBe(0);
+    expect(frMock.readAsArrayBuffer.mock.calls.length).toBe(0);
+    expect(frMock.readAsText.mock.calls.length).toBe(1);
+
+    files = [new File(1024, '')];
+
+    Simulate.change(input, { target: { files }});
+
+    expect(frMock.readAsDataURL.mock.calls.length).toBe(0);
+    expect(frMock.readAsArrayBuffer.mock.calls.length).toBe(0);
+    expect(frMock.readAsText.mock.calls.length).toBe(2);
+  });
+
+  it('allows for a custom readAs string', () => {
+    const eventListener = jasmine.createSpy();
+
+    const frMock = {
+      readAsDataURL: jest.fn(),
+      readAsArrayBuffer: jest.fn(),
+      readAsText: jest.fn(),
+      addEventListener: eventListener,
+    };
+    spyOn(window, 'FileReader').and.returnValue(frMock);
+
+    let fileUpload = renderIntoDocument(<FileUpload readAs="DataURL" />);
+    let input = findRenderedDOMComponentWithTag(fileUpload, 'input');
+    const files = [new File(2024, 'application/javascript')];
+
+    Simulate.change(input, { target: { files }});
+    expect(frMock.readAsDataURL.mock.calls.length).toBe(1);
+    expect(frMock.readAsArrayBuffer.mock.calls.length).toBe(0);
+    expect(frMock.readAsText.mock.calls.length).toBe(0);
+
+    fileUpload = renderIntoDocument(<FileUpload readAs="ArrayBuffer" />);
+    input = findRenderedDOMComponentWithTag(fileUpload, 'input');
+
+    Simulate.change(input, { target: { files }});
+    expect(frMock.readAsDataURL.mock.calls.length).toBe(1);
+    expect(frMock.readAsArrayBuffer.mock.calls.length).toBe(1);
+    expect(frMock.readAsText.mock.calls.length).toBe(0);
+
+    fileUpload = renderIntoDocument(<FileUpload readAs="Text" />);
+    input = findRenderedDOMComponentWithTag(fileUpload, 'input');
+
+    Simulate.change(input, { target: { files }});
+    expect(frMock.readAsDataURL.mock.calls.length).toBe(1);
+    expect(frMock.readAsArrayBuffer.mock.calls.length).toBe(1);
+    expect(frMock.readAsText.mock.calls.length).toBe(1);
+  });
+
+  it('allows for a custom readAs function that accepts the file type, the file, and the current file reader', () => {
+    const eventListener = jasmine.createSpy();
+
+    const frMock = {
+      readAsDataURL: jest.fn(),
+      readAsArrayBuffer: jest.fn(),
+      readAsText: jest.fn(),
+      addEventListener: eventListener,
+    };
+    spyOn(window, 'FileReader').and.returnValue(frMock);
+
+    const readAs = jest.fn();
+    const fileUpload = renderIntoDocument(<FileUpload readAs={readAs} />);
+    const input = findRenderedDOMComponentWithTag(fileUpload, 'input');
+    const files = [new File()];
+
+    Simulate.change(input, { target: { files }});
+    expect(readAs.mock.calls.length).toBe(1);
+    expect(readAs.mock.calls[0][0]).toBe(files[0].type);
+    expect(readAs.mock.calls[0][1]).toEqual(files[0]);
+    expect(readAs.mock.calls[0][2]).toEqual(frMock);
+  });
+});
diff --git a/src/js/NavigationDrawers/__tests__/NavigationDrawer.js b/src/js/NavigationDrawers/__tests__/NavigationDrawer.js
index a766fdf17b..82fe667caa 100644
--- a/src/js/NavigationDrawers/__tests__/NavigationDrawer.js
+++ b/src/js/NavigationDrawers/__tests__/NavigationDrawer.js
@@ -1,5 +1,5 @@
 /*eslint-env jest*/
-jest.unmock('../_NavigationDrawer');
+jest.unmock('../NavigationDrawer');
 
 import React from 'react';
 import CSSTransitionGroup from 'react-addons-css-transition-group';
@@ -10,7 +10,7 @@ import {
   scryRenderedComponentsWithType,
 } from 'react-addons-test-utils';
 
-import NavigationDrawer from '../_NavigationDrawer';
+import NavigationDrawer from '../NavigationDrawer';
 import Drawer from '../Drawer';
 import DrawerToolbar from '../DrawerToolbar';
 import Overlay from '../../Transitions/Overlay';
@@ -39,7 +39,13 @@ describe('NavigationDrawer', () => {
     };
 
     let nav = renderIntoDocument(<NavigationDrawer {...props} />);
-    nav.setState({ mobile: false, tablet: false, desktop: true });
+    // since there is no window object for media queries
+    nav.setState({
+      mobile: false,
+      tablet: false,
+      desktop: true,
+      drawerType: 'full-height',
+    });
 
     let drawers = scryRenderedComponentsWithType(nav, Drawer);
     expect(drawers.length).toBe(1);
diff --git a/src/js/TextFields/__tests__/FloatingLabel.js b/src/js/TextFields/__tests__/FloatingLabel.js
new file mode 100644
index 0000000000..0fea584c58
--- /dev/null
+++ b/src/js/TextFields/__tests__/FloatingLabel.js
@@ -0,0 +1,71 @@
+/*eslint-env jest*/
+jest.unmock('../FloatingLabel');
+
+import React from 'react';
+import { findDOMNode } from 'react-dom';
+import { renderIntoDocument } from 'react-addons-test-utils';
+
+import FloatingLabel from '../FloatingLabel';
+
+describe('FloatingLabel', () => {
+  it('automatically updates the label to include a \'*\' when required', () => {
+    let props = {
+      active: false,
+      error: false,
+      label: 'Hello World',
+      required: true,
+    };
+
+    let label = renderIntoDocument(<FloatingLabel {...props} />);
+    let labelNode = findDOMNode(label);
+
+    expect(labelNode.textContent).toBe('Hello World *');
+
+    props = Object.assign({}, props, { label: 'Hello World *'});
+    label = renderIntoDocument(<FloatingLabel {...props} />);
+    labelNode = findDOMNode(label);
+    expect(labelNode.textContent).toBe('Hello World *');
+  });
+
+  it('adds the statefull classNames to the label', () => {
+    let props = {
+      active: false,
+      error: false,
+      label: 'Hello World',
+    };
+    let label = renderIntoDocument(<FloatingLabel {...props} />);
+    let className = findDOMNode(label).className;
+
+    expect(className).toContain('md-floating-label');
+    expect(className).not.toContain('error');
+    expect(className).not.toContain('focus');
+    expect(className).not.toContain('active');
+
+    props = Object.assign({}, props, { active: true });
+    label = renderIntoDocument(<FloatingLabel {...props} />);
+    className = findDOMNode(label).className;
+
+    expect(className).toContain('md-floating-label');
+    expect(className).not.toContain('error');
+    expect(className).toContain('focus');
+    expect(className).toContain('active');
+
+    props = Object.assign({}, props, { error: true });
+    label = renderIntoDocument(<FloatingLabel {...props} />);
+    className = findDOMNode(label).className;
+
+    expect(className).toContain('md-floating-label');
+    expect(className).toContain('error');
+    expect(className).toContain('focus');
+    expect(className).toContain('active');
+
+    props = Object.assign({}, props, { active: false, value: 'Hello!', error: false });
+    label = renderIntoDocument(<FloatingLabel {...props} />);
+    className = findDOMNode(label).className;
+
+    expect(className).toContain('md-floating-label');
+    expect(className).not.toContain('error');
+    expect(className).not.toContain('focus');
+    expect(className).toContain('active');
+  });
+});
diff --git a/src/js/TextFields/__tests__/TextDivider.js b/src/js/TextFields/__tests__/TextDivider.js
new file mode 100644
index 0000000000..c2d6782afb
--- /dev/null
+++ b/src/js/TextFields/__tests__/TextDivider.js
@@ -0,0 +1,39 @@
+/*eslint-env jest*/
+jest.unmock('../TextDivider');
+
+import React from 'react';
+import { findDOMNode } from 'react-dom';
+import { renderIntoDocument } from 'react-addons-test-utils';
+
+import TextDivider from '../TextDivider';
+
+describe('TextDivider', () => {
+  it('updates the className with the stateful classNames', () => {
+    let props = {
+      lineDirection: 'left',
+      active: false,
+      error: false,
+      icon: false,
+    };
+
+    let divider = renderIntoDocument(<TextDivider {...props} />);
+    let className = findDOMNode(divider).className;
+
+    expect(className).toContain('md-text-divider');
+    expect(className).toContain('from-left');
+    expect(className).not.toContain('active');
+    expect(className).not.toContain('error');
+    expect(className).not.toContain('icon-offset');
+
+    props = Object.assign({}, props, { active: true, error: true, icon: true });
+
+    divider = renderIntoDocument(<TextDivider {...props} />);
+    className = findDOMNode(divider).className;
+
+    expect(className).toContain('md-text-divider');
+    expect(className).toContain('from-left');
+    expect(className).toContain('active');
+    expect(className).toContain('error');
+    expect(className).toContain('icon-offset');
+  });
+});
diff --git a/src/js/TextFields/__tests__/TextField.js b/src/js/TextFields/__tests__/TextField.js
index 6f023903c2..dcdcf31a27 100644
--- a/src/js/TextFields/__tests__/TextField.js
+++ b/src/js/TextFields/__tests__/TextField.js
@@ -1,21 +1,22 @@
 /*eslint-env jest*/
 jest.unmock('../TextField');
-jest.unmock('../FloatingLabel');
-jest.unmock('../TextDivider');
-jest.unmock('../TextFieldMessage');
-jest.unmock('../../FontIcons/FontIcon');
 
 import React from 'react';
 import { findDOMNode } from 'react-dom';
 import {
   Simulate,
   renderIntoDocument,
+  findRenderedComponentWithType,
+  scryRenderedComponentsWithType,
   findRenderedDOMComponentWithTag,
   findRenderedDOMComponentWithClass,
   scryRenderedDOMComponentsWithClass,
 } from 'react-addons-test-utils';
 
 import TextField from '../TextField';
+import FloatingLabel from '../FloatingLabel';
+import TextDivider from '../TextDivider';
+import TextFieldMessage from '../TextFieldMessage';
 import FontIcon from '../../FontIcons/FontIcon';
 
 describe('TextField', () => {
@@ -138,21 +139,21 @@ describe('TextField', () => {
       />
     );
 
-    let icon = findRenderedDOMComponentWithClass(textField, 'md-icon');
-    let input = findRenderedDOMComponentWithTag(textField, 'input');
-    let divider = findRenderedDOMComponentWithClass(textField, 'md-text-divider');
-    let label = findRenderedDOMComponentWithClass(textField, 'md-floating-label');
+    const input = findRenderedDOMComponentWithTag(textField, 'input');
+    let icon = findRenderedComponentWithType(textField, FontIcon);
+    let divider = findRenderedComponentWithType(textField, TextDivider);
+    let label = findRenderedComponentWithType(textField, FloatingLabel);
 
-    expect(icon.classList.contains('active')).toBe(false);
-    expect(input.classList.contains('active')).toBe(false);
-    expect(divider.classList.contains('active')).toBe(false);
-    expect(label.classList.contains('active')).toBe(false);
+    expect(input.className).not.toContain('active');
+    expect(icon.props.className).not.toContain('active');
+    expect(divider.props.active).toBe(false);
+    expect(label.props.active).toBe(false);
 
     Simulate.focus(input);
-    expect(icon.classList.contains('active')).toBe(true);
-    expect(input.classList.contains('active')).toBe(true);
-    expect(divider.classList.contains('active')).toBe(true);
-    expect(label.classList.contains('active')).toBe(true);
+    expect(input.className).toContain('active');
+    expect(icon.props.className).toContain('active');
+    expect(divider.props.active).toBe(true);
+    expect(label.props.active).toBe(true);
   });
 
   it('uses the placeholder prop as the placeholder and falls back to the label prop.', () => {
@@ -164,7 +165,7 @@ describe('TextField', () => {
     );
 
     let input = findRenderedDOMComponentWithTag(textField, 'input');
-    const floatingLabels = scryRenderedDOMComponentsWithClass(textField, 'md-floating-label');
+    const floatingLabels = scryRenderedComponentsWithType(textField, FloatingLabel);
     expect(floatingLabels.length).toBe(0);
 
     expect(input.getAttribute('placeholder')).toBe('Test');
@@ -191,71 +192,209 @@ describe('TextField', () => {
     );
 
     const input = findRenderedDOMComponentWithTag(textField, 'input');
-    const label = findRenderedDOMComponentWithClass(textField, 'md-floating-label');
+    const label = findRenderedComponentWithType(textField, FloatingLabel);
     expect(input.getAttribute('placeholder')).toBe('Placeholder');
-    expect(label.textContent).toBe('Label');
+    expect(label.props.label).toBe('Label');
   });
 
-  it('adds a counter if the maxLength prop is set to a number', () => {
-    let textField = renderIntoDocument(
-      <TextField
-        label="Test"
-        maxLength={120}
-      />
-    );
+  it('renders the FloatingLabel component with correct props', () => {
+    let props = { label: 'Hello' };
+    let textField = renderIntoDocument(<TextField {...props} />);
 
-    let message = findRenderedDOMComponentWithClass(textField, 'md-text-field-message');
-    expect(message.classList.contains('count-only')).toBe(true);
-    expect(message.textContent).toBe('0 / 120');
+    let labels = scryRenderedComponentsWithType(textField, FloatingLabel);
+    expect(labels.length).toBe(1);
 
-    textField = renderIntoDocument(
-      <TextField
-        label="Test"
-        maxLength={120}
-        value="Something something"
-      />
-    );
+    expect(labels[0].props.label).toBe(props.label);
+    expect(labels[0].props.active).toBe(false);
+    expect(labels[0].props.error).toBe(false);
+    expect(labels[0].props.required).toBeUndefined();
+    expect(labels[0].props.value).toBe('');
 
-    message = findRenderedDOMComponentWithClass(textField, 'md-text-field-message');
-    expect(message.textContent).toBe('19 / 120');
+    Simulate.focus(findRenderedDOMComponentWithTag(textField, 'input'));
 
-    const charString130 = Array.apply(null, new Array(131)).join('A');
-    expect(charString130.length).toBe(130);
-    textField = renderIntoDocument(
-      <TextField
-        label="Test"
-        value={charString130}
-        maxLength={120}
-      />
-    );
+    expect(labels[0].props.active).toBe(true);
 
-    message = findRenderedDOMComponentWithClass(textField, 'md-text-field-message');
-    expect(message.textContent).toBe('130 / 120');
+    props = Object.assign({}, props, { floatingLabel: false });
+    textField = renderIntoDocument(<TextField {...props} />);
+    labels = scryRenderedComponentsWithType(textField, FloatingLabel);
+    expect(labels.length).toBe(0);
   });
 
-  it('allows for a help text to be displayed with the text field', () => {
-    const textField = renderIntoDocument(
-      <TextField
-        label="Test"
-        helpText="Some amazing help text"
-      />
-    );
+  it('renders the TextDivider component when it is not displayed as a block', () => {
+    let textField = renderIntoDocument(<TextField block={false} />);
+
+    let dividers = scryRenderedComponentsWithType(textField, TextDivider);
+    expect(dividers.length).toBe(1);
+    expect(dividers[0].props.icon).toBe(false);
+    expect(dividers[0].props.active).toBe(false);
+    expect(dividers[0].props.lineDirection).toBe(TextField.defaultProps.lineDirection);
+
+    Simulate.focus(findRenderedDOMComponentWithTag(textField, 'input'));
 
-    const message = findRenderedDOMComponentWithClass(textField, 'md-text-field-message');
-    expect(message.textContent).toBe('Some amazing help text');
+    expect(dividers[0].props.active).toBe(true);
+
+    textField = renderIntoDocument(<TextField block={true} />);
+
+    dividers = scryRenderedComponentsWithType(textField, TextDivider);
+    expect(dividers.length).toBe(0);
   });
 
-  it('allows for help text and maxLength in the message', () => {
-    const textField = renderIntoDocument(
-      <TextField
-        label="Test"
-        helpText="Some amazing help text"
-        maxLength={120}
-      />
-    );
+  it('renders an optional icon component with additional stateful classNames', () => {
+    let props = {
+      icon: <FontIcon>wht</FontIcon>,
+      floatingLabel: true,
+    };
+
+    let textField = renderIntoDocument(<TextField {...props} />);
+    let icon = findRenderedComponentWithType(textField, FontIcon);
+    expect(icon.props.className).toContain('md-text-field-icon');
+    expect(icon.props.className).toContain('with-floating-label');
+
+    Simulate.focus(findRenderedDOMComponentWithTag(textField, 'input'));
+    expect(icon.props.className).toContain('active');
+
+    textField.setState({ value: 'something' });
+    expect(icon.props.className).toContain('normal');
+
+    props = Object.assign({}, props, { errorText: 'Some error', floatingLabel: false });
+    textField = renderIntoDocument(<TextField {...props} />);
+    icon = findRenderedComponentWithType(textField, FontIcon);
+    expect(icon.props.className).toContain('error');
+    expect(icon.props.className).not.toContain('with-floating-label');
+  });
+
+  it('renders an optional right icon with additional classNames', () => {
+    let props = {
+      rightIcon: <FontIcon>wht</FontIcon>,
+      floatingLabel: true,
+    };
+
+    let textField = renderIntoDocument(<TextField {...props} />);
+    let icon = findRenderedComponentWithType(textField, FontIcon);
+    expect(icon.props.className).toContain('md-text-field-ind');
+    expect(icon.props.className).not.toContain('single-line');
+
+    props = Object.assign({}, props, { floatingLabel: false });
+    textField = renderIntoDocument(<TextField {...props} />);
+    icon = findRenderedComponentWithType(textField, FontIcon);
+    expect(icon.props.className).toContain('md-text-field-ind');
+    expect(icon.props.className).toContain('single-line');
+  });
+
+  it('renders the TextFieldMessage component if there is a maxLength prop, an errorText prop, or helpText prop', () => {
+    let props = {
+      label: 'What',
+    };
+
+    let textField = renderIntoDocument(<TextField {...props} />);
+    let messages = scryRenderedComponentsWithType(textField, TextFieldMessage);
+    expect(messages.length).toBe(0);
+
+    props = Object.assign({}, props, { errorText: 'Some error' });
+    textField = renderIntoDocument(<TextField {...props} />);
+    messages = scryRenderedComponentsWithType(textField, TextFieldMessage);
+    expect(messages.length).toBe(1);
+
+    props = Object.assign({}, props, { errorText: null, helpText: 'What' });
+    textField = renderIntoDocument(<TextField {...props} />);
+    messages = scryRenderedComponentsWithType(textField, TextFieldMessage);
+    expect(messages.length).toBe(1);
+
+    props = Object.assign({}, props, { helpText: null, maxLength: 30 });
+    textField = renderIntoDocument(<TextField {...props} />);
+    messages = scryRenderedComponentsWithType(textField, TextFieldMessage);
+    expect(messages.length).toBe(1);
+  });
+
+  it('renders the TextFieldMessage component with the correct props for an error message', () => {
+    const props = {
+      label: 'Label',
+      errorText: 'Some error',
+      helpOnFocus: false,
+    };
+
+    const textField = renderIntoDocument(<TextField {...props} />);
+    const message = findRenderedComponentWithType(textField, TextFieldMessage);
+    expect(message.props.value).toBe('');
+    expect(message.props.error).toBe(true);
+    expect(message.props.helpOnFocus).toBe(false);
+    expect(message.props.active).toBe(false);
+    expect(message.props.message).toBe(props.errorText);
+    expect(message.props.maxLength).toBeUndefined();
+  });
+
+  it('renders the TextFieldMessage component with the correct props for a help message', () => {
+    const props = {
+      label: 'Label',
+      helpText: 'Some help',
+      helpOnFocus: false,
+    };
+
+    const textField = renderIntoDocument(<TextField {...props} />);
+    const message = findRenderedComponentWithType(textField, TextFieldMessage);
+    expect(message.props.value).toBe('');
+    expect(message.props.error).toBe(false);
+    expect(message.props.helpOnFocus).toBe(false);
+    expect(message.props.active).toBe(false);
+    expect(message.props.message).toBe(props.helpText);
+    expect(message.props.maxLength).toBeUndefined();
+  });
+
+  it('renders the TextFieldMessage component with the correct props for a counter message', () => {
+    const props = {
+      label: 'Label',
+      maxLength: 30,
+      helpOnFocus: false,
+    };
+
+    const textField = renderIntoDocument(<TextField {...props} />);
+    const message = findRenderedComponentWithType(textField, TextFieldMessage);
+    expect(message.props.value).toBe('');
+    expect(message.props.error).toBe(false);
+    expect(message.props.helpOnFocus).toBe(false);
+    expect(message.props.active).toBe(false);
+    expect(message.props.message).toBeUndefined();
+    expect(message.props.maxLength).toBe(props.maxLength);
+  });
+
+  it('if both an error and help text are given, the error message will be displayed', () => {
+    const props = {
+      label: 'Label',
+      helpText: 'Something to help',
+      errorText: 'Oh noooop',
+      helpOnFocus: false,
+    };
+
+    const textField = renderIntoDocument(<TextField {...props} />);
+    const message = findRenderedComponentWithType(textField, TextFieldMessage);
+    expect(message.props.value).toBe('');
+    expect(message.props.error).toBe(true);
+    expect(message.props.helpOnFocus).toBe(false);
+    expect(message.props.active).toBe(false);
+    expect(message.props.message).toBe(props.errorText);
+    expect(message.props.maxLength).toBeUndefined();
+  });
+
+  it('renders a password btn when the type is password', () => {
+    let textField = renderIntoDocument(<TextField type="password" />);
+    let btns = scryRenderedDOMComponentsWithClass(textField, 'md-password-btn');
+    expect(btns.length).toBe(1);
+
+    textField = renderIntoDocument(<TextField type="text" />);
+    btns = scryRenderedDOMComponentsWithClass(textField, 'md-password-btn');
+    expect(btns.length).toBe(0);
+  });
+
+  it('toggles the password\'s visible when the password button is clicked by switching the type to text', () => {
+    const textField = renderIntoDocument(<TextField type="password" />);
+    const btn = findRenderedDOMComponentWithClass(textField, 'md-password-btn');
+    const input = findRenderedDOMComponentWithTag(textField, 'input');
+
+    expect(input.getAttribute('type')).toBe('password');
+    expect(btn.className).not.toContain('active');
 
-    const message = findRenderedDOMComponentWithClass(textField, 'md-text-field-message');
-    expect(message.textContent).toBe('Some amazing help text0 / 120');
-    expect(message.classList.contains('count-only')).toBe(false);
+    Simulate.click(btn);
+    expect(input.getAttribute('type')).toBe('text');
+    expect(btn.className).toContain('active');
   });
 });
diff --git a/src/js/TextFields/__tests__/TextFieldMessage.js b/src/js/TextFields/__tests__/TextFieldMessage.js
new file mode 100644
index 0000000000..8620ab9b1a
--- /dev/null
+++ b/src/js/TextFields/__tests__/TextFieldMessage.js
@@ -0,0 +1,118 @@
+/*eslint-env jest*/
+jest.unmock('../TextFieldMessage');
+
+import React from 'react';
+import { findDOMNode } from 'react-dom';
+import {
+  renderIntoDocument,
+  findRenderedDOMComponentWithClass,
+  scryRenderedDOMComponentsWithClass,
+} from 'react-addons-test-utils';
+
+import TextFieldMessage from '../TextFieldMessage';
+
+describe('TextFieldMessage', () => {
+  it('merges the className', () => {
+    const message = renderIntoDocument(
+      <TextFieldMessage
+        className="test-class-name"
+        error={false}
+        active={false}
+        value=""
+      />
+    );
+
+    const className = findDOMNode(message).className;
+    expect(className).toContain('md-text-field-message');
+    expect(className).toContain('test-class-name');
+  });
+
+  it('renders a count when the maxLength prop is given', () => {
+    let props = {
+      maxLength: 30,
+      error: false,
+      active: false,
+      value: '',
+    };
+
+    let message = renderIntoDocument(<TextFieldMessage {...props} />);
+    let counters = scryRenderedDOMComponentsWithClass(message, 'md-text-field-counter');
+    expect(counters.length).toBe(1);
+
+    props = Object.assign({}, props, { maxLength: null });
+    message = renderIntoDocument(<TextFieldMessage {...props} />);
+    counters = scryRenderedDOMComponentsWithClass(message, 'md-text-field-counter');
+    expect(counters.length).toBe(0);
+  });
+
+  it('updates the counter based on the value\'s length', () => {
+    let props = {
+      maxLength: 30,
+      error: false,
+      active: false,
+      value: '',
+    };
+
+    let message = renderIntoDocument(<TextFieldMessage {...props} />);
+    let counter = findRenderedDOMComponentWithClass(message, 'md-text-field-counter');
+    expect(counter.textContent).toBe('0 / 30');
+
+    props = Object.assign({}, props, { value: 'Wow! I can\'t believe this is a value.' });
+
+    message = renderIntoDocument(<TextFieldMessage {...props} />);
+    counter = findRenderedDOMComponentWithClass(message, 'md-text-field-counter');
+    expect(counter.textContent).toBe(props.value.length + ' / 30');
+  });
+
+  it('renders a message', () => {
+    const props = {
+      error: false,
+      active: false,
+      value: '',
+      message: 'Lorem Ipsum',
+    };
+
+    const message = renderIntoDocument(<TextFieldMessage {...props} />);
+    const messageNode = findDOMNode(message);
+    expect(messageNode.textContent).toBe(props.message);
+  });
+
+  it('can render a message only on focus', () => {
+    let props = {
+      error: false,
+      active: false,
+      value: '',
+      message: 'Lorem Ipsum',
+      helpOnFocus: true,
+    };
+
+    let message = renderIntoDocument(<TextFieldMessage {...props} />);
+    let messageNode = findDOMNode(message);
+
+    expect(messageNode.textContent).toBe('');
+
+
+    props = Object.assign({}, props, { active: true });
+    message = renderIntoDocument(<TextFieldMessage {...props} />);
+    messageNode = findDOMNode(message);
+
+    expect(messageNode.textContent).toBe(props.message);
+  });
+
+  it('can render help text and a counter', () => {
+    const props = {
+      error: false,
+      active: false,
+      value: '',
+      message: 'Lorem Ipsum',
+      maxLength: 30,
+    };
+
+    const message = renderIntoDocument(<TextFieldMessage {...props} />);
+    const nodes = findDOMNode(message).childNodes;
+    expect(nodes.length).toBe(2);
+    expect(nodes[0].textContent).toBe(props.message);
+
+    expect(nodes[1].textContent).toBe('0 / 30');
+  });
+});