1
This commit is contained in:
+216
@@ -0,0 +1,216 @@
|
||||
import * as React from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
import { useSelectInputContext } from "./context";
|
||||
import useLayoutEffect from "@rc-component/util/es/hooks/useLayoutEffect";
|
||||
import useBaseProps from "../hooks/useBaseProps";
|
||||
import { composeRef } from "@rc-component/util/es/ref";
|
||||
const Input = /*#__PURE__*/React.forwardRef((props, ref) => {
|
||||
const {
|
||||
onChange,
|
||||
onKeyDown,
|
||||
onBlur,
|
||||
style,
|
||||
syncWidth,
|
||||
value,
|
||||
className,
|
||||
autoComplete,
|
||||
...restProps
|
||||
} = props;
|
||||
const {
|
||||
prefixCls,
|
||||
mode,
|
||||
onSearch,
|
||||
onSearchSubmit,
|
||||
onInputBlur,
|
||||
autoFocus,
|
||||
tokenWithEnter,
|
||||
placeholder,
|
||||
components: {
|
||||
input: InputComponent = 'input'
|
||||
}
|
||||
} = useSelectInputContext();
|
||||
const {
|
||||
id,
|
||||
classNames,
|
||||
styles,
|
||||
open,
|
||||
activeDescendantId,
|
||||
role,
|
||||
disabled
|
||||
} = useBaseProps() || {};
|
||||
const inputCls = clsx(`${prefixCls}-input`, classNames?.input, className);
|
||||
|
||||
// Used to handle input method composition status
|
||||
const compositionStatusRef = React.useRef(false);
|
||||
|
||||
// Used to handle paste content, similar to original Selector implementation
|
||||
const pastedTextRef = React.useRef(null);
|
||||
|
||||
// ============================== Refs ==============================
|
||||
const inputRef = React.useRef(null);
|
||||
React.useImperativeHandle(ref, () => inputRef.current);
|
||||
|
||||
// ============================== Data ==============================
|
||||
// Handle input changes
|
||||
const handleChange = event => {
|
||||
let {
|
||||
value: nextVal
|
||||
} = event.target;
|
||||
|
||||
// Handle pasted text with tokenWithEnter, similar to original Selector implementation
|
||||
if (tokenWithEnter && pastedTextRef.current && /[\r\n]/.test(pastedTextRef.current)) {
|
||||
// CRLF will be treated as a single space for input element
|
||||
const replacedText = pastedTextRef.current.replace(/[\r\n]+$/, '').replace(/\r\n/g, ' ').replace(/[\r\n]/g, ' ');
|
||||
nextVal = nextVal.replace(replacedText, pastedTextRef.current);
|
||||
}
|
||||
|
||||
// Reset pasted text reference
|
||||
pastedTextRef.current = null;
|
||||
|
||||
// Call onSearch callback
|
||||
if (onSearch) {
|
||||
onSearch(nextVal, true, compositionStatusRef.current);
|
||||
}
|
||||
|
||||
// Call original onChange callback
|
||||
onChange?.(event);
|
||||
};
|
||||
|
||||
// ============================ Keyboard ============================
|
||||
// Handle keyboard events
|
||||
const handleKeyDown = event => {
|
||||
const {
|
||||
key
|
||||
} = event;
|
||||
const {
|
||||
value: nextVal
|
||||
} = event.currentTarget;
|
||||
|
||||
// Handle Enter key submission - referencing Selector implementation
|
||||
if (key === 'Enter' && mode === 'tags' && !open && !compositionStatusRef.current && onSearchSubmit) {
|
||||
onSearchSubmit(nextVal);
|
||||
}
|
||||
|
||||
// Call original onKeyDown callback
|
||||
onKeyDown?.(event);
|
||||
};
|
||||
|
||||
// Handle blur events
|
||||
const handleBlur = event => {
|
||||
// Call onInputBlur callback
|
||||
onInputBlur?.();
|
||||
|
||||
// Call original onBlur callback
|
||||
onBlur?.(event);
|
||||
};
|
||||
|
||||
// Handle input method composition start
|
||||
const handleCompositionStart = () => {
|
||||
compositionStatusRef.current = true;
|
||||
};
|
||||
|
||||
// Handle input method composition end
|
||||
const handleCompositionEnd = event => {
|
||||
compositionStatusRef.current = false;
|
||||
|
||||
// Trigger search when input method composition ends, similar to original Selector
|
||||
if (mode !== 'combobox') {
|
||||
const {
|
||||
value: nextVal
|
||||
} = event.currentTarget;
|
||||
onSearch?.(nextVal, true, false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle paste events to track pasted content
|
||||
const handlePaste = event => {
|
||||
const {
|
||||
clipboardData
|
||||
} = event;
|
||||
const pastedValue = clipboardData?.getData('text');
|
||||
pastedTextRef.current = pastedValue || '';
|
||||
};
|
||||
|
||||
// ============================= Width ==============================
|
||||
const [widthCssVar, setWidthCssVar] = React.useState(undefined);
|
||||
|
||||
// When syncWidth is enabled, adjust input width based on content
|
||||
useLayoutEffect(() => {
|
||||
const input = inputRef.current;
|
||||
if (syncWidth && input) {
|
||||
input.style.width = '0px';
|
||||
const scrollWidth = input.scrollWidth;
|
||||
setWidthCssVar(scrollWidth);
|
||||
|
||||
// Reset input style
|
||||
input.style.width = '';
|
||||
}
|
||||
}, [syncWidth, value]);
|
||||
|
||||
// ============================= Render =============================
|
||||
// Extract shared input props
|
||||
const sharedInputProps = {
|
||||
id,
|
||||
type: mode === 'combobox' ? 'text' : 'search',
|
||||
...restProps,
|
||||
ref: inputRef,
|
||||
style: {
|
||||
...styles?.input,
|
||||
...style,
|
||||
'--select-input-width': widthCssVar
|
||||
},
|
||||
autoFocus,
|
||||
autoComplete: autoComplete || 'off',
|
||||
className: inputCls,
|
||||
disabled,
|
||||
value: value || '',
|
||||
onChange: handleChange,
|
||||
onKeyDown: handleKeyDown,
|
||||
onBlur: handleBlur,
|
||||
onPaste: handlePaste,
|
||||
onCompositionStart: handleCompositionStart,
|
||||
onCompositionEnd: handleCompositionEnd,
|
||||
// Accessibility attributes
|
||||
role: role || 'combobox',
|
||||
'aria-expanded': open || false,
|
||||
'aria-haspopup': 'listbox',
|
||||
'aria-owns': open ? `${id}_list` : undefined,
|
||||
'aria-autocomplete': 'list',
|
||||
'aria-controls': open ? `${id}_list` : undefined,
|
||||
'aria-activedescendant': open ? activeDescendantId : undefined
|
||||
};
|
||||
|
||||
// Handle different InputComponent types
|
||||
if ( /*#__PURE__*/React.isValidElement(InputComponent)) {
|
||||
// If InputComponent is a ReactElement, use cloneElement with merged props
|
||||
const existingProps = InputComponent.props || {};
|
||||
|
||||
// Start with shared props as base
|
||||
const mergedProps = {
|
||||
placeholder: props.placeholder || placeholder,
|
||||
...sharedInputProps,
|
||||
...existingProps
|
||||
};
|
||||
|
||||
// Batch update function calls
|
||||
Object.keys(existingProps).forEach(key => {
|
||||
const existingValue = existingProps[key];
|
||||
if (typeof existingValue === 'function') {
|
||||
// Merge event handlers
|
||||
mergedProps[key] = (...args) => {
|
||||
existingValue(...args);
|
||||
sharedInputProps[key]?.(...args);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Update ref
|
||||
mergedProps.ref = composeRef(InputComponent.ref, sharedInputProps.ref);
|
||||
return /*#__PURE__*/React.cloneElement(InputComponent, mergedProps);
|
||||
}
|
||||
|
||||
// If InputComponent is a component type, render normally
|
||||
const Component = InputComponent;
|
||||
return /*#__PURE__*/React.createElement(Component, sharedInputProps);
|
||||
});
|
||||
export default Input;
|
||||
Reference in New Issue
Block a user