src/components/Form/Textarea/Textarea.jsx

Summary

Maintainability
A
55 mins
Test Coverage
import React from 'react'
import ValidationElement from '../ValidationElement'
import { ariaLabel } from '../Generic'

export default class Textarea extends ValidationElement {
  constructor(props) {
    super(props)

    this.state = {
      uid: `${this.props.name}-${super.guid()}`,
      focus: props.focus,
      error: props.error,
      valid: props.valid,
    }
  }

  /**
   * Handle the change event.
   */
  handleChange(event) {
    event.persist()
    super.handleChange(event)
    this.props.onUpdate({
      name: this.props.name,
      value: event.target.value,
    })
  }

  /**
   * Handle the focus event.
   */
  handleFocus(event) {
    event.persist()
    this.setState({ focus: true }, () => {
      super.handleFocus(event)
    })
  }

  /**
   * Handle the blur event.
   */
  handleBlur(event) {
    event.persist()
    this.setState({ focus: false }, () => {
      super.handleBlur(event)
    })
  }

  /**
   * Execute validation checks on the value.
   */
  handleValidation(event) {
    const value = `${this.props.value}`.trim()
    const errors =
      this.props.onError(
        value,
        this.constructor.errors.map(err => {
          return {
            code: err.code,
            valid: err.func(value, this.props),
            uid: this.state.uid
          }
        })
      ) || []

    this.setState({
      error: errors.some(x => x.valid === false),
      valid: errors.every(x => x.valid === true)
    })
  }

  /**
   * Generated name for the error message.
   */
  errorName() {
    return '' + this.props.name + '-error'
  }

  /**
   * Style classes applied to the wrapper.
   */
  divClass() {
    let klass = this.props.className || ''

    if (this.state.error) {
      klass += ' usa-input-error'
    }

    return klass.trim()
  }

  /**
   * Style classes applied to the label element.
   */
  labelClass() {
    let klass = ''

    if (this.state.error) {
      klass += ' usa-input-error-label'
    }

    return klass.trim()
  }

  /**
   * Style classes applied to the input element.
   */
  inputClass() {
    let klass = ''

    if (this.state.focus) {
      klass += ' usa-input-focus'
    }

    if (this.state.valid) {
      klass += ' usa-input-success'
    }

    return klass.trim()
  }

  render() {
    return (
      <div className={`hide-for-print ${this.divClass()}`}>
        <label className={this.labelClass()} htmlFor={this.state.uid}>
          {this.props.label}
        </label>
        <textarea
          className={this.inputClass()}
          id={this.state.uid}
          name={this.props.name}
          aria-describedby={this.errorName()}
          aria-label={this.props.label || ariaLabel(this.refs.textarea)}
          disabled={this.props.disabled}
          maxLength={this.props.maxlength}
          pattern={this.props.pattern}
          readOnly={this.props.readonly}
          autoCapitalize={this.props.autocapitalize}
          autoCorrect={this.props.autocorrect}
          autoComplete={this.props.autocomplete}
          spellCheck={this.props.spellcheck}
          required={this.props.required}
          value={this.props.value}
          onChange={this.handleChange}
          onFocus={this.handleFocus}
          onBlur={this.handleBlur}
          ref="textarea"
        />
        <div className="textarea-print print-only">
          {this.props.value}
        </div>
      </div>
    )
  }
}

Textarea.defaultProps = {
  name: 'textarea',
  value: '',
  focus: false,
  error: false,
  valid: false,
  minlength: 0,
  maxlength: 4000,
  spellcheck: true,
  autocapitalize: true,
  autocorrect: true,
  autocomplete: true,
  onError: (value, arr) => {
    return arr
  },
  onUpdate: () => {},
}

Textarea.errors = [
  {
    code: 'required',
    func: (value, props) => {
      if (props.required) {
        return !!value
      }
      return true
    }
  },
  {
    code: 'length',
    func: (value, props) => {
      if (!value || !value.length) {
        return null
      }
      return (
        value.length >= parseInt(props.minlength || 0) &&
        value.length <= parseInt(props.maxlength || 4000)
      )
    }
  },
  {
    code: 'pattern',
    func: (value, props) => {
      if (!value || !value.length) {
        return null
      }
      const re = new RegExp(props.pattern)
      return re.test(value)
    }
  }
]