import * as React from 'react'; import classNames from 'classnames'; import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled'; import { tuple } from '../_util/type'; import type { InputProps } from './Input'; import { DirectionType } from '../config-provider'; import { SizeType } from '../config-provider/SizeContext'; import { cloneElement } from '../_util/reactNode'; import { getInputClassName, hasPrefixSuffix } from './utils'; const ClearableInputType = tuple('text', 'input'); function hasAddon(props: InputProps | ClearableInputProps) { return !!(props.addonBefore || props.addonAfter); } /** This basic props required for input and textarea. */ interface BasicProps { prefixCls: string; inputType: typeof ClearableInputType[number]; value?: any; allowClear?: boolean; element: React.ReactElement; handleReset: (event: React.MouseEvent) => void; className?: string; style?: React.CSSProperties; disabled?: boolean; direction?: DirectionType; focused?: boolean; readOnly?: boolean; bordered: boolean; } /** This props only for input. */ export interface ClearableInputProps extends BasicProps { size?: SizeType; suffix?: React.ReactNode; prefix?: React.ReactNode; addonBefore?: React.ReactNode; addonAfter?: React.ReactNode; triggerFocus?: () => void; } class ClearableLabeledInput extends React.Component { /** @private Do Not use out of this class. We do not promise this is always keep. */ private containerRef = React.createRef(); onInputMouseUp: React.MouseEventHandler = e => { if (this.containerRef.current?.contains(e.target as Element)) { const { triggerFocus } = this.props; triggerFocus?.(); } }; renderClearIcon(prefixCls: string) { const { allowClear, value, disabled, readOnly, handleReset, suffix } = this.props; if (!allowClear) { return null; } const needClear = !disabled && !readOnly && value; const className = `${prefixCls}-clear-icon`; return ( e.preventDefault()} className={classNames( { [`${className}-hidden`]: !needClear, [`${className}-has-suffix`]: !!suffix, }, className, )} role="button" /> ); } renderSuffix(prefixCls: string) { const { suffix, allowClear } = this.props; if (suffix || allowClear) { return ( {this.renderClearIcon(prefixCls)} {suffix} ); } return null; } renderLabeledIcon(prefixCls: string, element: React.ReactElement) { const { focused, value, prefix, className, size, suffix, disabled, allowClear, direction, style, readOnly, bordered, } = this.props; const suffixNode = this.renderSuffix(prefixCls); if (!hasPrefixSuffix(this.props)) { return cloneElement(element, { value, }); } const prefixNode = prefix ? {prefix} : null; const affixWrapperCls = classNames(`${prefixCls}-affix-wrapper`, { [`${prefixCls}-affix-wrapper-focused`]: focused, [`${prefixCls}-affix-wrapper-disabled`]: disabled, [`${prefixCls}-affix-wrapper-sm`]: size === 'small', [`${prefixCls}-affix-wrapper-lg`]: size === 'large', [`${prefixCls}-affix-wrapper-input-with-clear-btn`]: suffix && allowClear && value, [`${prefixCls}-affix-wrapper-rtl`]: direction === 'rtl', [`${prefixCls}-affix-wrapper-readonly`]: readOnly, [`${prefixCls}-affix-wrapper-borderless`]: !bordered, // className will go to addon wrapper [`${className}`]: !hasAddon(this.props) && className, }); return ( {prefixNode} {cloneElement(element, { style: null, value, className: getInputClassName(prefixCls, bordered, size, disabled), })} {suffixNode} ); } renderInputWithLabel(prefixCls: string, labeledElement: React.ReactElement) { const { addonBefore, addonAfter, style, size, className, direction } = this.props; // Not wrap when there is not addons if (!hasAddon(this.props)) { return labeledElement; } const wrapperClassName = `${prefixCls}-group`; const addonClassName = `${wrapperClassName}-addon`; const addonBeforeNode = addonBefore ? ( {addonBefore} ) : null; const addonAfterNode = addonAfter ? {addonAfter} : null; const mergedWrapperClassName = classNames(`${prefixCls}-wrapper`, wrapperClassName, { [`${wrapperClassName}-rtl`]: direction === 'rtl', }); const mergedGroupClassName = classNames( `${prefixCls}-group-wrapper`, { [`${prefixCls}-group-wrapper-sm`]: size === 'small', [`${prefixCls}-group-wrapper-lg`]: size === 'large', [`${prefixCls}-group-wrapper-rtl`]: direction === 'rtl', }, className, ); // Need another wrapper for changing display:table to display:inline-block // and put style prop in wrapper return ( {addonBeforeNode} {cloneElement(labeledElement, { style: null })} {addonAfterNode} ); } renderTextAreaWithClearIcon(prefixCls: string, element: React.ReactElement) { const { value, allowClear, className, style, direction, bordered } = this.props; if (!allowClear) { return cloneElement(element, { value, }); } const affixWrapperCls = classNames( `${prefixCls}-affix-wrapper`, `${prefixCls}-affix-wrapper-textarea-with-clear-btn`, { [`${prefixCls}-affix-wrapper-rtl`]: direction === 'rtl', [`${prefixCls}-affix-wrapper-borderless`]: !bordered, // className will go to addon wrapper [`${className}`]: !hasAddon(this.props) && className, }, ); return ( {cloneElement(element, { style: null, value, })} {this.renderClearIcon(prefixCls)} ); } render() { const { prefixCls, inputType, element } = this.props; if (inputType === ClearableInputType[0]) { return this.renderTextAreaWithClearIcon(prefixCls, element); } return this.renderInputWithLabel(prefixCls, this.renderLabeledIcon(prefixCls, element)); } } export default ClearableLabeledInput;