Skip to content

Commit faf8d0c

Browse files
committed
fix(components-react): 修复 Input 组件受控模式下无法输入中文
1 parent fffb1ce commit faf8d0c

File tree

1 file changed

+105
-27
lines changed
  • packages/taro-components-react/src/components/input

1 file changed

+105
-27
lines changed

packages/taro-components-react/src/components/input/index.tsx

Lines changed: 105 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)