@momentum-ui/react
Version:
Cisco Momentum UI framework for ReactJs applications
386 lines (351 loc) • 10.6 kB
JavaScript
/** @component input */
import React from 'react';
import PropTypes from 'prop-types';
import omit from 'lodash/omit';
import toLower from 'lodash/toLower';
import {
Icon,
InputHelper,
InputMessage,
InputSection,
Label,
} from '@momentum-ui/react';
const determineMessageType = array => {
return array.reduce((agg, e) => {
return agg === 'error' ? agg : e.type || '';
}, '');
};
const filterMessagesByType = (array, value) => {
return array.reduce(
(agg, e) => (e.type === value ? agg.concat(e.message) : agg),
[]
);
};
/** Text input with integrated label to enforce consistency in layout, error display, label placement, and required field marker. */
class Input extends React.Component {
state = {
isEditing: false,
value: this.props.value || this.props.defaultValue,
};
componentDidUpdate (prevProps) {
const { value } = this.props;
value !== prevProps.value
&& this.setValue(value);
}
setValue = value => {
this.setState({
value
});
};
handleKeyDown = e => {
const { onKeyDown } = this.props;
onKeyDown && onKeyDown(e);
};
handleFocus = e => {
const { onFocus, disabled } = this.props;
if (disabled) {
e.stopPropagation();
return;
}
if (onFocus) {
onFocus(e);
}
this.setState({
isEditing: true,
});
};
handleMouseDown = e => {
const { onMouseDown, disabled } = this.props;
if (disabled) {
e.stopPropagation();
return;
}
if (onMouseDown) {
onMouseDown(e);
}
this.setState({
isEditing: true,
});
};
handleChange = e => {
const { onChange } = this.props;
const value = e.target.value;
e.persist();
this.setState(() => {
onChange && onChange(e);
return { value };
});
};
handleBlur = e => {
const { onDoneEditing } = this.props;
const value = e.target.value;
if (e.which === 27 || e.which === 13 || e.type === 'blur') {
this.setState(
{ isEditing: false }
, () => onDoneEditing && onDoneEditing(e, value)
);
}
e.stopPropagation();
};
handleClear = e => {
const value = '';
e.target.value = value;
e.persist();
this.input.focus();
this.handleChange(e);
};
setInputRef = input => {
const { clear, inputRef } = this.props;
if (clear) this.input = input;
if (inputRef) return inputRef(input);
}
render() {
const {
ariaDescribedBy,
ariaLabel,
className,
clear,
clearAriaLabel,
containerSize,
disabled,
messageArr,
htmlId,
id,
inputClassName,
helpText,
inputSize,
isFilled,
label,
multiline,
nestedLevel,
placeholder,
readOnly,
secondaryLabel,
shape,
type,
...props
} = this.props;
const { isEditing, value } = this.state;
const otherProps = omit({ ...props }, [
'defaultValue',
'inputAfter',
'inputBefore',
'inputRef',
'onChange',
'onDoneEditing',
'onFocus',
'onKeyDown',
'onMouseDown',
'ref',
'value',
]);
const messageType =
(messageArr.length > 0 && determineMessageType(messageArr)) || '';
const messages = (messageType && filterMessagesByType(messageArr, messageType)) || null;
const clearButton = (clear && !disabled && value) && (
<InputSection position='after'>
<Icon
name='clear-active_16'
onClick={this.handleClear}
ariaLabel={clearAriaLabel || 'clear input'}
buttonClassName='md-input__icon-clear'
/>
</InputSection>
);
const inputSection = position => (
this.props[`input${position}`]
&& (
<InputSection position={toLower(position)}>
{this.props[`input${position}`]}
</InputSection>
)
);
const inputLeft = inputSection('Before');
const inputRight = clearButton || inputSection('After');
const InputTag = multiline ? 'textarea' : 'input';
const inputElement = (
<div className={
'md-input__wrapper' +
`${inputSize ? ` columns ${inputSize}` : ''}`
}>
{inputLeft}
<InputTag
className={
'md-input' +
`${multiline ? ' md-input--multiline' : ''}` +
`${shape ? ` md-input--${shape}` : ''}` +
`${inputLeft ? ` md-input--before` : ''}` +
`${inputRight ? ` md-input--after` : ''}` +
`${isEditing ? ` md-active` : ''}` +
`${inputClassName ? ` ${inputClassName}` : ''}` +
`${readOnly ? ' md-read-only' : ''}` +
`${disabled ? ' md-disabled' : ''}` +
`${value ? ` md-dirty` : ''}`
}
onBlur={this.handleBlur}
onChange={this.handleChange}
onFocus={this.handleFocus}
onKeyDown={this.handleKeyDown}
onMouseDown={this.handleMouseDown}
ref={this.setInputRef}
tabIndex={0}
type={type}
value={value}
{...ariaDescribedBy && { 'aria-describedby': ariaDescribedBy }}
{...ariaLabel && { 'aria-label': ariaLabel }}
{...disabled && { disabled }}
{...(htmlId || id) && { id: htmlId || id }}
{...otherProps}
{...placeholder && { placeholder }}
{...readOnly && { readOnly }}
/>
{inputRight}
</div>
);
return (
<div
className={
`md-input-container` +
`${isFilled ? ' md-input--filled' : ''}` +
`${containerSize ? ` columns ${containerSize}` : ''}` +
`${readOnly ? ' md-read-only' : ''}` +
`${disabled ? ' md-disabled' : ''}` +
`${messageType ? ` md-${messageType}` : ''}` +
`${nestedLevel && ` md-input--nested-${nestedLevel}` || ''}` +
`${className ? ` ${className}` : ''}`
}
>
{
label &&
<Label
className='md-input__label'
htmlFor={htmlId || id}
label={label}
/>
}
{inputElement}
{
secondaryLabel &&
<Label
className='md-input__secondary-label'
htmlFor={htmlId || id}
label={secondaryLabel}
/>
}
{helpText && <InputHelper message={helpText} />}
{messages &&
<div className='md-input__messages'>
{messages.map((m, i) => (
<InputMessage message={m} key={`input-message-${i}`} />
))}
</div>
}
</div>
);
}
}
Input.propTypes = {
/** @prop ID to reference for blindness accessibility feature | null */
ariaDescribedBy: PropTypes.string,
/** @prop Text to display for blindness accessibility features | null */
ariaLabel: PropTypes.string,
/** @prop Optional css class name | '' */
className: PropTypes.string,
/** @prop Clears Input values | false */
clear: PropTypes.bool,
/** @prop Optional aria label on the clear button | null */
clearAriaLabel: PropTypes.string,
/** @prop Overall input container size | '' */
containerSize: PropTypes.string,
/** @prop Default Value same as value but used when onChange isn't invoked | '' */
defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
/*** @prop Sets the disabled attribute of the Input | false */
disabled: PropTypes.bool,
/** @prop Array of Objects with message and type [{error: '', type: 'error, success, warning'}] to display error message and assign class | [] */
messageArr: PropTypes.array,
/** @prop Unique HTML ID used for tying label to HTML input for automated testing | null */
htmlId: PropTypes.string,
/** Optional Icon node that overrides right section of input | null */
inputAfter: PropTypes.node,
/** Optional Icon node that overrides left section of input | null */
inputBefore: PropTypes.node,
/** Unique HTML ID used for tying label to HTML input | null */
id: PropTypes.string,
/** @prop Input css class name string | '' */
inputClassName: PropTypes.string,
/** @prop Help Text to show form validation rules | '' */
helpText: PropTypes.string,
/*** @prop Optional Input ref prop type | null */
inputRef: PropTypes.func,
/** @prop Overall input wrapper size | '' */
inputSize: PropTypes.string,
/*** @prop Applies the filled attribute of the Input | false */
isFilled: PropTypes.bool,
/** @prop Input label text | '' */
label: PropTypes.string,
/** @prop Input is multiline(textarea) | false */
multiline: PropTypes.bool,
/*** @prop Optional Input name prop type | null */
name: PropTypes.string,
/** @prop Set the level of nested Input components | 0 */
nestedLevel: PropTypes.number,
/** @prop Callback function invoked when user types into the Input field | null */
onChange: PropTypes.func,
/*** @prop Callback function invoked when user is done editing Input field | null */
onDoneEditing: PropTypes.func,
/*** @prop Callback function invoked when user focuses on the Input field | null */
onFocus: PropTypes.func,
/*** @prop Callback function invoked when user presses any key | null */
onKeyDown: PropTypes.func,
/*** @prop Callback function invoked when user clicks on the mouse/trackpad | null */
onMouseDown: PropTypes.func,
/** @prop Placeholder text to display when Input is empty | '' */
placeholder: PropTypes.string,
/*** @prop Determines if Input can be edited | false */
readOnly: PropTypes.bool,
/** @prop Secondary Input label | '' */
secondaryLabel: PropTypes.string,
/** @prop Input shape property | '' */
shape: PropTypes.string,
/** @prop Input type | 'text' */
type: PropTypes.oneOf(['text', 'number', 'password', 'email']),
/** @prop Input value | '' */
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
Input.defaultProps = {
ariaDescribedBy: null,
ariaLabel: null,
className: '',
clear: false,
clearAriaLabel: null,
containerSize: '',
defaultValue: '',
disabled: false,
messageArr: [],
htmlId: null,
inputAfter: null,
inputBefore: null,
id: null,
inputClassName: '',
helpText: '',
inputRef: null,
inputSize: '',
isFilled: false,
label: '',
multiline: false,
name: null,
nestedLevel: 0,
onChange: null,
onDoneEditing: null,
onFocus: null,
onKeyDown: null,
onMouseDown: null,
placeholder: '',
readOnly: false,
secondaryLabel: '',
shape: '',
type: 'text',
value: '',
};
Input.displayName = 'Input';
export default Input;