import React, { forwardRef, useContext, useEffect, useRef } from 'react'; import type { InputProps as RcInputProps, InputRef } from 'rc-input'; import RcInput from 'rc-input'; import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled'; import classNames from 'classnames'; import { composeRef } from 'rc-util/lib/ref'; import type { SizeType } from '../config-provider/SizeContext'; import SizeContext from '../config-provider/SizeContext'; import type { InputStatus } from '../_util/statusUtils'; import DisabledContext from '../config-provider/DisabledContext'; import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils'; import { ConfigContext } from '../config-provider'; import { FormItemInputContext, NoFormStatus } from '../form/context'; import { hasPrefixSuffix } from './utils'; import devWarning from '../_util/devWarning'; // CSSINJS import useStyle from './style'; export interface InputFocusOptions extends FocusOptions { cursor?: 'start' | 'end' | 'all'; } export type { InputRef }; export function fixControlledValue(value: T) { if (typeof value === 'undefined' || value === null) { return ''; } return String(value); } export function resolveOnChange( target: E, e: | React.ChangeEvent | React.MouseEvent | React.CompositionEvent, onChange: undefined | ((event: React.ChangeEvent) => void), targetValue?: string, ) { if (!onChange) { return; } let event = e; if (e.type === 'click') { // Clone a new target for event. // Avoid the following usage, the setQuery method gets the original value. // // const [query, setQuery] = React.useState(''); // { // setQuery((prevStatus) => e.target.value); // }} // /> const currentTarget = target.cloneNode(true) as E; // click clear icon event = Object.create(e, { target: { value: currentTarget }, currentTarget: { value: currentTarget }, }); currentTarget.value = ''; onChange(event as React.ChangeEvent); return; } // Trigger by composition event, this means we need force change the input value if (targetValue !== undefined) { event = Object.create(e, { target: { value: target }, currentTarget: { value: target }, }); target.value = targetValue; onChange(event as React.ChangeEvent); return; } onChange(event as React.ChangeEvent); } export function triggerFocus( element?: HTMLInputElement | HTMLTextAreaElement, option?: InputFocusOptions, ) { if (!element) return; element.focus(option); // Selection content const { cursor } = option || {}; if (cursor) { const len = element.value.length; switch (cursor) { case 'start': element.setSelectionRange(0, 0); break; case 'end': element.setSelectionRange(len, len); break; default: element.setSelectionRange(0, len); } } } export interface InputProps extends Omit< RcInputProps, 'wrapperClassName' | 'groupClassName' | 'inputClassName' | 'affixWrapperClassName' > { size?: SizeType; disabled?: boolean; status?: InputStatus; bordered?: boolean; [key: `data-${string}`]: string; } const Input = forwardRef((props, ref) => { const { prefixCls: customizePrefixCls, bordered = true, status: customStatus, size: customSize, disabled: customDisabled, onBlur, onFocus, suffix, allowClear, addonAfter, addonBefore, ...rest } = props; const { getPrefixCls, direction, input } = React.useContext(ConfigContext); const prefixCls = getPrefixCls('input', customizePrefixCls); const inputRef = useRef(null); // Style const [wrapSSR, hashId] = useStyle(prefixCls); // ===================== Size ===================== const size = React.useContext(SizeContext); const mergedSize = customSize || size; // ===================== Disabled ===================== const disabled = React.useContext(DisabledContext); const mergedDisabled = customDisabled || disabled; // ===================== Status ===================== const { status: contextStatus, hasFeedback, feedbackIcon } = useContext(FormItemInputContext); const mergedStatus = getMergedStatus(contextStatus, customStatus); // ===================== Focus warning ===================== const inputHasPrefixSuffix = hasPrefixSuffix(props) || !!hasFeedback; const prevHasPrefixSuffix = useRef(inputHasPrefixSuffix); useEffect(() => { if (inputHasPrefixSuffix && !prevHasPrefixSuffix.current) { devWarning( document.activeElement === inputRef.current?.input, '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`, ); } prevHasPrefixSuffix.current = inputHasPrefixSuffix; }, [inputHasPrefixSuffix]); // ===================== Remove Password value ===================== const removePasswordTimeoutRef = useRef([]); const removePasswordTimeout = () => { removePasswordTimeoutRef.current.push( window.setTimeout(() => { if ( inputRef.current?.input && inputRef.current?.input.getAttribute('type') === 'password' && inputRef.current?.input.hasAttribute('value') ) { inputRef.current?.input.removeAttribute('value'); } }), ); }; useEffect(() => { removePasswordTimeout(); return () => removePasswordTimeoutRef.current.forEach(item => window.clearTimeout(item)); }, []); const handleBlur = (e: React.FocusEvent) => { removePasswordTimeout(); onBlur?.(e); }; const handleFocus = (e: React.FocusEvent) => { removePasswordTimeout(); onFocus?.(e); }; const suffixNode = (hasFeedback || suffix) && ( <> {suffix} {hasFeedback && feedbackIcon} ); // Allow clear let mergedAllowClear; if (typeof allowClear === 'object' && allowClear?.clearIcon) { mergedAllowClear = allowClear; } else if (allowClear) { mergedAllowClear = { clearIcon: }; } return wrapSSR( {addonAfter}} addonBefore={addonBefore && {addonBefore}} inputClassName={classNames( { [`${prefixCls}-sm`]: mergedSize === 'small', [`${prefixCls}-lg`]: mergedSize === 'large', [`${prefixCls}-rtl`]: direction === 'rtl', [`${prefixCls}-borderless`]: !bordered, }, !inputHasPrefixSuffix && getStatusClassNames(prefixCls, mergedStatus), hashId, )} affixWrapperClassName={classNames( { [`${prefixCls}-affix-wrapper-sm`]: mergedSize === 'small', [`${prefixCls}-affix-wrapper-lg`]: mergedSize === 'large', [`${prefixCls}-affix-wrapper-rtl`]: direction === 'rtl', [`${prefixCls}-affix-wrapper-borderless`]: !bordered, }, getStatusClassNames(`${prefixCls}-affix-wrapper`, mergedStatus, hasFeedback), hashId, )} wrapperClassName={classNames( { [`${prefixCls}-group-rtl`]: direction === 'rtl', }, hashId, )} groupClassName={classNames( { [`${prefixCls}-group-wrapper-sm`]: mergedSize === 'small', [`${prefixCls}-group-wrapper-lg`]: mergedSize === 'large', [`${prefixCls}-group-wrapper-rtl`]: direction === 'rtl', }, getStatusClassNames(`${prefixCls}-group-wrapper`, mergedStatus, hasFeedback), hashId, )} />, ); }); export default Input;