terra-form-field
Version:
The Form Field component handles the layout of the label, help text and error text associated with a form element.
216 lines (198 loc) • 6.54 kB
JSX
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import classNamesBind from 'classnames/bind';
import ThemeContext from 'terra-theme-context';
import IconError from 'terra-icon/lib/icon/IconError';
import styles from './Field.module.scss';
const cx = classNamesBind.bind(styles);
const propTypes = {
/**
* The form control elements the Field contains.
*/
children: PropTypes.node,
/**
* Error message for when the input is invalid. This will only be displayed if isInvalid is true.
*/
error: PropTypes.node,
/**
* Error Icon to display when the input is invalid.
*/
errorIcon: PropTypes.element,
/**
* Help element to display with the input.
*/
help: PropTypes.node,
/**
* @private
* Whether or not to hide the required indicator on the label.
*/
hideRequired: PropTypes.bool,
/**
* The htmlFor attribute on the field label.
*/
htmlFor: PropTypes.string,
/**
* Whether the field displays as Invalid. Use when value does not meet validation pattern.
*/
isInvalid: PropTypes.bool,
/**
* Whether or not the field is an inline field.
*/
isInline: PropTypes.bool,
/**
* @private
* Whether or not the label is visible. Use this props to hide a label while still creating it on the DOM for accessibility.
*/
isLabelHidden: PropTypes.bool,
/**
* The label of the form control children.
*/
label: PropTypes.string.isRequired,
/**
* Attributes to attach to the label.
*/
// eslint-disable-next-line react/forbid-prop-types
labelAttrs: PropTypes.object,
/**
* Set the max-width of a field using `length` or `%`. Best practice recommendation to never exceed
* a rendered value of 1020px. _(Note: Providing custom inline styles will take precedence.)_
*/
maxWidth: PropTypes.string,
/**
* Whether or not the field is required.
*/
required: PropTypes.bool,
/**
* Whether or not to append the 'optional' label to a non-required field label.
*/
showOptional: PropTypes.bool,
/**
* Provides first class prop for custom inline styles
*/
// eslint-disable-next-line react/forbid-prop-types
style: PropTypes.object,
};
const defaultProps = {
children: null,
error: null,
errorIcon: <IconError />,
help: null,
hideRequired: false,
htmlFor: undefined,
isInvalid: false,
isInline: false,
isLabelHidden: false,
labelAttrs: {},
maxWidth: undefined,
required: false,
showOptional: false,
};
const hasWhiteSpace = s => /\s/g.test(s);
// Detect IE 10 or IE 11
// TODO - Delete detection logic when we drop support for IE
const isIE = () => (window.navigator.userAgent.indexOf('Trident/6.0') > -1 || window.navigator.userAgent.indexOf('Trident/7.0') > -1);
const Field = (props) => {
const {
children,
error,
errorIcon,
help,
hideRequired,
htmlFor,
isInvalid,
isInline,
isLabelHidden,
label,
labelAttrs,
maxWidth,
required,
showOptional,
style,
...customProps
} = props;
const theme = React.useContext(ThemeContext);
const customStyles = maxWidth ? ({ maxWidth, ...style }) : style;
const fieldClasses = classNames(
cx([
'field',
{ 'field-inline': isInline },
theme.className,
]),
customProps.className,
);
const labelClassNames = cx([
'label',
labelAttrs.className,
]);
// Checks to run when not in production
// TODO - Remove this once we make this a required prop.
// More info: https://github.com/cerner/terra-core/issues/2290
if (process.env.NODE_ENV !== 'production') {
if (!htmlFor) {
// eslint-disable-next-line
console.warn('The htmlFor prop will be required in the next major version bump of terra-form-field. It is needed for creating an accessible mapping from the form field to its related error and help text.');
}
if (htmlFor && hasWhiteSpace(htmlFor)) {
// eslint-disable-next-line
console.warn('The htmlFor prop should be a string without white spaces as it will be used as an HTML attribute value. Use - or _ in place of white space characters.');
}
}
/**
* IE + JAWS has trouble reading aria-describedby content with our form components.
* Using feature detect for Microsoft browsers and injecting the help and error messages
* into the label as visually hidden text so JAWS can announce them correctly in IE.
*/
const IEDescriptionText = (
isIE()
? (
<div className={cx('visually-hidden-text')}>
{isInvalid && error ? error : null}
{help}
</div>
) : null
);
const content = React.Children.map(children, (child) => {
if ((required || isInvalid) && child && (child.type.isInput || child.type.isSelect || child.type.isTextarea)) {
return React.cloneElement(child, {
...required && { required: true },
...isInvalid && { isInvalid: true, 'aria-invalid': true },
});
}
return child;
});
const labelGroup = (
<div className={cx(['label-group', { 'label-group-hidden': isLabelHidden }])}>
{isInvalid && <div className={cx('error-icon')}>{errorIcon}</div>}
<label htmlFor={htmlFor} {...labelAttrs} className={labelClassNames}>
{required && (isInvalid || !hideRequired) && <div className={cx('required')} aria-hidden="true">*</div>}
{label}
{required && !isInvalid && hideRequired && <div className={cx('required-hidden')} aria-hidden="true">*</div>}
{showOptional && !required
&& (
<FormattedMessage id="Terra.form.field.optional">
{optionalText => (
<span className={cx('optional')}>{optionalText}</span>
)}
</FormattedMessage>
)}
{IEDescriptionText}
</label>
{!isInvalid && <div className={cx('error-icon-hidden')}>{errorIcon}</div>}
</div>
);
/* eslint-disable react/forbid-dom-props */
return (
<div style={customStyles} {...customProps} className={fieldClasses}>
{labelGroup}
{content}
{isInvalid && error && <div id={htmlFor ? `${htmlFor}-error` : undefined} className={cx('error-text')}>{error}</div>}
{help && <div id={htmlFor ? `${htmlFor}-help` : undefined} className={cx('help-text')}>{help}</div>}
</div>
);
/* eslint-enable react/forbid-dom-props */
};
Field.propTypes = propTypes;
Field.defaultProps = defaultProps;
export default Field;