diff --git a/src/components/Fieldsets/NameFieldset/NameFieldset.jsx b/src/components/Fieldsets/NameFieldset/NameFieldset.jsx
new file mode 100644
index 000000000..99eeae771
--- /dev/null
+++ b/src/components/Fieldsets/NameFieldset/NameFieldset.jsx
@@ -0,0 +1,201 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+
+import i18n from 'util/i18n'
+
+import {
+ ConnectedTextFormField, ConnectedSelectFormField, ConnectedCheckboxInput,
+} from 'components/Inputs/connectedInputs'
+
+import nameModel from 'models/shared/name'
+import { getEffectiveModel, getValidationPropsFromModel } from 'helpers/validation'
+
+const NameFieldset = (props) => {
+ const {
+ value, title, className, disabled, required, prefix, onUpdate,
+ hideMiddleName, errors,
+ } = props
+
+ const classes = classnames('name', className, { disabled })
+
+ const suffixOptions = [
+ { label: '', value: '' },
+ { label: i18n.t(`${prefix}.label.jr`), value: 'Jr' },
+ { label: i18n.t(`${prefix}.label.sr`), value: 'Sr' },
+ { label: i18n.t(`${prefix}.label.i`), value: 'I' },
+ { label: i18n.t(`${prefix}.label.ii`), value: 'II' },
+ { label: i18n.t(`${prefix}.label.iii`), value: 'III' },
+ { label: i18n.t(`${prefix}.label.iv`), value: 'IV' },
+ { label: i18n.t(`${prefix}.label.v`), value: 'V' },
+ { label: i18n.t(`${prefix}.label.vi`), value: 'VI' },
+ { label: i18n.t(`${prefix}.label.vii`), value: 'VII' },
+ { label: i18n.t(`${prefix}.label.viii`), value: 'VIII' },
+ { label: i18n.t(`${prefix}.label.ix`), value: 'IX' },
+ { label: i18n.t(`${prefix}.label.x`), value: 'X' },
+ { label: i18n.t(`${prefix}.label.other`), value: 'Other' },
+ ]
+
+ const onChange = (name, newValue) => {
+ onUpdate({
+ ...value,
+ [`${name}`]: newValue,
+ })
+ }
+
+ const {
+ first, middle, last, firstInitialOnly, middleInitialOnly, noMiddleName,
+ suffix, suffixOther,
+ } = value
+
+ const inputProps = {
+ disabled,
+ required,
+ onChange,
+ }
+
+ const errorPrefix = 'Name.model'
+ const effectiveNameModel = getEffectiveModel(nameModel, value)
+
+ const validationProps = {}
+
+ Object.keys(effectiveNameModel).forEach((field) => {
+ const fieldModel = getEffectiveModel(effectiveNameModel[field], value)
+ const fieldErrors = errors.filter(e => e.indexOf(`${errorPrefix}.${field}`) === 0)
+
+ validationProps[field] = {
+ ...getValidationPropsFromModel(fieldModel, required),
+ errors: fieldErrors,
+ }
+ })
+
+ const firstInitialOnlyCheckbox = (
+
+
+
+ )
+
+ const middleInitialOnlyCheckbox = (
+
+
+
+ )
+
+ const noMiddleNameCheckbox = (
+
+
+
+ )
+
+ return (
+
+ )
+}
+
+NameFieldset.propTypes = {
+ value: PropTypes.object,
+ title: PropTypes.node,
+ className: PropTypes.string,
+ disabled: PropTypes.bool,
+ required: PropTypes.bool,
+ prefix: PropTypes.string,
+ onUpdate: PropTypes.func,
+ errors: PropTypes.array,
+ hideMiddleName: PropTypes.bool,
+}
+
+NameFieldset.defaultProps = {
+ value: {},
+ title: null,
+ className: null,
+ disabled: false,
+ required: false,
+ prefix: 'name',
+ onUpdate: () => {},
+ errors: [],
+ hideMiddleName: false,
+}
+
+export default NameFieldset
diff --git a/src/components/Fieldsets/NameFieldset/NameFieldset.stories.jsx b/src/components/Fieldsets/NameFieldset/NameFieldset.stories.jsx
new file mode 100644
index 000000000..745720dda
--- /dev/null
+++ b/src/components/Fieldsets/NameFieldset/NameFieldset.stories.jsx
@@ -0,0 +1,34 @@
+/* eslint import/no-extraneous-dependencies: 0 */
+
+import React from 'react'
+import { storiesOf } from '@storybook/react'
+
+import NameFieldset from './NameFieldset'
+
+storiesOf('NameFieldset', module)
+ .add('default', () => (
+
+ ))
+ .add('with a value', () => (
+
+ ))
+ .add('with a title', () => (
+
+ ))
diff --git a/src/components/Inputs/CheckboxInput/CheckboxInput.jsx b/src/components/Inputs/CheckboxInput/CheckboxInput.jsx
new file mode 100644
index 000000000..25ffae7c5
--- /dev/null
+++ b/src/components/Inputs/CheckboxInput/CheckboxInput.jsx
@@ -0,0 +1,101 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+
+const CheckboxInput = (props) => {
+ const {
+ label, uid, name, ariaLabel, disabled, error, readonly, value, className,
+ valid, onChange, onFocus, onBlur, children,
+ } = props
+
+ // eslint-disable-next-line eqeqeq
+ const checked = value == true
+
+ const showError = !disabled && error
+
+ const divClass = classnames(
+ className,
+ 'block',
+ {
+ disabled,
+ 'usa-input-error': showError,
+ }
+ )
+
+ const labelClass = classnames(
+ 'checkbox',
+ {
+ disabled,
+ 'usa-input-error-label': showError,
+ checked,
+ }
+ )
+
+ const inputClass = classnames({
+ 'usa-input-success': valid,
+ })
+
+ const handleChange = (e) => {
+ onChange(e.target.checked)
+ }
+
+ return (
+
+
+
+
+ )
+}
+
+CheckboxInput.propTypes = {
+ name: PropTypes.string.isRequired,
+ uid: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired,
+ ariaLabel: PropTypes.string,
+ children: PropTypes.node,
+ value: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
+ error: PropTypes.bool,
+ valid: PropTypes.bool,
+ disabled: PropTypes.bool,
+ className: PropTypes.string,
+ readonly: PropTypes.bool,
+ onChange: PropTypes.func,
+ onFocus: PropTypes.func,
+ onBlur: PropTypes.func,
+}
+
+CheckboxInput.defaultProps = {
+ ariaLabel: null,
+ children: null,
+ value: '',
+ error: false,
+ valid: false,
+ disabled: false,
+ className: '',
+ readonly: false,
+ onChange: () => {},
+ onFocus: () => {},
+ onBlur: () => {},
+}
+
+export default CheckboxInput
diff --git a/src/components/Inputs/FormField/FormField.jsx b/src/components/Inputs/FormField/FormField.jsx
new file mode 100644
index 000000000..463640e76
--- /dev/null
+++ b/src/components/Inputs/FormField/FormField.jsx
@@ -0,0 +1,101 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+
+import FormFieldHelp from './FormFieldHelp'
+
+const FormField = ({
+ title, required, inputId, children, errors, help, toggleHelp, showHelp,
+ className,
+}) => {
+ const classes = classnames(
+ 'field',
+ 'usa-form-control',
+ { required },
+ className,
+ )
+
+ const iconClasses = classnames(
+ 'toggle',
+ { active: showHelp },
+ )
+
+ return (
+
+ {title && (
+
+ {title}
+ {!required && (Optional)}
+
+ )}
+
+ {help && toggleHelp && (
+
+ )}
+
+ {help && showHelp && (
+
+
+
+ )}
+
+ {errors && errors.length > 0 && (
+
+ {errors.map((e, i) => (
+
+ ))}
+
+ )}
+
+
+ {children}
+
+
+ )
+}
+
+FormField.propTypes = {
+ title: PropTypes.node,
+ required: PropTypes.bool,
+ inputId: PropTypes.string.isRequired,
+ children: PropTypes.node.isRequired,
+ errors: PropTypes.array,
+ help: PropTypes.string,
+ toggleHelp: PropTypes.func,
+ showHelp: PropTypes.bool,
+ className: PropTypes.string,
+}
+
+FormField.defaultProps = {
+ title: null,
+ required: false,
+ errors: [],
+ help: null,
+ toggleHelp: null,
+ showHelp: false,
+ className: null,
+}
+
+export default FormField
diff --git a/src/components/Inputs/FormField/FormField.module.scss b/src/components/Inputs/FormField/FormField.module.scss
new file mode 100644
index 000000000..6b531dc9c
--- /dev/null
+++ b/src/components/Inputs/FormField/FormField.module.scss
@@ -0,0 +1,5 @@
+.modifiers {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+}
\ No newline at end of file
diff --git a/src/components/Inputs/FormField/FormField.stories.jsx b/src/components/Inputs/FormField/FormField.stories.jsx
new file mode 100644
index 000000000..a3800f844
--- /dev/null
+++ b/src/components/Inputs/FormField/FormField.stories.jsx
@@ -0,0 +1,21 @@
+/* eslint import/no-extraneous-dependencies: 0 */
+
+import React from 'react'
+import { storiesOf } from '@storybook/react'
+
+import FormField from './FormField'
+
+storiesOf('FormField', module)
+ .add('default', () => (
+
+ ))
+ .add('with a value', () => (
+
+ ))
diff --git a/src/components/Inputs/FormField/FormFieldHelp.jsx b/src/components/Inputs/FormField/FormFieldHelp.jsx
new file mode 100644
index 000000000..91ce512c5
--- /dev/null
+++ b/src/components/Inputs/FormField/FormFieldHelp.jsx
@@ -0,0 +1,52 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+
+import i18n from 'util/i18n'
+import FormFieldMessage from './FormFieldMessage'
+
+const FormFieldHelp = ({
+ id, message, title, toggle,
+}) => {
+ const helpMessage = message || i18n.m(`${id}.message`)
+ const helpTitle = title || i18n.t(`${id}.title`)
+ const helpNote = i18n.m(`${id}.note`)
+
+ return (
+
+
+
+
+ {toggle && (
+
+ )}
+
+
+ )
+}
+
+FormFieldHelp.propTypes = {
+ id: PropTypes.string.isRequired,
+ message: PropTypes.node,
+ title: PropTypes.node,
+ toggle: PropTypes.func,
+}
+
+FormFieldHelp.defaultProps = {
+ message: null,
+ title: null,
+ toggle: null,
+}
+
+export default FormFieldHelp
diff --git a/src/components/Inputs/FormField/FormFieldMessage.jsx b/src/components/Inputs/FormField/FormFieldMessage.jsx
new file mode 100644
index 000000000..0eb655f3e
--- /dev/null
+++ b/src/components/Inputs/FormField/FormFieldMessage.jsx
@@ -0,0 +1,28 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+
+// Used by help text & errors to render a group of text
+const FormFieldMessage = ({
+ id, title, message, note,
+}) => (
+
+ {title && (
{title}
)}
+ {message && {message}}
+ {note && {note}}
+
+)
+
+FormFieldMessage.propTypes = {
+ id: PropTypes.string.isRequired,
+ title: PropTypes.node,
+ message: PropTypes.node,
+ note: PropTypes.node,
+}
+
+FormFieldMessage.defaultProps = {
+ title: null,
+ message: null,
+ note: null,
+}
+
+export default FormFieldMessage
diff --git a/src/components/Inputs/InputConnector.jsx b/src/components/Inputs/InputConnector.jsx
new file mode 100644
index 000000000..d646e1a28
--- /dev/null
+++ b/src/components/Inputs/InputConnector.jsx
@@ -0,0 +1,128 @@
+/**
+ * HOC that connects input event handlers and optionally renders inside of a
+ * FormField component
+ */
+import React from 'react'
+import PropTypes from 'prop-types'
+
+import { newGuid } from 'components/Form/ValidationElement/helpers'
+import { REQUIRED } from 'constants/errors'
+
+import FormField from './FormField/FormField'
+import styles from './FormField/FormField.module.scss'
+
+const connectInput = (Component, renderFormField = true) => {
+ class ConnectedInput extends React.Component {
+ constructor(props) {
+ super(props)
+
+ // TODO - newGuid should be replaced with a library for generating uuids
+ this.uid = newGuid()
+
+ this.state = {
+ focus: false,
+ showHelp: false,
+ }
+ }
+
+ toggleHelp = () => {
+ const { showHelp } = this.state
+ this.setState({ showHelp: !showHelp })
+ }
+
+ handleChange = (value) => {
+ const { name, onChange } = this.props
+ onChange(name, value)
+ }
+
+ handleFocus = () => {
+ this.setState({ focus: true })
+ }
+
+ handleBlur = () => {
+ this.setState({ focus: false })
+ }
+
+ showErrors = () => {
+ // Only show required error if required prop is true
+ // Otherwise only show other errors if there is no required error (there's an invalid value)
+ const { errors, required } = this.props
+ if (required) return true
+ return errors.filter(e => e.indexOf(`presence.${REQUIRED}`) > -1).length < 1
+ }
+
+ filterErrors = () => {
+ // If there's a required error, only only that
+ // Otherwise show the rest of the errors
+ const { errors } = this.props
+ const requiredError = errors.filter(e => e.indexOf(`presence.${REQUIRED}`) > -1)
+ if (requiredError.length) return requiredError
+ return errors
+ }
+
+ render() {
+ const { errors } = this.props
+
+ const showErrors = this.showErrors()
+
+ const errorProps = {
+ errors: showErrors && this.filterErrors(),
+ valid: !errors || errors.length < 1,
+ error: showErrors && errors.length > 0,
+ }
+
+ const inputComponent = (
+
+ )
+
+ if (renderFormField) {
+ const { modifiers } = this.props
+ const { showHelp } = this.state
+ return (
+
+ {inputComponent}
+ {modifiers && (
+ {modifiers}
+ )}
+
+ )
+ }
+
+ return inputComponent
+ }
+ }
+
+ ConnectedInput.propTypes = {
+ name: PropTypes.string.isRequired,
+ onChange: PropTypes.func,
+ modifiers: PropTypes.array,
+ errors: PropTypes.array,
+ required: PropTypes.bool,
+ }
+
+ ConnectedInput.defaultProps = {
+ modifiers: null,
+ onChange: () => {},
+ errors: [],
+ required: false,
+ }
+
+ return ConnectedInput
+}
+
+export default connectInput
diff --git a/src/components/Inputs/SelectInput/SelectInput.jsx b/src/components/Inputs/SelectInput/SelectInput.jsx
new file mode 100644
index 000000000..6b9a08500
--- /dev/null
+++ b/src/components/Inputs/SelectInput/SelectInput.jsx
@@ -0,0 +1,82 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+
+const SelectInput = (props) => {
+ const {
+ uid, name, value, error, valid, disabled, className,
+ label, options, optional, onChange,
+ } = props
+
+ const wrapperClasses = classnames(
+ className,
+ { 'usa-input-error': error }
+ )
+
+ const selectClasses = classnames(
+ { 'usa-input-success': valid }
+ )
+
+ const handleChange = (e) => {
+ onChange(e.target.value)
+ }
+
+ return (
+
+
+
+
+
+ )
+}
+
+SelectInput.propTypes = {
+ name: PropTypes.string.isRequired,
+ uid: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired,
+ options: PropTypes.array.isRequired,
+ value: PropTypes.string,
+ error: PropTypes.bool,
+ valid: PropTypes.bool,
+ disabled: PropTypes.bool,
+ className: PropTypes.string,
+ optional: PropTypes.bool,
+ onChange: PropTypes.func,
+}
+
+SelectInput.defaultProps = {
+ value: '',
+ error: false,
+ valid: false,
+ disabled: false,
+ className: '',
+ optional: false,
+ onChange: () => {},
+}
+
+export default SelectInput
diff --git a/src/components/Inputs/TextInput/TextInput.jsx b/src/components/Inputs/TextInput/TextInput.jsx
new file mode 100644
index 000000000..d5bed61f6
--- /dev/null
+++ b/src/components/Inputs/TextInput/TextInput.jsx
@@ -0,0 +1,136 @@
+/**
+ * Input requirements
+ * Generic text input (text, email, number, password)
+*/
+import React from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+
+import { autotab } from 'components/Form/Generic'
+
+const TextInput = (props) => {
+ const {
+ label, uid, name, type, placeholder, ariaLabel, disabled, error, maxlength,
+ pattern, readonly, autocapitalize, autocorrect, autocomplete, spellcheck,
+ value, clipboard, className, focus, valid, onChange, onFocus, onBlur,
+ tabBack, tabNext,
+ } = props
+
+ const divClass = classnames(
+ 'hide-for-print',
+ className,
+ { 'usa-input-error': !disabled && error }
+ )
+
+ const labelClass = classnames({
+ disabled,
+ 'usa-input-error-label': !disabled && error,
+ })
+
+ const inputClass = classnames({
+ 'usa-input-focus': !disabled && focus,
+ 'usa-input-success': !disabled && valid,
+ }, className)
+
+ const handleKeyDown = (e) => {
+ autotab(e, maxlength, tabBack, tabNext)
+ }
+
+ const allowClipboard = (e) => {
+ if (!clipboard) e.preventDefault()
+ }
+
+ const handleChange = (e) => {
+ onChange(e.target.value)
+ }
+
+ return (
+
+ {label && (
+
+ )}
+
+ {/* for print CSS */}
+
{value}
+
+ )
+}
+
+TextInput.propTypes = {
+ name: PropTypes.string.isRequired,
+ uid: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired,
+ ariaLabel: PropTypes.string,
+ type: PropTypes.oneOf(['text', 'email', 'number', 'password']),
+ value: PropTypes.string,
+ placeholder: PropTypes.string,
+ error: PropTypes.bool,
+ valid: PropTypes.bool,
+ focus: PropTypes.bool,
+ disabled: PropTypes.bool,
+ className: PropTypes.string,
+ clipboard: PropTypes.bool,
+ spellcheck: PropTypes.bool,
+ autocapitalize: PropTypes.bool,
+ autocorrect: PropTypes.bool,
+ autocomplete: PropTypes.bool,
+ readonly: PropTypes.bool,
+ maxlength: PropTypes.number,
+ pattern: PropTypes.string,
+ tabBack: PropTypes.func,
+ tabNext: PropTypes.func,
+ onChange: PropTypes.func,
+ onFocus: PropTypes.func,
+ onBlur: PropTypes.func,
+}
+
+TextInput.defaultProps = {
+ ariaLabel: null,
+ type: 'text',
+ value: '',
+ placeholder: '',
+ error: false,
+ valid: false,
+ focus: false,
+ disabled: false,
+ className: '',
+ clipboard: true,
+ spellcheck: true,
+ autocapitalize: true,
+ autocorrect: true,
+ autocomplete: true,
+ readonly: false,
+ maxlength: 255,
+ pattern: '.*',
+ tabBack: () => {},
+ tabNext: () => {},
+ onChange: () => {},
+ onFocus: () => {},
+ onBlur: () => {},
+}
+
+export default TextInput
diff --git a/src/components/Inputs/TextInput/TextInput.stories.jsx b/src/components/Inputs/TextInput/TextInput.stories.jsx
new file mode 100644
index 000000000..c3293b5d8
--- /dev/null
+++ b/src/components/Inputs/TextInput/TextInput.stories.jsx
@@ -0,0 +1,55 @@
+/* eslint-disable import/no-extraneous-dependencies */
+import React from 'react'
+import { storiesOf } from '@storybook/react'
+
+import TextInput from './TextInput'
+import { ConnectedTextInput } from '../connectedInputs'
+
+storiesOf('Input', module)
+ .add('default', () => (
+
+ ))
+ .add('with a value', () => (
+
+ ))
+ .add('with FormField', () => (
+
+ ))
+ .add('with FormField and errors', () => (
+
+ ))
+ .add('with FormField and help text', () => (
+
+ ))
+ .add('with FormField and errors and help text', () => (
+
+ ))
diff --git a/src/components/Inputs/connectedInputs.js b/src/components/Inputs/connectedInputs.js
new file mode 100644
index 000000000..5bf0ae6b2
--- /dev/null
+++ b/src/components/Inputs/connectedInputs.js
@@ -0,0 +1,22 @@
+import connectInput from 'components/Inputs/InputConnector'
+
+import TextInput from 'components/Inputs/TextInput/TextInput'
+import CheckboxInput from 'components/Inputs/CheckboxInput/CheckboxInput'
+import SelectInput from 'components/Inputs/SelectInput/SelectInput'
+
+const ConnectedTextInput = connectInput(TextInput, false)
+const ConnectedCheckboxInput = connectInput(CheckboxInput, false)
+const ConnectedSelectInput = connectInput(SelectInput, false)
+
+const ConnectedTextFormField = connectInput(TextInput)
+const ConnectedCheckboxFormField = connectInput(CheckboxInput)
+const ConnectedSelectFormField = connectInput(SelectInput)
+
+export {
+ ConnectedTextInput,
+ ConnectedCheckboxInput,
+ ConnectedSelectInput,
+ ConnectedTextFormField,
+ ConnectedCheckboxFormField,
+ ConnectedSelectFormField,
+}
diff --git a/src/components/Section/Identification/ApplicantName/ApplicantName.jsx b/src/components/Section/Identification/ApplicantName/ApplicantName.jsx
index 238167104..621fb841c 100644
--- a/src/components/Section/Identification/ApplicantName/ApplicantName.jsx
+++ b/src/components/Section/Identification/ApplicantName/ApplicantName.jsx
@@ -2,6 +2,7 @@ import React from 'react'
import { i18n } from 'config'
import { Name, Field } from 'components/Form'
+import NameFieldset from 'components/Fieldsets/NameFieldset/NameFieldset'
import {
IDENTIFICATION,
@@ -50,6 +51,8 @@ export class ApplicantName extends Subsection {
}
render() {
+ const { errors } = this.props
+
const klass = `section-content applicant-name ${this.props.className
|| ''}`.trim()
@@ -60,6 +63,15 @@ export class ApplicantName extends Subsection {
data-subsection={IDENTIFICATION_NAME.key}
>
{i18n.t('identification.destination.name')}
+
+
+
(
return s.isValid === false
})
)
+
+/** Helper for using model object in UI components */
+// Gets model as a static object with given value and options
+export const getEffectiveModel = (model, attributes, options = {}) => {
+ const effectiveModel = {}
+
+ const constraints = Object.keys(model)
+ for (let i = 0; i < constraints.length; i += 1) {
+ const constraint = constraints[i] // attributeName
+ const constraintValue = model[constraint] // validation function
+ if (validate.isFunction(constraintValue)) {
+ const value = attributes[constraint] // attribute value
+ effectiveModel[constraint] = constraintValue(value, attributes, constraint, options)
+ break
+ }
+
+ effectiveModel[constraint] = constraintValue
+ }
+
+ return effectiveModel
+}
+
+// Input is a validate.js constraints object
+// Return object representing input attributes that handle native HTML5 validation
+export const getValidationPropsFromModel = (model = {}, required = true) => {
+ const props = {}
+
+ const constraints = Object.keys(model)
+ for (let i = 0; i < constraints.length; i += 1) {
+ const constraint = constraints[i]
+ switch (constraint) {
+ case 'presence':
+ if (required) props.required = model[constraint]
+ break
+
+ case 'format':
+ props.pattern = model[constraint]
+ break
+
+ case 'length': {
+ const length = model[constraint]
+ if (length.is) {
+ props.maxLength = length.is
+ props.minLength = length.is
+ }
+
+ if (length.maximum) props.maxLength = length.maximum
+ if (length.minimum) props.minLength = length.minimum
+ break
+ }
+
+ default:
+ // no op
+ }
+ }
+
+ return props
+}