import * as React from 'react'; import * as PropTypes from 'prop-types'; import classNames from 'classnames'; import omit from 'omit.js'; import { polyfill } from 'react-lifecycles-compat'; import Group from './Group'; import Search from './Search'; import TextArea from './TextArea'; import { ConfigConsumer, ConfigConsumerProps } from '../config-provider'; import Password from './Password'; import Icon from '../icon'; import { Omit, tuple } from '../_util/type'; import warning from '../_util/warning'; function fixControlledValue(value: T) { if (typeof value === 'undefined' || value === null) { return ''; } return value; } function hasPrefixSuffix(props: InputProps) { return !!('prefix' in props || props.suffix || props.allowClear); } const InputSizes = tuple('small', 'default', 'large'); export interface InputProps extends Omit, 'size' | 'prefix'> { prefixCls?: string; size?: (typeof InputSizes)[number]; onPressEnter?: React.KeyboardEventHandler; addonBefore?: React.ReactNode; addonAfter?: React.ReactNode; prefix?: React.ReactNode; suffix?: React.ReactNode; allowClear?: boolean; } class Input extends React.Component { static Group: typeof Group; static Search: typeof Search; static TextArea: typeof TextArea; static Password: typeof Password; static defaultProps = { type: 'text', }; static propTypes = { type: PropTypes.string, id: PropTypes.string, size: PropTypes.oneOf(InputSizes), maxLength: PropTypes.number, disabled: PropTypes.bool, value: PropTypes.any, defaultValue: PropTypes.any, className: PropTypes.string, addonBefore: PropTypes.node, addonAfter: PropTypes.node, prefixCls: PropTypes.string, onPressEnter: PropTypes.func, onKeyDown: PropTypes.func, onKeyUp: PropTypes.func, onFocus: PropTypes.func, onBlur: PropTypes.func, prefix: PropTypes.node, suffix: PropTypes.node, allowClear: PropTypes.bool, }; static getDerivedStateFromProps(nextProps: InputProps) { if ('value' in nextProps) { return { value: nextProps.value, }; } return null; } input: HTMLInputElement; constructor(props: InputProps) { super(props); const value = typeof props.value === 'undefined' ? props.defaultValue : props.value; this.state = { value, }; } getSnapshotBeforeUpdate(prevProps: InputProps) { if (hasPrefixSuffix(prevProps) !== hasPrefixSuffix(this.props)) { warning( this.input !== document.activeElement, 'Input', `When Input is focused, dynamic add or remove prefix / suffix will make it lose focus caused by dom structure change. Read more: https://ant.design/components/input/#FAQ`, ); } return null; } // Since polyfill `getSnapshotBeforeUpdate` need work with `componentDidUpdate`. // We keep an empty function here. componentDidUpdate() {} handleKeyDown = (e: React.KeyboardEvent) => { const { onPressEnter, onKeyDown } = this.props; if (e.keyCode === 13 && onPressEnter) { onPressEnter(e); } if (onKeyDown) { onKeyDown(e); } }; focus() { this.input.focus(); } blur() { this.input.blur(); } select() { this.input.select(); } getInputClassName(prefixCls: string) { const { size, disabled } = this.props; return classNames(prefixCls, { [`${prefixCls}-sm`]: size === 'small', [`${prefixCls}-lg`]: size === 'large', [`${prefixCls}-disabled`]: disabled, }); } saveInput = (node: HTMLInputElement) => { this.input = node; }; setValue( value: string, e: React.ChangeEvent | React.MouseEvent, callback?: () => void, ) { if (!('value' in this.props)) { this.setState({ value }, callback); } const { onChange } = this.props; if (onChange) { let event = e; if (e.type === 'click') { // click clear icon event = Object.create(e); event.target = this.input; event.currentTarget = this.input; const originalInputValue = this.input.value; // change input value cause e.target.value should be '' when clear input this.input.value = ''; onChange(event as React.ChangeEvent); // reset input value this.input.value = originalInputValue; return; } onChange(event as React.ChangeEvent); } } handleReset = (e: React.MouseEvent) => { this.setValue('', e, () => { this.focus(); }); }; handleChange = (e: React.ChangeEvent) => { this.setValue(e.target.value, e); }; renderClearIcon(prefixCls: string) { const { allowClear } = this.props; const { value } = this.state; if (!allowClear || value === undefined || value === null || value === '') { return null; } return ( ); } renderSuffix(prefixCls: string) { const { suffix, allowClear } = this.props; if (suffix || allowClear) { return ( {this.renderClearIcon(prefixCls)} {suffix} ); } return null; } renderLabeledInput(prefixCls: string, children: React.ReactElement) { const { addonBefore, addonAfter, style, size, className } = this.props; // Not wrap when there is not addons if (!addonBefore && !addonAfter) { return children; } const wrapperClassName = `${prefixCls}-group`; const addonClassName = `${wrapperClassName}-addon`; const addonBeforeNode = addonBefore ? ( {addonBefore} ) : null; const addonAfterNode = addonAfter ? {addonAfter} : null; const mergedWrapperClassName = classNames(`${prefixCls}-wrapper`, { [wrapperClassName]: addonBefore || addonAfter, }); const mergedGroupClassName = classNames(className, `${prefixCls}-group-wrapper`, { [`${prefixCls}-group-wrapper-sm`]: size === 'small', [`${prefixCls}-group-wrapper-lg`]: size === 'large', }); // Need another wrapper for changing display:table to display:inline-block // and put style prop in wrapper return ( {addonBeforeNode} {React.cloneElement(children, { style: null })} {addonAfterNode} ); } renderLabeledIcon(prefixCls: string, children: React.ReactElement) { const { props } = this; const suffix = this.renderSuffix(prefixCls); if (!hasPrefixSuffix(props)) { return children; } const prefix = props.prefix ? ( {props.prefix} ) : null; const affixWrapperCls = classNames(props.className, `${prefixCls}-affix-wrapper`, { [`${prefixCls}-affix-wrapper-sm`]: props.size === 'small', [`${prefixCls}-affix-wrapper-lg`]: props.size === 'large', [`${prefixCls}-affix-wrapper-with-clear-btn`]: props.suffix && props.allowClear && this.state.value, }); return ( {prefix} {React.cloneElement(children, { style: null, className: this.getInputClassName(prefixCls), })} {suffix} ); } renderInput(prefixCls: string) { const { className, addonBefore, addonAfter } = this.props; const { value } = this.state; // Fix https://fb.me/react-unknown-prop const otherProps = omit(this.props, [ 'prefixCls', 'onPressEnter', 'addonBefore', 'addonAfter', 'prefix', 'suffix', 'allowClear', // Input elements must be either controlled or uncontrolled, // specify either the value prop, or the defaultValue prop, but not both. 'defaultValue', ]); return this.renderLabeledIcon( prefixCls, , ); } renderComponent = ({ getPrefixCls }: ConfigConsumerProps) => { const { prefixCls: customizePrefixCls } = this.props; const prefixCls = getPrefixCls('input', customizePrefixCls); return this.renderLabeledInput(prefixCls, this.renderInput(prefixCls)); }; render() { return {this.renderComponent}; } } polyfill(Input); export default Input;