@@ -37,9 +37,25 @@ interface IProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'ty
3737 forwardedRef ?: React . MutableRefObject < HTMLInputElement >
3838}
3939
40- class Input extends React . Component < IProps , null > {
40+ interface IState {
41+ compositionValue ?: string
42+ }
43+
44+
45+ /**
46+ * 谷歌浏览器: compositionstart -> onChange -> compositionend
47+ * 其他浏览器: compositionstart -> compositionend -> onChange
48+ * 普通按键 (A-Z): handleInput -> setState(compositionValue) -> UI 更新。
49+ * 空格选词 (中文输入法): compositionend -> triggerValueChange(外部回调) -> onInputExecuted = true -> 紧随其后的 handleInput 被拦截退出。
50+ */
51+
52+ class Input extends React . Component < IProps , IState > {
4153 constructor ( props ) {
4254 super ( props )
55+ this . state = {
56+ compositionValue : undefined
57+ }
58+
4359 this . handleInput = this . handleInput . bind ( this )
4460 this . handlePaste = this . handlePaste . bind ( this )
4561 this . handleFocus = this . handleFocus . bind ( this )
@@ -48,12 +64,13 @@ class Input extends React.Component<IProps, null> {
4864 this . handleComposition = this . handleComposition . bind ( this )
4965 this . handleBeforeInput = this . handleBeforeInput . bind ( this )
5066 this . isOnComposition = false
51- this . onInputExcuted = false
67+ // onInputExecuted 标记用于防止某些浏览器的事件重复触发
68+ this . onInputExecuted = false
5269 }
5370
5471 inputRef : HTMLInputElement
5572 isOnComposition : boolean
56- onInputExcuted : boolean
73+ onInputExecuted : boolean
5774
5875 componentDidMount ( ) {
5976 // 修复无法选择文件
@@ -80,8 +97,10 @@ class Input extends React.Component<IProps, null> {
8097 if ( ! this . props . focus && nextProps . focus && this . inputRef ) this . inputRef . focus ( )
8198 }
8299
83- handleInput ( e ) {
84- e . stopPropagation ( )
100+ /**
101+ * 处理 maxLength 逻辑并调用 props.onInput
102+ */
103+ triggerValueChange ( value : string , e : any ) {
85104 const {
86105 type,
87106 maxlength = 140 ,
@@ -90,18 +109,23 @@ class Input extends React.Component<IProps, null> {
90109 onInput
91110 } = this . props
92111
93- if ( ! this . isOnComposition && ! this . onInputExcuted ) {
94- let { value } = e . target
95- const inputType = getTrueType ( type , confirmType , password )
96- this . onInputExcuted = true
97- /* 修复 number 类型 maxLength 无效 */
98- if ( inputType === 'number' && value && maxlength <= value . length ) {
99- value = value . substring ( 0 , maxlength )
100- e . target . value = value
112+ let finalValue = value
113+ const inputType = getTrueType ( type , confirmType , password )
114+
115+ /* 修复 number 类型 maxLength 无效 */
116+ if ( inputType === 'number' && finalValue && maxlength <= finalValue . length ) {
117+ finalValue = finalValue . substring ( 0 , maxlength )
118+ // 如果被截断了,需要同步回 DOM
119+ if ( e . target && e . target . value !== finalValue ) {
120+ e . target . value = finalValue
101121 }
122+ }
102123
124+ // 只有当值确实改变,或者需要强制触发时才调用
125+ if ( typeof onInput === 'function' ) {
103126 Object . defineProperty ( e , 'detail' , {
104- value : { value, cursor : value . length }
127+ value : { value : finalValue , cursor : finalValue . length } ,
128+ configurable : true
105129 } )
106130 // // 修复 IOS 光标跳转问题
107131 // if (!(['number', 'file'].indexOf(inputType) >= 0)) {
@@ -113,16 +137,45 @@ class Input extends React.Component<IProps, null> {
113137 // }
114138 // )
115139 // }
140+ onInput ( e )
141+ }
142+ }
116143
117- typeof onInput === 'function' && onInput ( e )
118- this . onInputExcuted = false
144+ handleInput ( e ) {
145+ e . stopPropagation ( )
146+ // 如果是 compositionend 刚刚触发过的,这里消费掉标记并退出,防止双重触发
147+ // 适配其他浏览器的 compositionend -> onChange 顺序
148+ if ( this . onInputExecuted ) {
149+ this . onInputExecuted = false
150+ return
151+ }
152+
153+ const newValue = e . target . value
154+
155+ if ( this . isOnComposition ) {
156+ // Case 1: 正在拼写中文(compositionstart 已触发但 compositionend 未触发)
157+ // 只更新组件内部 State,让 Input 显示拼音,不触发外部 onChange
158+ // 适配谷歌浏览器的 compositionstart -> onChange -> compositionend 顺序
159+ this . setState ( { compositionValue : newValue } )
160+ } else {
161+ // Case 2: 普通输入 (英文、数字、或中文选词后)
162+ // 标记执行,防止重复
163+ this . onInputExecuted = true
164+
165+ // 清理中间状态
166+ if ( this . state . compositionValue !== undefined ) {
167+ this . setState ( { compositionValue : undefined } )
168+ }
169+
170+ this . triggerValueChange ( newValue , e )
171+ this . onInputExecuted = false
119172 }
120173 }
121174
122175 handlePaste ( e ) {
123176 e . stopPropagation ( )
124177 const { onPaste } = this . props
125- this . onInputExcuted = false
178+ this . onInputExecuted = false
126179 Object . defineProperty ( e , 'detail' , {
127180 value : {
128181 value : e . target . value
@@ -134,7 +187,7 @@ class Input extends React.Component<IProps, null> {
134187 handleFocus ( e ) {
135188 e . stopPropagation ( )
136189 const { onFocus } = this . props
137- this . onInputExcuted = false
190+ this . onInputExecuted = false
138191 Object . defineProperty ( e , 'detail' , {
139192 value : {
140193 value : e . target . value
@@ -159,7 +212,7 @@ class Input extends React.Component<IProps, null> {
159212 const { onConfirm, onKeyDown } = this . props
160213 const { value } = e . target
161214 const keyCode = e . keyCode || e . code
162- this . onInputExcuted = false
215+ this . onInputExecuted = false
163216
164217 if ( typeof onKeyDown === 'function' ) {
165218 Object . defineProperty ( e , 'detail' , {
@@ -186,11 +239,28 @@ class Input extends React.Component<IProps, null> {
186239 e . stopPropagation ( )
187240 if ( ! ( e . target instanceof HTMLInputElement ) ) return
188241
189- if ( e . type === 'compositionend' ) {
190- this . isOnComposition = false
191- this . handleInput ( e )
192- } else {
242+ if ( e . type === 'compositionstart' ) {
243+ // 开始输入中文,标记进入拼音输入状态
244+ this . isOnComposition = true
245+ } else if ( e . type === 'compositionupdate' ) {
246+ // 拼音输入过程中,保持标记并更新显示
193247 this . isOnComposition = true
248+ // 必须在这里触发 setState 才能让输入框里的拼音实时更新
249+ this . handleInput ( e )
250+ } else if ( e . type === 'compositionend' ) {
251+ // 中文选词结束,退出拼音输入状态
252+ this . isOnComposition = false
253+ // 立即获取最终值
254+ const newValue = e . target . value
255+
256+ // 清空中间状态
257+ this . setState ( { compositionValue : undefined } )
258+
259+ // 设置标记,防止后续的 handleInput 重复触发(适配其他浏览器)
260+ this . onInputExecuted = true
261+
262+ // 强制触发一次 value change,确保父组件收到最终汉字
263+ this . triggerValueChange ( newValue , e )
194264 }
195265 }
196266
@@ -219,6 +289,9 @@ class Input extends React.Component<IProps, null> {
219289 name,
220290 value
221291 } = this . props
292+
293+ const { compositionValue } = this . state
294+
222295 const cls = classNames ( 'taro-input-core' , 'weui-input' , className )
223296
224297 const otherProps = omit ( this . props , [
@@ -231,12 +304,15 @@ class Input extends React.Component<IProps, null> {
231304 'maxlength' ,
232305 'confirmType' ,
233306 'focus' ,
234- 'name'
307+ 'name' ,
308+ 'onInput'
235309 ] )
236310
237- if ( 'value' in this . props ) {
238- otherProps . value = fixControlledValue ( value )
239- }
311+ // 如果有 compositionValue (正在输入拼音),则显示 compositionValue
312+ // 否则显示 props 传进来的受控 value
313+ const displayValue = compositionValue !== undefined
314+ ? compositionValue
315+ : fixControlledValue ( value )
240316
241317 return (
242318 < input
@@ -253,12 +329,14 @@ class Input extends React.Component<IProps, null> {
253329 disabled = { disabled }
254330 maxLength = { maxlength }
255331 name = { name }
332+ value = { displayValue }
256333 onInput = { this . handleInput }
257334 onPaste = { this . handlePaste }
258335 onFocus = { this . handleFocus }
259336 onBlur = { this . handleBlur }
260337 onKeyDown = { this . handleKeyDown }
261338 onCompositionStart = { this . handleComposition }
339+ onCompositionUpdate = { this . handleComposition }
262340 onCompositionEnd = { this . handleComposition }
263341 onBeforeInput = { this . handleBeforeInput }
264342 />
0 commit comments