src/components/Forms/TextField.js
import React from 'react'
import PropTypes from 'prop-types'
import Radium from '@instacart/radium'
import { colors } from '../../styles'
import FormComponent from './FormComponent'
import ValidationError from './ValidationError'
import FloatingLabel from './FloatingLabel'
import TextFieldHint from './TextFieldHint'
import ServerError from './ServerError'
import HelperText from './HelperText'
import withTheme from '../../styles/themer/withTheme'
import { themePropTypes } from '../../styles/themer/utils'
import spacing from '../../styles/spacing'
const styles = {
wrapper: {
cursor: 'auto',
display: 'inline-block',
position: 'relative',
width: '343px',
},
inputContainer: {
borderRadius: '4px',
position: 'relative',
},
input: {
backgroundColor: '#FFF',
border: `solid 1px ${colors.GRAY_74}`,
borderRadius: '4px',
boxSizing: 'border-box',
color: colors.GRAY_20,
fontSize: '16px',
height: '56px',
marginTop: 0,
marginRight: 0,
marginBottom: 0,
marginLeft: 0,
paddingTop: '25px',
...spacing.PADDING_X_XS,
paddingBottom: spacing.XS,
position: 'relative',
width: '100%',
WebkitOpacity: 1,
WebkitTapHighlightColor: 'rgba(0,0,0,0)',
},
inputDisabled: {
border: `1px dashed ${colors.GRAY_74}`,
backgroundColor: colors.GRAY_93,
color: colors.GRAY_46,
cursor: 'not-allowed',
},
inputError: {
border: `1px solid ${colors.RED_700}`,
backgroundColor: '#FDE6EB',
},
fullWidth: {
width: '100%',
},
halfWidth: {
width: '162px',
},
}
const getSnackStyles = snacksTheme => {
const { action } = snacksTheme.colors
return {
highlight: {
border: `1px solid ${action}`,
},
}
}
@withTheme({ forwardRef: true })
@FormComponent
@Radium
class TextField extends React.Component {
static propTypes = {
/** Name of the field */
name: PropTypes.string.isRequired,
/** HTML autocomplete attribute */
autoComplete: PropTypes.string,
/** DefaultValue for non controlled component */
defaultValue: PropTypes.any,
/** Disable the text field */
disabled: PropTypes.bool,
/** Text of label that will animate when TextField is focused */
floatingLabelText: PropTypes.string,
/** Sets width to 100% */
fullWidth: PropTypes.bool,
/** Sets width to 162px */
halfWidth: PropTypes.bool,
/** FormComponent error for validation */
hasError: PropTypes.bool,
/** Helper text will show up in bottom right corner below TextField */
helperText: PropTypes.string,
/** Hint text will show up when input is focused and there is no value */
hintText: PropTypes.string,
/** Uniq id for input */
id: PropTypes.string,
/** Style for input */
inputStyle: PropTypes.object,
/** Style for input label */
labelStyle: PropTypes.object,
/** Set by FormComponent by default. */
isValid: PropTypes.bool,
/** onFocus callback */
onFocus: PropTypes.func,
/** onChange callback */
onChange: PropTypes.func,
/** onBlur callback */
onBlur: PropTypes.func,
/** onKeyDown callback */
onKeyDown: PropTypes.func,
/** Mark the field as required. */
required: PropTypes.bool,
/** Error from server to show ServerError message */
serverError: PropTypes.string,
/** Wrapper styles */
style: PropTypes.object,
/** Input type ie. 'text', 'email', password, etc.. */
type: PropTypes.string,
/** Text to show for validation error */
validationErrorText: PropTypes.string,
/** Value will make TextField a controlled component */
value: PropTypes.string,
/** Snacks theme attributes provided by `Themer` */
snacksTheme: themePropTypes,
/** Any additonal props to add to the element (e.g. data attributes) */
elementAttributes: PropTypes.object,
}
static defaultProps = {
autoComplete: 'on',
disabled: false,
type: 'text',
defaultValue: null,
onKeyDown: () => {}, // eslint-disable-line no-empty-function
}
state = {
hasValue: this.props.defaultValue !== null || Boolean(this.props.value),
}
componentWillReceiveProps(nextProps) {
if (nextProps.disabled && !this.props.disabled) {
this.setState({ isFocused: false })
}
if (!this.state.hasValue && nextProps.value) {
this.setState({ hasValue: true })
}
}
getValue = () => {
if (!this.input) {
return null
}
return this.input.value
}
handleInputChange = e => {
const { onChange } = this.props
const { hasValue } = this.state
const { value } = e.target
// Limit setState call to only when hasValue changes
if (value && !hasValue) {
this.setState({ hasValue: true })
} else if (!value && hasValue) {
this.setState({ hasValue: false })
}
onChange && onChange(e, value)
}
handleInputFocus = e => {
this.setState({ isFocused: true })
this.props.onFocus && this.props.onFocus(e)
}
handleInputBlur = e => {
this.setState({ isFocused: false })
this.props.onBlur && this.props.onBlur(e)
}
handleKeyDown = e => {
this.props.onKeyDown(e)
}
triggerFocus = () => this.input.focus()
render() {
const {
floatingLabelText,
defaultValue,
disabled,
fullWidth,
halfWidth,
hasError,
hintText,
id,
inputStyle,
labelStyle,
isValid,
name,
required,
serverError,
type,
validationErrorText,
style,
value,
helperText,
autoComplete,
snacksTheme,
elementAttributes,
} = this.props
const { hasValue, isFocused } = this.state
const snacksStyles = getSnackStyles(snacksTheme)
const inputId = id
const showHintText = hintText && !hasValue && isFocused
return (
<div
style={[
styles.wrapper,
fullWidth && styles.fullWidth,
halfWidth && styles.halfWidth,
style,
]}
>
{serverError && !disabled && !isValid && <ServerError text={serverError} />}
<div style={styles.inputContainer}>
{floatingLabelText && (
<FloatingLabel
text={floatingLabelText}
float={isFocused || hasValue}
disabled={disabled}
isActive={isFocused}
hasError={hasError}
htmlFor={inputId}
snacksTheme={snacksTheme}
style={labelStyle}
/>
)}
{hintText && (
<TextFieldHint
inputId={`hint_${inputId}`}
text={hintText}
show={showHintText}
disabled={disabled}
/>
)}
<input
value={value}
id={inputId}
ref={node => {
this.input = node
}}
defaultValue={value !== undefined ? undefined : defaultValue}
disabled={disabled}
name={name}
type={type}
aria-required={required}
aria-invalid={hasError}
aria-describedby={[
hasError ? `error_${inputId}` : null,
hintText ? `hint_${inputId}` : null,
]
.filter(Boolean)
.join(' ')}
style={[
styles.input,
inputStyle,
disabled && styles.inputDisabled,
!disabled && hasError && styles.inputError,
isFocused && !hasError && snacksStyles.highlight,
]}
onBlur={this.handleInputBlur}
onChange={this.handleInputChange}
onFocus={this.handleInputFocus}
onKeyDown={this.handleKeyDown}
autoComplete={autoComplete}
placeholder=""
{...elementAttributes}
/>
</div>
<ValidationError
text={validationErrorText}
show={!disabled && !isValid && !serverError}
inputId={inputId}
/>
<HelperText helperText={helperText} />
</div>
)
}
}
export default TextField