diff --git a/components/Forms/Input.css b/components/Forms/Input.css new file mode 100644 index 000000000..58a30394e --- /dev/null +++ b/components/Forms/Input.css @@ -0,0 +1,103 @@ +.root { + position: relative; +} + +.root * { + box-sizing: border-box; +} + +.input { + border: 1px solid var(--color-greyLighter); + padding: var(--size-small); + font: var(--font-base); + letter-spacing: 0.2px; + color: var(--color-greyDarker); + transition: border 300ms var(--animation-sharp), color 300ms var(--animation-sharp); + width: 100%; +} + +.inputFocused { + border-color: var(--color-greyDarker); + color: var(--color-greyDarker); + outline: none; +} + +.inputErrored { + border-color: var(--color-danger); + color: var(--color-danger); +} + +.label { + font: var(--font-small); + margin-bottom: var(--size-small); + border: 1px solid transparent; + display: block; + transition: color 300ms var(--animation-sharp); +} + +.labelErrored { + color: var(--color-danger); +} + +.optional { + font: var(--font-micro); + color: var(--color-grey); + margin-left: var(--size-small); +} + +.helperContainer { + position: relative; + z-index: -1; +} + +.description { + position: relative; + width: 100%; + padding-top: var(--size-small); + padding-bottom: var(--size-small); + background-color: transparent; + color: var(--color-grey); + font: var(--font-micro); + transform: translateY(-100%); + opacity: 0; + transition: transform 300ms var(--animation-sharp), opacity 500ms var(--animation-sharp); +} + +.descriptionFocused { + transform: translateY(0); + opacity: 1; +} + +.error { + position: absolute; + top: 0; + width: 100%; + padding: var(--size-small); + background-color: var(--color-danger); + color: var(--color-white); + font: var(--font-micro); + transition: transform 300ms var(--animation-sharp), opacity 300ms var(--animation-sharp); +} + +/* Error transitions */ +.enter, +.appear { + transform: translateY(-100%); + opacity: 0; +} + +.enterActive, +.appearActive { + transform: translateY(0); + opacity: 1; +} + +.leave { + transform: translateY(0); + opacity: 1; +} + +.leaveActive { + transform: translateY(-100%); + opacity: 0; +} diff --git a/components/Forms/Input.js b/components/Forms/Input.js new file mode 100644 index 000000000..80632e2be --- /dev/null +++ b/components/Forms/Input.js @@ -0,0 +1,172 @@ +import React, { Component, PropTypes } from 'react'; +import { findDOMNode } from 'react-dom'; +import CSSTransitionGroup from 'react-addons-css-transition-group'; +import cx from 'classnames'; +import uniqueId from 'lodash/fp/uniqueId'; + +import m from '../../globals/modifiers.css'; + +import css from './Input.css'; + +export default class Input extends Component { + static propTypes = { + label: PropTypes.node.isRequired, + optionalLabel: PropTypes.string, + onChange: PropTypes.func, + onFocus: PropTypes.func, + onBlur: PropTypes.func, + optional: PropTypes.bool, + error: PropTypes.string, + placeholder: PropTypes.string, + name: PropTypes.string, + /** + * Subset of the HTML5 spec, as other types will most likely have their + * own, bespoke component + */ + type: PropTypes.oneOf([ + 'text', + 'email', + 'password', + 'search', + 'url', + ]), + }; + + static defaultProps = { + onChange: () => {}, + onFocus: () => {}, + onBlur: () => {}, + optional: false, + optionalLabel: 'optional', + type: 'text', + error: '', + value: '', + }; + + constructor(props) { + super(props); + + this.id = uniqueId('forminput'); + } + + state = { + hasFocus: false, + }; + + focus = () => { + findDOMNode(this.input).focus(); + this.handleFocus(); + }; + + blur = () => { + findDOMNode(this.input).blur(); + this.handleBlur(); + }; + + handleFocus = () => { + const { onFocus } = this.props; + this.setState({ + hasFocus: true, + }, onFocus); + }; + + handleBlur = () => { + const { onBlur } = this.props; + this.setState({ + hasFocus: false, + }, onBlur); + }; + + handleChange = (e) => { + const { onChange } = this.props; + onChange(e, e.target.value); + }; + + render() { + const { hasFocus } = this.state; + const { + label, + value, + description, + optional, + optionalLabel, + error, + placeholder, + name, + type, + className, + ...rest, + } = this.props; + + const labelClasses = cx( + css.label, + hasFocus || value.length > 0 ? css.labelFocused : null, + error ? css.labelErrored : null, + ); + + const inputClasses = cx( + css.input, + hasFocus ? css.inputFocused : null, + error ? css.inputErrored : null, + ); + + const descriptionClasses = cx( + css.description, + hasFocus || value.length > 0 ? css.descriptionFocused : null, + ); + + const describedBy = `${this.id}-description`; + + return ( +