-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Input component which supports a label, error message, optional bool and helper text. Should provide the basis for all forms
- Loading branch information
Richard Palmer
committed
Oct 20, 2016
1 parent
93dfdd6
commit b5a830f
Showing
6 changed files
with
392 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<div className={ cx(css.root, className) }> | ||
<label htmlFor={ this.id } className={ labelClasses }> | ||
{ label } { optional && ( | ||
<span className={ css.optional }> | ||
{ optionalLabel } | ||
</span> | ||
) } | ||
</label> | ||
<input | ||
{ ...rest } | ||
id={ this.id } | ||
ref={ (c) => { this.input = c; } } | ||
className={ inputClasses } | ||
onFocus={ this.handleFocus } | ||
onBlur={ this.handleBlur } | ||
onChange={ this.handleChange } | ||
value={ value } | ||
aria-describedby={ describedBy } | ||
placeholder={ placeholder } | ||
required={ !optional } | ||
name={ name } | ||
type={ type } | ||
/> | ||
<div id={ describedBy } className={ css.helperContainer }> | ||
{ description && ( | ||
<div | ||
className={ descriptionClasses } | ||
id={ describedBy } | ||
> | ||
{ description } | ||
</div> | ||
) } | ||
<CSSTransitionGroup | ||
transitionName={ css } | ||
transitionEnterTimeout={ 500 } | ||
transitionLeaveTimeout={ 300 } | ||
transitionAppearTimeout={ 500 } | ||
transitionAppear={ true } | ||
> | ||
{ error.length > 0 && ( | ||
<div | ||
className={ css.error } | ||
> | ||
{ error } | ||
</div> | ||
) } | ||
</CSSTransitionGroup> | ||
</div> | ||
</div> | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import React, { Component } from 'react'; | ||
import { storiesOf, action } from '@kadira/storybook'; | ||
import Input from './Input'; | ||
|
||
class StateManager extends Component { | ||
state = { | ||
value: '', | ||
error: '', | ||
}; | ||
|
||
handleChange = (e, value) => { | ||
this.setState({ | ||
value, | ||
error: value === 'error' ? 'error message' : '', | ||
}); | ||
} | ||
|
||
render() { | ||
return React.cloneElement(this.props.children, { | ||
onChange: this.handleChange, | ||
value: this.state.value, | ||
error: this.state.error, | ||
}); | ||
} | ||
} | ||
|
||
storiesOf('Input', module) | ||
.add('base', () => ( | ||
<Input | ||
label="Name" | ||
onChange={ action('Change') } | ||
/> | ||
)).add('optional', () => ( | ||
<Input | ||
label="Name" | ||
onChange={ action('Change') } | ||
optional | ||
/> | ||
)).add('with description', () => ( | ||
<Input | ||
label="Name" | ||
description="What shall we call you?" | ||
onChange={ action('Change') } | ||
value=" " | ||
/> | ||
)).add('with custom optional label', () => ( | ||
<Input | ||
label="Name" | ||
optionalLabel="optionnel" | ||
onChange={ action('Change') } | ||
optional | ||
/> | ||
)).add('with error', () => ( | ||
<Input | ||
label="Name" | ||
error="Uh oh, something went wrong" | ||
onChange={ action('Change') } | ||
/> | ||
)).add('with placeholder', () => ( | ||
<Input | ||
label="Name" | ||
placeholder="First and last name" | ||
onChange={ action('Change') } | ||
/> | ||
)).add('with everything', () => ( | ||
<StateManager> | ||
<Input | ||
label="Name" | ||
placeholder="First and last name" | ||
optionalLabel="optionnel" | ||
description="Type 'error' to generate an error message" | ||
optional | ||
/> | ||
</StateManager> | ||
)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import React from 'react'; | ||
import ReactDOM, { findDOMNode } from 'react-dom'; | ||
|
||
import Input from './Input'; | ||
|
||
it('renders without crashing', () => { | ||
const div = document.createElement('div'); | ||
ReactDOM.render( | ||
<Input | ||
label="" | ||
/>, | ||
div | ||
); | ||
}); | ||
|
||
it('handles external focusing and blurring', () => { | ||
const div = document.createElement('div'); | ||
let component; | ||
|
||
ReactDOM.render( | ||
<Input | ||
ref={ (c) => { component = c; }} | ||
label="" | ||
/>, | ||
div | ||
); | ||
|
||
const input = findDOMNode(component).querySelector('input'); | ||
component.focus(); | ||
|
||
expect(input === document.activeElement).toBe(true); | ||
expect(component.state.hasFocus).toBe(true); | ||
|
||
component.blur(); | ||
expect(input === document.activeElement).toBe(false); | ||
expect(component.state.hasFocus).toBe(false); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
:root { | ||
--animation-sharp: cubic-bezier(0,1,0.75,1); | ||
} |
Oops, something went wrong.