React自定义货币输入框无法在光标位置编辑替换字符问题
React自定义货币输入光标错位、无法定点编辑问题修复
核心问题
当前出现的两个异常本质是三个逻辑漏洞:
- 输入逻辑完全忽略光标位置:输入数字时直接对原值做算术运算拼接,相当于永远把新数字加到值的末尾,完全不响应用户移动光标的操作
- 受控组件重渲染默认重置光标:React控制的input每次value更新触发重渲染时,光标会默认跳到内容末尾,现有代码没有主动把光标设回操作位置
- 删除逻辑索引计算错误,未覆盖多内容选中场景:退格、删除键的位置偏移计算有误,用户选中一段内容删除/替换时逻辑直接失效
修复思路
- 新增ref存储每次操作后的光标位置,值更新完成后主动把input光标设置到对应位置,避免自动跳转到末尾
- 废弃原有的算术运算拼接值逻辑,改为基于字符串切片的方式,根据当前光标/选区位置插入、删除字符
- 补全选中多段内容时批量删除、输入替换的场景处理
修复后完整组件代码
import React, { useCallback, useRef, useEffect, useState } from "react"; import _ from "lodash"; import { Form } from "react-bootstrap"; export const CurrencyInput = ({ value, setValue, controlId, label, max, min, required, message, setValidity,defaultValue }) => { const [isValid, setValid] = useState(true); const amountInvalidMessage = "amount-invalid"; const amountRequiredMessage = "amount-required"; const [invalidMessage, setInvalidMessage] = useState(amountInvalidMessage); const formatter = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", currencyDisplay: "narrowSymbol"}) const currencyInput = useRef(null) // 存储操作后的光标偏移位置 const cursorPos = useRef(0); const validate = useCallback(_.debounce((amount) => { let valid = true; if (amount !== null && amount !== "") { amount= amount.replace("$","") let amountNumber = parseFloat(amount); if (isNaN(amountNumber)) { valid = false; } else if (min > amountNumber) { valid = false; } else if (max < amountNumber) { valid = false; } if(!valid) { setValue(""); currencyInput.current.focus() setInvalidMessage(amountInvalidMessage); } } else if (required) { valid = false; setValue(""); currencyInput.current.focus() setInvalidMessage(amountRequiredMessage); } setValid(valid); setValidity(valid); }, 500), [min, max]); // 值更新渲染完成后复位光标位置 useEffect(() => { if (currencyInput.current) { currencyInput.current.selectionStart = cursorPos.current; currencyInput.current.selectionEnd = cursorPos.current; } }, [value]) const onBlur = (e) => { if(value !== null && value !== "") { let formattedValue = parseFloat(value).toFixed(2) setValue(formattedValue); } validate(value); } const onKeyDown = (e) => { const input = e.target; const start = input.selectionStart; const end = input.selectionEnd; const currentVal = value || ""; // 处理退格、删除键 if (e.keyCode === 8 || e.keyCode === 46) { e.preventDefault(); // 选中多内容场景:直接删除选中区间 if (start !== end) { const newVal = currentVal.slice(0, start) + currentVal.slice(end); setValue(newVal === "0.00" ? "" : newVal); cursorPos.current = start; return; } // 单光标场景:退格删光标前字符,删除键删光标后字符 const deletePos = e.keyCode === 8 ? start - 1 : start; if (deletePos < 0 || deletePos >= currentVal.length) return; const newVal = currentVal.slice(0, deletePos) + currentVal.slice(deletePos + 1); setValue(newVal === "0.00" ? "" : newVal); cursorPos.current = e.keyCode === 8 ? start - 1 : start; return; } // 处理数字输入(支持主键盘、小键盘数字区) let inputNum = null; if (e.keyCode >= 48 && e.keyCode <= 57) inputNum = e.keyCode - 48; else if (e.keyCode >= 96 && e.keyCode <= 105) inputNum = e.keyCode - 96; else return; e.preventDefault(); let newVal; // 选中多内容场景:用新输入数字替换选中内容 if (start !== end) { newVal = currentVal.slice(0, start) + inputNum + currentVal.slice(end); cursorPos.current = start + 1; } else { // 单光标场景:在光标位置插入数字 newVal = currentVal.slice(0, start) + inputNum + currentVal.slice(start); cursorPos.current = start + 1; } // 统一格式化为两位小数 const numVal = parseFloat(newVal); if (!isNaN(numVal)) { newVal = numVal.toFixed(2).toString(); } setValue(newVal === "0.00" ? "" : newVal); }; return <Form.Group {...{ controlId }}> <Form.Control type="text" isInvalid={!isValid} onBlur={onBlur} ref ={currencyInput} onKeyDown={onKeyDown} placeholder= {formatter.format(defaultValue)} value={value} pattern="/\D/g"> </Form.Control> {!isValid ? <Form.Control.Feedback className={'classes.feedbackInvalid'} type="invalid">{invalidMessage}</Form.Control.Feedback> : null} </Form.Group> }
效果验证
修复后可实现:
- 在任意光标位置插入数字,新字符不会强制追加到内容末尾
- 删除任意位置字符后光标停留在操作位,不会自动跳转到内容末尾
- 支持选中一段内容后直接输入数字替换、按删除键批量清除
内容的提问来源于stack exchange,提问作者Vishnu Vedagna
相关产品推荐
相关产品推荐

