This commit is contained in:
2026-05-25 17:08:18 +08:00
parent df501f6151
commit f1259b71b5
9178 changed files with 1626125 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
import React from 'react';
import type { BaseInputProps } from './interface';
export interface HolderRef {
/** Provider holder ref. Will return `null` if not wrap anything */
nativeElement: HTMLElement | null;
}
declare const BaseInput: React.ForwardRefExoticComponent<BaseInputProps & React.RefAttributes<HolderRef>>;
export default BaseInput;
+139
View File
@@ -0,0 +1,139 @@
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
import { clsx } from 'clsx';
import React, { cloneElement, useRef } from 'react';
import { hasAddon, hasPrefixSuffix } from "./utils/commonUtils";
const BaseInput = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
inputElement: inputEl,
children,
prefixCls,
prefix,
suffix,
addonBefore,
addonAfter,
className,
style,
disabled,
readOnly,
focused,
triggerFocus,
allowClear,
value,
handleReset,
hidden,
classes,
classNames,
dataAttrs,
styles,
components,
onClear
} = props;
const inputElement = children ?? inputEl;
const AffixWrapperComponent = components?.affixWrapper || 'span';
const GroupWrapperComponent = components?.groupWrapper || 'span';
const WrapperComponent = components?.wrapper || 'span';
const GroupAddonComponent = components?.groupAddon || 'span';
const containerRef = useRef(null);
const onInputClick = e => {
if (containerRef.current?.contains(e.target)) {
triggerFocus?.();
}
};
const hasAffix = hasPrefixSuffix(props);
let element = /*#__PURE__*/cloneElement(inputElement, {
value,
className: clsx(inputElement.props?.className, !hasAffix && classNames?.variant) || null
});
// ======================== Ref ======================== //
const groupRef = useRef(null);
React.useImperativeHandle(ref, () => ({
nativeElement: groupRef.current || containerRef.current
}));
// ================== Prefix & Suffix ================== //
if (hasAffix) {
// ================== Clear Icon ================== //
let clearIcon = null;
if (allowClear) {
const needClear = !disabled && !readOnly && value;
const clearIconCls = `${prefixCls}-clear-icon`;
const iconNode = typeof allowClear === 'object' && allowClear?.clearIcon ? allowClear.clearIcon : '✖';
clearIcon = /*#__PURE__*/React.createElement("button", {
type: "button",
tabIndex: -1,
onClick: event => {
handleReset?.(event);
onClear?.();
}
// Do not trigger onBlur when clear input
// https://github.com/ant-design/ant-design/issues/31200
,
onMouseDown: e => e.preventDefault(),
className: clsx(clearIconCls, {
[`${clearIconCls}-hidden`]: !needClear,
[`${clearIconCls}-has-suffix`]: !!suffix
})
}, iconNode);
}
const affixWrapperPrefixCls = `${prefixCls}-affix-wrapper`;
const affixWrapperCls = clsx(affixWrapperPrefixCls, {
[`${prefixCls}-disabled`]: disabled,
[`${affixWrapperPrefixCls}-disabled`]: disabled,
// Not used, but keep it
[`${affixWrapperPrefixCls}-focused`]: focused,
// Not used, but keep it
[`${affixWrapperPrefixCls}-readonly`]: readOnly,
[`${affixWrapperPrefixCls}-input-with-clear-btn`]: suffix && allowClear && value
}, classes?.affixWrapper, classNames?.affixWrapper, classNames?.variant);
const suffixNode = (suffix || allowClear) && /*#__PURE__*/React.createElement("span", {
className: clsx(`${prefixCls}-suffix`, classNames?.suffix),
style: styles?.suffix
}, clearIcon, suffix);
element = /*#__PURE__*/React.createElement(AffixWrapperComponent, _extends({
className: affixWrapperCls,
style: styles?.affixWrapper,
onClick: onInputClick
}, dataAttrs?.affixWrapper, {
ref: containerRef
}), prefix && /*#__PURE__*/React.createElement("span", {
className: clsx(`${prefixCls}-prefix`, classNames?.prefix),
style: styles?.prefix
}, prefix), element, suffixNode);
}
// ================== Addon ================== //
if (hasAddon(props)) {
const wrapperCls = `${prefixCls}-group`;
const addonCls = `${wrapperCls}-addon`;
const groupWrapperCls = `${wrapperCls}-wrapper`;
const mergedWrapperClassName = clsx(`${prefixCls}-wrapper`, wrapperCls, classes?.wrapper, classNames?.wrapper);
const mergedGroupClassName = clsx(groupWrapperCls, {
[`${groupWrapperCls}-disabled`]: disabled
}, classes?.group, classNames?.groupWrapper);
// Need another wrapper for changing display:table to display:inline-block
// and put style prop in wrapper
element = /*#__PURE__*/React.createElement(GroupWrapperComponent, {
className: mergedGroupClassName,
ref: groupRef
}, /*#__PURE__*/React.createElement(WrapperComponent, {
className: mergedWrapperClassName
}, addonBefore && /*#__PURE__*/React.createElement(GroupAddonComponent, {
className: addonCls
}, addonBefore), element, addonAfter && /*#__PURE__*/React.createElement(GroupAddonComponent, {
className: addonCls
}, addonAfter)));
}
// `className` and `style` are always on the root element
return /*#__PURE__*/React.cloneElement(element, {
className: clsx(element.props?.className, className) || null,
style: {
...element.props?.style,
...style
},
hidden
});
});
export default BaseInput;
+4
View File
@@ -0,0 +1,4 @@
import React from 'react';
import type { InputProps, InputRef } from './interface';
declare const Input: React.ForwardRefExoticComponent<InputProps & React.RefAttributes<InputRef>>;
export default Input;
+215
View File
@@ -0,0 +1,215 @@
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
import { clsx } from 'clsx';
import useControlledState from "@rc-component/util/es/hooks/useControlledState";
import omit from "@rc-component/util/es/omit";
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
import BaseInput from "./BaseInput";
import useCount from "./hooks/useCount";
import { resolveOnChange } from "./utils/commonUtils";
import { triggerFocus } from "@rc-component/util/es/Dom/focus";
const Input = /*#__PURE__*/forwardRef((props, ref) => {
const {
autoComplete,
onChange,
onFocus,
onBlur,
onPressEnter,
onKeyDown,
onKeyUp,
prefixCls = 'rc-input',
disabled,
htmlSize,
className,
maxLength,
suffix,
showCount,
count,
type = 'text',
classes,
classNames,
styles,
onCompositionStart,
onCompositionEnd,
...rest
} = props;
const [focused, setFocused] = useState(false);
const compositionRef = useRef(false);
const keyLockRef = useRef(false);
const inputRef = useRef(null);
const holderRef = useRef(null);
const focus = option => {
if (inputRef.current) {
triggerFocus(inputRef.current, option);
}
};
// ====================== Value =======================
const [value, setValue] = useControlledState(props.defaultValue, props.value);
const formatValue = value === undefined || value === null ? '' : String(value);
// =================== Select Range ===================
const [selection, setSelection] = useState(null);
// ====================== Count =======================
const countConfig = useCount(count, showCount);
const mergedMax = countConfig.max || maxLength;
const valueLength = countConfig.strategy(formatValue);
const isOutOfRange = !!mergedMax && valueLength > mergedMax;
// ======================= Ref ========================
useImperativeHandle(ref, () => ({
focus,
blur: () => {
inputRef.current?.blur();
},
setSelectionRange: (start, end, direction) => {
inputRef.current?.setSelectionRange(start, end, direction);
},
select: () => {
inputRef.current?.select();
},
input: inputRef.current,
nativeElement: holderRef.current?.nativeElement || inputRef.current
}));
useEffect(() => {
if (keyLockRef.current) {
keyLockRef.current = false;
}
setFocused(prev => prev && disabled ? false : prev);
}, [disabled]);
const triggerChange = (e, currentValue, info) => {
let cutValue = currentValue;
if (!compositionRef.current && countConfig.exceedFormatter && countConfig.max && countConfig.strategy(currentValue) > countConfig.max) {
cutValue = countConfig.exceedFormatter(currentValue, {
max: countConfig.max
});
if (currentValue !== cutValue) {
setSelection([inputRef.current?.selectionStart || 0, inputRef.current?.selectionEnd || 0]);
}
} else if (info.source === 'compositionEnd') {
// Avoid triggering twice
// https://github.com/ant-design/ant-design/issues/46587
return;
}
setValue(cutValue);
if (inputRef.current) {
resolveOnChange(inputRef.current, e, onChange, cutValue);
}
};
useEffect(() => {
if (selection) {
inputRef.current?.setSelectionRange(...selection);
}
}, [selection]);
const onInternalChange = e => {
triggerChange(e, e.target.value, {
source: 'change'
});
};
const onInternalCompositionEnd = e => {
compositionRef.current = false;
triggerChange(e, e.currentTarget.value, {
source: 'compositionEnd'
});
onCompositionEnd?.(e);
};
const handleKeyDown = e => {
if (onPressEnter && e.key === 'Enter' && !keyLockRef.current && !e.nativeEvent.isComposing) {
keyLockRef.current = true;
onPressEnter(e);
}
onKeyDown?.(e);
};
const handleKeyUp = e => {
if (e.key === 'Enter') {
keyLockRef.current = false;
}
onKeyUp?.(e);
};
const handleFocus = e => {
setFocused(true);
onFocus?.(e);
};
const handleBlur = e => {
if (keyLockRef.current) {
keyLockRef.current = false;
}
setFocused(false);
onBlur?.(e);
};
const handleReset = e => {
setValue('');
focus();
if (inputRef.current) {
resolveOnChange(inputRef.current, e, onChange);
}
};
// ====================== Input =======================
const outOfRangeCls = isOutOfRange && `${prefixCls}-out-of-range`;
const getInputElement = () => {
// Fix https://fb.me/react-unknown-prop
const otherProps = omit(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', 'showCount', 'count', 'classes', 'htmlSize', 'styles', 'classNames', 'onClear']);
return /*#__PURE__*/React.createElement("input", _extends({
autoComplete: autoComplete
}, otherProps, {
onChange: onInternalChange,
onFocus: handleFocus,
onBlur: handleBlur,
onKeyDown: handleKeyDown,
onKeyUp: handleKeyUp,
className: clsx(prefixCls, {
[`${prefixCls}-disabled`]: disabled
}, classNames?.input),
style: styles?.input,
ref: inputRef,
size: htmlSize,
type: type,
onCompositionStart: e => {
compositionRef.current = true;
onCompositionStart?.(e);
},
onCompositionEnd: onInternalCompositionEnd
}));
};
const getSuffix = () => {
// Max length value
const hasMaxLength = Number(mergedMax) > 0;
if (suffix || countConfig.show) {
const dataCount = countConfig.showFormatter ? countConfig.showFormatter({
value: formatValue,
count: valueLength,
maxLength: mergedMax
}) : `${valueLength}${hasMaxLength ? ` / ${mergedMax}` : ''}`;
return /*#__PURE__*/React.createElement(React.Fragment, null, countConfig.show && /*#__PURE__*/React.createElement("span", {
className: clsx(`${prefixCls}-show-count-suffix`, {
[`${prefixCls}-show-count-has-suffix`]: !!suffix
}, classNames?.count),
style: {
...styles?.count
}
}, dataCount), suffix);
}
return null;
};
// ====================== Render ======================
return /*#__PURE__*/React.createElement(BaseInput, _extends({}, rest, {
prefixCls: prefixCls,
className: clsx(className, outOfRangeCls),
handleReset: handleReset,
value: formatValue,
focused: focused,
triggerFocus: focus,
suffix: getSuffix(),
disabled: disabled,
classes: classes,
classNames: classNames,
styles: styles,
ref: holderRef
}), getInputElement());
});
export default Input;
@@ -0,0 +1,12 @@
import type { InputProps } from '..';
import type { CountConfig, ShowCountFormatter } from '../interface';
type ForcedCountConfig = Omit<CountConfig, 'show'> & Pick<Required<CountConfig>, 'strategy'> & {
show: boolean;
showFormatter?: ShowCountFormatter;
};
/**
* Cut `value` by the `count.max` prop.
*/
export declare function inCountRange(value: string, countConfig: ForcedCountConfig): boolean;
export default function useCount(count?: CountConfig, showCount?: InputProps['showCount']): ForcedCountConfig;
export {};
+33
View File
@@ -0,0 +1,33 @@
import * as React from 'react';
/**
* Cut `value` by the `count.max` prop.
*/
export function inCountRange(value, countConfig) {
if (!countConfig.max) {
return true;
}
const count = countConfig.strategy(value);
return count <= countConfig.max;
}
export default function useCount(count, showCount) {
return React.useMemo(() => {
let mergedConfig = {};
if (showCount) {
mergedConfig.show = typeof showCount === 'object' && showCount.formatter ? showCount.formatter : !!showCount;
}
mergedConfig = {
...mergedConfig,
...count
};
const {
show,
...rest
} = mergedConfig;
return {
...rest,
show: !!show,
showFormatter: typeof show === 'function' ? show : undefined,
strategy: rest.strategy || (value => value.length)
};
}, [count, showCount]);
}
+5
View File
@@ -0,0 +1,5 @@
import BaseInput from './BaseInput';
import Input from './Input';
export { BaseInput };
export type { InputProps, InputRef } from './interface';
export default Input;
+4
View File
@@ -0,0 +1,4 @@
import BaseInput from "./BaseInput";
import Input from "./Input";
export { BaseInput };
export default Input;
+107
View File
@@ -0,0 +1,107 @@
import type { CSSProperties, InputHTMLAttributes, KeyboardEventHandler, MouseEventHandler, ReactElement, ReactNode } from 'react';
import type { LiteralUnion } from './utils/types';
import type { InputFocusOptions } from '@rc-component/util/lib/Dom/focus';
export interface CommonInputProps {
prefix?: ReactNode;
suffix?: ReactNode;
addonBefore?: ReactNode;
addonAfter?: ReactNode;
/** @deprecated Use `classNames` instead */
classes?: {
affixWrapper?: string;
group?: string;
wrapper?: string;
};
classNames?: {
affixWrapper?: string;
prefix?: string;
suffix?: string;
groupWrapper?: string;
wrapper?: string;
variant?: string;
};
styles?: {
affixWrapper?: CSSProperties;
prefix?: CSSProperties;
suffix?: CSSProperties;
};
allowClear?: boolean | {
clearIcon?: ReactNode;
};
}
type DataAttr = Record<`data-${string}`, string>;
export type ValueType = InputHTMLAttributes<HTMLInputElement>['value'] | bigint;
export interface BaseInputProps extends CommonInputProps {
value?: ValueType;
/** @deprecated Use `children` instead */
inputElement?: ReactElement;
prefixCls?: string;
className?: string;
style?: CSSProperties;
disabled?: boolean;
focused?: boolean;
triggerFocus?: () => void;
readOnly?: boolean;
handleReset?: MouseEventHandler;
onClear?: () => void;
hidden?: boolean;
dataAttrs?: {
affixWrapper?: DataAttr;
};
components?: {
affixWrapper?: 'span' | 'div';
groupWrapper?: 'span' | 'div';
wrapper?: 'span' | 'div';
groupAddon?: 'span' | 'div';
};
children: ReactElement;
}
export type ShowCountFormatter = (args: {
value: string;
count: number;
maxLength?: number;
}) => ReactNode;
export type ExceedFormatter = (value: string, config: {
max: number;
}) => string;
export interface CountConfig {
max?: number;
strategy?: (value: string) => number;
show?: boolean | ShowCountFormatter;
/** Trigger when content larger than the `max` limitation */
exceedFormatter?: ExceedFormatter;
}
export interface InputProps extends CommonInputProps, Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'prefix' | 'type' | 'value'> {
value?: ValueType;
prefixCls?: string;
type?: LiteralUnion<'button' | 'checkbox' | 'color' | 'date' | 'datetime-local' | 'email' | 'file' | 'hidden' | 'image' | 'month' | 'number' | 'password' | 'radio' | 'range' | 'reset' | 'search' | 'submit' | 'tel' | 'text' | 'time' | 'url' | 'week', string>;
onPressEnter?: KeyboardEventHandler<HTMLInputElement>;
/** It's better to use `count.show` instead */
showCount?: boolean | {
formatter: ShowCountFormatter;
};
autoComplete?: string;
htmlSize?: number;
classNames?: CommonInputProps['classNames'] & {
input?: string;
count?: string;
};
styles?: CommonInputProps['styles'] & {
input?: CSSProperties;
count?: CSSProperties;
};
count?: CountConfig;
onClear?: () => void;
}
export interface InputRef {
focus: (options?: InputFocusOptions) => void;
blur: () => void;
setSelectionRange: (start: number, end: number, direction?: 'forward' | 'backward' | 'none') => void;
select: () => void;
input: HTMLInputElement | null;
nativeElement: HTMLElement | null;
}
export interface ChangeEventInfo {
source: 'compositionEnd' | 'change';
}
export {};
+1
View File
@@ -0,0 +1 @@
export {};
@@ -0,0 +1,5 @@
import type React from 'react';
import type { BaseInputProps, InputProps } from '../interface';
export declare function hasAddon(props: BaseInputProps | InputProps): boolean;
export declare function hasPrefixSuffix(props: BaseInputProps | InputProps): boolean;
export declare function resolveOnChange<E extends HTMLInputElement | HTMLTextAreaElement>(target: E, e: React.ChangeEvent<E> | React.MouseEvent<HTMLElement, MouseEvent> | React.CompositionEvent<HTMLElement>, onChange: undefined | ((event: React.ChangeEvent<E>) => void), targetValue?: string): void;
@@ -0,0 +1,71 @@
export function hasAddon(props) {
return !!(props.addonBefore || props.addonAfter);
}
export function hasPrefixSuffix(props) {
return !!(props.prefix || props.suffix || props.allowClear);
}
// TODO: It's better to use `Proxy` replace the `element.value`. But we still need support IE11.
function cloneEvent(event, target, value) {
// A bug report filed on WebKit's Bugzilla tracker, dating back to 2009, specifically addresses the issue of cloneNode() not copying files of <input type="file"> elements.
// As of the last update, this bug was still marked as "NEW," indicating that it might not have been resolved yet.
// https://bugs.webkit.org/show_bug.cgi?id=28123
const currentTarget = target.cloneNode(true);
// click clear icon
const newEvent = Object.create(event, {
target: {
value: currentTarget
},
currentTarget: {
value: currentTarget
}
});
// Fill data
currentTarget.value = value;
// Fill selection. Some type like `email` not support selection
// https://github.com/ant-design/ant-design/issues/47833
if (typeof target.selectionStart === 'number' && typeof target.selectionEnd === 'number') {
currentTarget.selectionStart = target.selectionStart;
currentTarget.selectionEnd = target.selectionEnd;
}
currentTarget.setSelectionRange = (...args) => {
target.setSelectionRange(...args);
};
return newEvent;
}
export function resolveOnChange(target, e, onChange, targetValue) {
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('');
// <Input
// allowClear
// value={query}
// onChange={(e)=> {
// setQuery((prevStatus) => e.target.value);
// }}
// />
event = cloneEvent(e, target, '');
onChange(event);
return;
}
// Trigger by composition event, this means we need force change the input value
// https://github.com/ant-design/ant-design/issues/45737
// https://github.com/ant-design/ant-design/issues/46598
if (target.type !== 'file' && targetValue !== undefined) {
event = cloneEvent(e, target, targetValue);
onChange(event);
return;
}
onChange(event);
}
+2
View File
@@ -0,0 +1,2 @@
/** https://github.com/Microsoft/TypeScript/issues/29729 */
export type LiteralUnion<T extends U, U> = T | (U & Record<never, never>);
+1
View File
@@ -0,0 +1 @@
export {};