mdc-react
Version:
Material Components for the web implemented in React
237 lines (207 loc) • 7.63 kB
JSX
import { forwardRef, useRef, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Clone } from '../component';
import NotchedOutline from '../notched-outline';
import LineRipple from '../line-ripple';
import FloatingLabel from '../floating-label';
import Icon from '../icon';
import { cssClasses } from './constants';
import HelperText from './HelperText';
import CharacterCounter from './CharacterCounter';
import Input from './Input';
import Resizer from './Resizer';
const TextField = forwardRef(({
value,
defaultValue,
label,
leadingIcon,
trailingIcon,
prefix,
suffix,
persistentHelperText,
helperText = persistentHelperText,
validationMessage,
filled = false,
outlined = false,
fullWidth = false,
disabled = false,
textarea = false,
endAligned = false,
autoResize = false,
characterCounter = false,
internalCharacterCounter = characterCounter === 'internal',
className,
element: Element = 'label',
onFocus = Function.prototype,
onBlur = Function.prototype,
onChange = Function.prototype,
...props
}, ref) => {
const inputRef = useRef();
const [focused, setFocused] = useState(false);
const [touched, setTouched] = useState(false);
const [valid, setValid] = useState(true);
const [interactionCoords, setInteractionCoords] = useState();
const [count, setCount] = useState(value?.length || defaultValue?.value || 0);
const handleInteraction = useCallback(event => {
const targetClientRect = event.target.getBoundingClientRect();
setInteractionCoords({
x: event.clientX - targetClientRect.left,
y: event.clientY - targetClientRect.top
});
}, []);
const handleInputFocus = useCallback(event => {
setFocused(true);
setTouched(true);
onFocus(event);
}, [onFocus]);
const handleInputBlur = useCallback(event => {
setFocused(false);
setInteractionCoords();
onBlur(event);
}, [onBlur]);
const handleInputChange = useCallback(event => {
const value = inputRef.current.value;
const isValid = inputRef.current?.validity.valid;
setCount(value.length);
setValid(isValid);
onChange(event, value);
}, [onChange]);
const focusedOrHasValue = (
focused ||
(value !== undefined && value !== null && value !== '') ||
(defaultValue !== undefined && defaultValue !== null && defaultValue !== '') ||
Boolean(inputRef.current?.value)
);
const hasHelperLine = helperText || validationMessage || characterCounter;
const classNames = classnames(cssClasses.ROOT, {
[cssClasses.FILLED]: filled && !fullWidth,
[cssClasses.OUTLINED]: outlined && !fullWidth,
[cssClasses.TEXTAREA]: textarea,
[cssClasses.DISABLED]: disabled,
[cssClasses.FOCUSED]: focused,
[cssClasses.INVALID]: !valid && touched,
[cssClasses.LABEL_FLOATING]: focusedOrHasValue,
[cssClasses.NO_LABEL]: !label,
[cssClasses.END_ALIGNED]: endAligned,
[cssClasses.WITH_LEADING_ICON]: leadingIcon,
[cssClasses.WITH_TRAILING_ICON]: trailingIcon,
[cssClasses.WITH_INTERNAL_COUNTER]: internalCharacterCounter
}, className);
return (<>
<Element
ref={ref}
className={classNames}
onMouseDown={handleInteraction}
onTouchStart={handleInteraction}
>
{filled &&
<div className={cssClasses.RIPPLE} />
}
{filled && label &&
<FloatingLabel
label={label}
float={focusedOrHasValue}
/>
}
{outlined &&
<NotchedOutline notched={focusedOrHasValue}>
{label &&
<FloatingLabel
label={label}
float={focusedOrHasValue}
/>
}
</NotchedOutline>
}
{leadingIcon &&
<Clone
component={leadingIcon}
fallback={Icon}
className={`${cssClasses.ICON} ${cssClasses.ICON_LEADING}`}
tabIndex="0"
role="button"
/>
}
{prefix &&
<span className={`${cssClasses.AFFIX} ${cssClasses.AFFIX_PREFIX}`}>{prefix}</span>
}
<Resizer
textarea={textarea}
autoResize={autoResize}
>
<Input
ref={inputRef}
value={value}
defaultValue={defaultValue}
textarea={textarea}
autoResize={autoResize}
disabled={disabled}
onInput={handleInputChange}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
{...props}
/>
{internalCharacterCounter &&
<CharacterCounter
value={count}
maxValue={props.maxLength}
/>
}
</Resizer>
{suffix &&
<span className={`${cssClasses.AFFIX} ${cssClasses.AFFIX_SUFFIX}`}>{suffix}</span>
}
{trailingIcon &&
<Clone
component={trailingIcon}
fallback={Icon}
className={`${cssClasses.ICON} ${cssClasses.ICON_TRAILING}`}
tabIndex="0"
role="button"
/>
}
{filled &&
<LineRipple
active={focused}
transformOrigin={interactionCoords?.x}
/>
}
</Element>
{hasHelperLine &&
<div className={cssClasses.HELPER_LINE}>
{helperText &&
<HelperText persistent={Boolean(persistentHelperText)}>{helperText}</HelperText>
}
{(validationMessage && !valid) &&
<HelperText validation>{typeof validationMessage === 'string' ? validationMessage : inputRef.current?.validationMessage}</HelperText>
}
{(characterCounter && !internalCharacterCounter) &&
<CharacterCounter
value={count}
maxValue={props.maxLength}
/>
}
</div>
}
</>);
});
TextField.displayName = 'MDCTextField';
TextField.propTypes = {
value: PropTypes.any,
label: PropTypes.string,
leadingIcon: PropTypes.node,
trailingIcon: PropTypes.node,
prefix: PropTypes.string,
suffix: PropTypes.string,
outline: PropTypes.bool,
fullWidth: PropTypes.bool,
textarea: PropTypes.bool,
dense: PropTypes.bool,
disabled: PropTypes.bool,
helperText: PropTypes.string,
persistentHelperText: PropTypes.string,
validationMessage: PropTypes.oneOfType([PropTypes.bool, PropTypes.string])
};
export default TextField;