/* eslint-disable no-unused-vars, max-len, @typescript-eslint/no-unused-vars */ import React from 'react'; import cls from 'classnames'; import PropTypes from 'prop-types'; import InputFoundation from '@douyinfe/semi-foundation/input/foundation'; import { cssClasses, strings } from '@douyinfe/semi-foundation/input/constants'; import { isSemiIcon } from '../_utils'; import BaseComponent from '../_base/baseComponent'; import '@douyinfe/semi-foundation/input/input.scss'; import { isString, noop, isFunction } from 'lodash'; import { IconClear, IconEyeOpened, IconEyeClosedSolid } from '@douyinfe/semi-icons'; const prefixCls = cssClasses.PREFIX; const sizeSet = strings.SIZE; const statusSet = strings.STATUS; const modeSet = strings.MODE; export { InputGroupProps } from './inputGroup'; export { TextAreaProps } from './textarea'; export type InputSize = 'small' | 'large' | 'default'; export type InputMode = 'password'; // still keep success as ValidateStatus optional value because form will pass success as props.validateStatus in sometime // Although we do not consume success in the input to configure special styles, we should allow it as a legal props value, otherwise a warning will be thrown export type ValidateStatus = "default" | "error" | "warning" | "success"; export interface InputProps extends Omit, 'onChange' | 'prefix' | 'size' | 'autoFocus' | 'placeholder' | 'onFocus' | 'onBlur'> { 'aria-label'?: React.AriaAttributes['aria-label']; 'aria-describedby'?: React.AriaAttributes['aria-describedby']; 'aria-errormessage'?: React.AriaAttributes['aria-errormessage']; 'aria-invalid'?: React.AriaAttributes['aria-invalid']; 'aria-labelledby'?: React.AriaAttributes['aria-labelledby']; 'aria-required'?: React.AriaAttributes['aria-required']; addonBefore?: React.ReactNode; addonAfter?: React.ReactNode; prefix?: React.ReactNode; suffix?: React.ReactNode; mode?: InputMode; value?: React.ReactText; defaultValue?: React.ReactText; disabled?: boolean; readonly?: boolean; autofocus?: boolean; type?: string; showClear?: boolean; hideSuffix?: boolean; placeholder?: React.ReactText; insetLabel?: React.ReactNode; insetLabelId?: string; size?: InputSize; className?: string; style?: React.CSSProperties; validateStatus?: ValidateStatus; onClear?: (e: React.MouseEvent) => void; onChange?: (value: string, e: React.ChangeEvent) => void; onBlur?: (e: React.FocusEvent) => void; onFocus?: (e: React.FocusEvent) => void; onInput?: (e: React.MouseEvent) => void; onKeyDown?: (e: React.KeyboardEvent) => void; onKeyUp?: (e: React.KeyboardEvent) => void; onKeyPress?: (e: React.KeyboardEvent) => void; onEnterPress?: (e: React.KeyboardEvent) => void; inputStyle?: React.CSSProperties; getValueLength?: (value: string) => number; forwardRef?: ((instance: any) => void) | React.MutableRefObject | null; } export interface InputState { value: React.ReactText; cachedValue: React.ReactText; disabled: boolean; props: Record; paddingLeft: string; isFocus: boolean; isHovering: boolean; eyeClosed: boolean; minLength: number; } class Input extends BaseComponent { static propTypes = { 'aria-label': PropTypes.string, 'aria-labelledby': PropTypes.string, 'aria-invalid': PropTypes.bool, 'aria-errormessage': PropTypes.string, 'aria-describedby': PropTypes.string, 'aria-required': PropTypes.bool, addonBefore: PropTypes.node, addonAfter: PropTypes.node, prefix: PropTypes.node, suffix: PropTypes.node, mode: PropTypes.oneOf(modeSet), value: PropTypes.any, defaultValue: PropTypes.any, disabled: PropTypes.bool, readonly: PropTypes.bool, autofocus: PropTypes.bool, type: PropTypes.string, showClear: PropTypes.bool, hideSuffix: PropTypes.bool, placeholder: PropTypes.any, size: PropTypes.oneOf(sizeSet), className: PropTypes.string, style: PropTypes.object, validateStatus: PropTypes.oneOf(statusSet), onClear: PropTypes.func, onChange: PropTypes.func, onBlur: PropTypes.func, onFocus: PropTypes.func, onInput: PropTypes.func, onKeyDown: PropTypes.func, onKeyUp: PropTypes.func, onKeyPress: PropTypes.func, onEnterPress: PropTypes.func, insetLabel: PropTypes.node, insetLabelId: PropTypes.string, inputStyle: PropTypes.object, getValueLength: PropTypes.func, }; static defaultProps = { addonBefore: '', addonAfter: '', prefix: '', suffix: '', disabled: false, readonly: false, type: 'text', showClear: false, hideSuffix: false, placeholder: '', size: 'default', className: '', onClear: noop, onChange: noop, onBlur: noop, onFocus: noop, onInput: noop, onKeyDown: noop, onKeyUp: noop, onKeyPress: noop, onEnterPress: noop, validateStatus: 'default' }; inputRef!: React.RefObject; prefixRef!: React.RefObject; suffixRef!: React.RefObject; foundation!: InputFoundation; constructor(props: InputProps) { super(props); this.state = { value: '', cachedValue: null, // Cache current props.value value disabled: false, props: {}, paddingLeft: '', isFocus: false, isHovering: false, eyeClosed: props.mode === 'password', minLength: props.minLength, }; this.inputRef = React.createRef(); this.prefixRef = React.createRef(); this.suffixRef = React.createRef(); this.foundation = new InputFoundation(this.adapter); } get adapter() { return { ...super.adapter, setValue: (value: string) => this.setState({ value }), setEyeClosed: (value: boolean) => this.setState({ eyeClosed: value }), toggleFocusing: (isFocus: boolean) => { const input = this.inputRef && this.inputRef.current; if (isFocus) { input && input.focus(); } else { input && input.blur(); } this.setState({ isFocus }); }, toggleHovering: (isHovering: boolean) => this.setState({ isHovering }), getIfFocusing: () => this.state.isFocus, notifyChange: (cbValue: string, e: React.ChangeEvent) => this.props.onChange(cbValue, e), notifyBlur: (val: string, e: React.FocusEvent) => this.props.onBlur(e), notifyFocus: (val: string, e: React.FocusEvent) => this.props.onFocus(e), notifyInput: (e: React.MouseEvent) => this.props.onInput(e), notifyKeyPress: (e: React.KeyboardEvent) => this.props.onKeyPress(e), notifyKeyDown: (e: React.KeyboardEvent) => this.props.onKeyDown(e), notifyKeyUp: (e: React.KeyboardEvent) => this.props.onKeyUp(e), notifyEnterPress: (e: React.KeyboardEvent) => this.props.onEnterPress(e), notifyClear: (e: React.MouseEvent) => this.props.onClear(e), setPaddingLeft: (paddingLeft: string) => this.setState({ paddingLeft }), setMinLength: (minLength: number) => this.setState({ minLength }), isEventTarget: (e: React.MouseEvent) => e && e.target === e.currentTarget }; } static getDerivedStateFromProps(props: InputProps, state: InputState) { const willUpdateStates: Partial = {}; if (props.value !== state.cachedValue) { willUpdateStates.value = props.value; willUpdateStates.cachedValue = props.value; } return willUpdateStates; } componentDidUpdate(prevProps: InputProps) { const { mode } = this.props; if (prevProps.mode !== mode) { this.handleModeChange(mode); } } handleClear = (e: React.MouseEvent) => { this.foundation.handleClear(e); }; handleClearEnterPress = (e: React.KeyboardEvent) => { this.foundation.handleClearEnterPress(e); }; handleClick = (e: React.MouseEvent) => { this.foundation.handleClick(e); }; handleMouseOver = (e: React.MouseEvent) => { this.setState({ isHovering: true }); }; handleMouseLeave = (e: React.MouseEvent) => { this.setState({ isHovering: false }); }; handleModeChange = (mode: string) => { this.foundation.handleModeChange(mode); }; handleClickEye = (e: React.MouseEvent) => { this.foundation.handleClickEye(e); }; handleMouseDown = (e: React.MouseEvent) => { this.foundation.handleMouseDown(e); }; handleMouseUp = (e: React.MouseEvent) => { this.foundation.handleMouseUp(e); }; handleModeEnterPress = (e: React.KeyboardEvent) => { this.foundation.handleModeEnterPress(e); } handleClickPrefixOrSuffix = (e: React.MouseEvent) => { this.foundation.handleClickPrefixOrSuffix(e); }; handlePreventMouseDown = (e: React.MouseEvent) => { this.foundation.handlePreventMouseDown(e); }; renderPrepend() { const { addonBefore } = this.props; if (addonBefore) { const prefixWrapperCls = cls({ [`${prefixCls}-prepend`]: true, [`${prefixCls}-prepend-text`]: addonBefore && isString(addonBefore), [`${prefixCls}-prepend-icon`]: isSemiIcon(addonBefore), }); return
{addonBefore}
; } return null; } renderAppend() { const { addonAfter } = this.props; if (addonAfter) { const prefixWrapperCls = cls({ [`${prefixCls}-append`]: true, [`${prefixCls}-append-text`]: addonAfter && isString(addonAfter), [`${prefixCls}-append-icon`]: isSemiIcon(addonAfter), }); return
{addonAfter}
; } return null; } renderClearBtn() { const clearCls = cls(`${prefixCls}-clearbtn`); const allowClear = this.foundation.isAllowClear(); // use onMouseDown to fix issue 1203 if (allowClear) { return (
); } return null; } renderModeBtn() { const { value, isFocus, isHovering, eyeClosed } = this.state; const { mode, disabled } = this.props; const modeCls = cls(`${prefixCls}-modebtn`); const modeIcon = eyeClosed ? : ; const showModeBtn = mode === 'password' && value && !disabled && (isFocus || isHovering); const ariaLabel = eyeClosed ? 'Show password' : 'Hidden password'; if (showModeBtn) { return (
{modeIcon}
); } return null; } renderPrefix() { const { prefix, insetLabel, insetLabelId } = this.props; const labelNode = prefix || insetLabel; if (!labelNode) { return null; } const prefixWrapperCls = cls({ [`${prefixCls}-prefix`]: true, [`${prefixCls}-inset-label`]: insetLabel, [`${prefixCls}-prefix-text`]: labelNode && isString(labelNode), [`${prefixCls}-prefix-icon`]: isSemiIcon(labelNode), }); // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions return
{labelNode}
; } showClearBtn() { const { value, isFocus, isHovering } = this.state; const { disabled, showClear } = this.props; return Boolean(value) && showClear && !disabled && (isFocus || isHovering); } renderSuffix(suffixAllowClear: boolean) { const { suffix, hideSuffix } = this.props; if (!suffix) { return null; } const suffixWrapperCls = cls({ [`${prefixCls }-suffix`]: true, [`${prefixCls }-suffix-text`]: suffix && isString(suffix), [`${prefixCls }-suffix-icon`]: isSemiIcon(suffix), [`${prefixCls}-suffix-hidden`]: suffixAllowClear && Boolean(hideSuffix), }); // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions return
{suffix}
; } render() { const { addonAfter, addonBefore, autofocus, className, disabled, placeholder, prefix, mode, insetLabel, insetLabelId, validateStatus, type, readonly, size, suffix, style, showClear, onEnterPress, onClear, hideSuffix, inputStyle, forwardRef, maxLength, getValueLength, ...rest } = this.props; const { value, paddingLeft, isFocus, minLength: stateMinLength } = this.state; const suffixAllowClear = this.showClearBtn(); const suffixIsIcon = isSemiIcon(suffix); const ref = forwardRef || this.inputRef; const wrapperPrefix = `${prefixCls}-wrapper`; const wrapperCls = cls(wrapperPrefix, className, { [`${prefixCls}-wrapper__with-prefix`]: prefix || insetLabel, [`${prefixCls}-wrapper__with-suffix`]: suffix, [`${prefixCls}-wrapper__with-suffix-hidden`]: suffixAllowClear && Boolean(hideSuffix), [`${prefixCls}-wrapper__with-suffix-icon`]: suffixIsIcon, [`${prefixCls}-wrapper__with-append`]: addonBefore, [`${prefixCls}-wrapper__with-prepend`]: addonAfter, [`${prefixCls}-wrapper__with-append-only`]: addonBefore && !addonAfter, [`${prefixCls}-wrapper__with-prepend-only`]: !addonBefore && addonAfter, [`${wrapperPrefix}-readonly`]: readonly, [`${wrapperPrefix}-disabled`]: disabled, [`${wrapperPrefix}-warning`]: validateStatus === 'warning', [`${wrapperPrefix}-error`]: validateStatus === 'error', [`${wrapperPrefix}-focus`]: isFocus, [`${wrapperPrefix}-clearable`]: showClear, [`${wrapperPrefix}-modebtn`]: mode === 'password', [`${wrapperPrefix}-hidden`]: type === 'hidden', [`${wrapperPrefix}-${size}`]: size, }); const inputCls = cls(prefixCls, { [`${prefixCls}-${size}`]: size, [`${prefixCls}-disabled`]: disabled, [`${prefixCls}-sibling-clearbtn`]: this.foundation.isAllowClear(), [`${prefixCls}-sibling-modebtn`]: mode === 'password', }); const inputValue = value === null || value === undefined ? '' : value; const inputProps: React.InputHTMLAttributes = { ...rest, style: { paddingLeft, ...inputStyle }, autoFocus: autofocus, className: inputCls, disabled, readOnly: readonly, type: this.foundation.handleInputType(type), placeholder: placeholder as string, onInput: e => this.foundation.handleInput(e), onChange: e => this.foundation.handleChange(e.target.value, e), onFocus: e => this.foundation.handleFocus(e), onBlur: e => this.foundation.handleBlur(e), onKeyUp: e => this.foundation.handleKeyUp(e), onKeyDown: e => this.foundation.handleKeyDown(e), onKeyPress: e => this.foundation.handleKeyPress(e), value: inputValue, }; if (!isFunction(getValueLength)) { inputProps.maxLength = maxLength; } if (stateMinLength) { inputProps.minLength = stateMinLength; } if (validateStatus === 'error') { inputProps['aria-invalid'] = "true"; } return ( // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
this.handleMouseOver(e)} onMouseLeave={e => this.handleMouseLeave(e)} onClick={e => this.handleClick(e)} > {this.renderPrepend()} {this.renderPrefix()} {this.renderClearBtn()} {this.renderSuffix(suffixAllowClear)} {this.renderModeBtn()} {this.renderAppend()}
); } } const ForwardInput = React.forwardRef>((props, ref) => ); export default ForwardInput; export { Input };