UNPKG

mdc-react

Version:

Material Components for the web implemented in React

237 lines (207 loc) 7.63 kB
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;