反应控制的输入光标跳跃

Posted

技术标签:

【中文标题】反应控制的输入光标跳跃【英文标题】:React controlled input cursor jumps 【发布时间】:2018-02-10 12:53:02 【问题描述】:

我正在使用 React 并格式化了一个受控输入字段,当我写一些数字并在输入字段外单击时,它可以正常工作。但是,当我想编辑输入时,光标会跳到输入字段中值的前面。这仅发生在 IE 中,而不发生在例如铬合金。我已经看到,对于一些程序员来说,光标会跳到值的后面。所以我认为我的光标跳到前面的原因是因为该值在输入字段中向右而不是向左对齐。这是一个情景:

我的第一个输入是 1000 然后我想将其编辑为10003,但结果是 31000

有没有办法控制光标不跳转?

【问题讨论】:

【参考方案1】:

根据您的问题猜测一下,您的代码很可能与此类似:

this.setState(value: e.target.value) />

如果您的事件使用onBlur 处理,这可能会有所不同,但本质上是相同的问题。这里的行为,许多人称之为 React “错误”,实际上是预期的行为。

输入控件的值不是控件加载时的初始值,而是绑定到this.state 的底层value。当状态改变时,控件会被 React 重新渲染。

本质上,这意味着控件由 React 重新创建并由状态的值填充。问题是它无法知道重新创建之前的光标位置。

我发现解决这个问题的一种方法是在重新渲染之前记住光标位置,如下所示:

this.cursor = e.target.selectionStart; this.setState(value: e.target.value); onFocus=(e) => e.target.selectionStart = this.cursor; />

【讨论】:

用钩子怎么做? @JuliodeLeon - 使用useState 作为cursorPos。但这很糟糕,因为有一些边缘情况需要处理已删除和粘贴的输入。 特别是对于钩子,您可以执行以下操作(基于@abhinav-prabhakar 建议):const inputRef = useRef(null); useEffect(() => inputRef.current.focus(); , [inputRef]); 然后<input ref=inputRef /> 我只是在此处添加它以免混淆答案和脱离问题的上下文,使其更加混乱。【参考方案2】:

这是<input/> 标记的直接替换。它是一个简单的功能组件,使用钩子来保存和恢复光标位置:

import React,  useEffect, useRef, useState  from 'react';

const ControlledInput = (props) => 
   const  value, onChange, ...rest  = props;
   const [cursor, setCursor] = useState(null);
   const ref = useRef(null);

   useEffect(() => 
      const input = ref.current;
      if (input) input.setSelectionRange(cursor, cursor);
   , [ref, cursor, value]);

   const handleChange = (e) => 
      setCursor(e.target.selectionStart);
      onChange && onChange(e);
   ;

   return <input ref=ref value=value onChange=handleChange ...rest />;
;

export default ControlledInput;

【讨论】:

这很漂亮,你是个传奇。我在这里尝试了其他答案,但这不仅确实有效,而且效果很好(没有光标跳跃然后向后移动,非常流畅的用户体验),而且它非常易于使用! ? 请注意,input.setSelecetionRange() 不能与 type="number" 一起使用——如果这是您想要的,我发现我必须使用 type="text" inputMode="numeric" 谢谢,这对我也有用。我在打字稿中使用textarea,所以我不得不change the code一点:)【参考方案3】:

这是我的解决方案:

import React,  Component  from "react";

class App extends Component 
  constructor(props) 
    super(props);
    this.state = 
      name: ""
    ;

    //get reference for input
    this.nameRef = React.createRef();

    //Setup cursor position for input
    this.cursor;
  

  componentDidUpdate() 
    this._setCursorPositions();
  

  _setCursorPositions = () => 
    //reset the cursor position for input
    this.nameRef.current.selectionStart = this.cursor;
    this.nameRef.current.selectionEnd = this.cursor;
  ;

  handleInputChange = (key, val) => 
    this.setState(
      [key]: val
    );
  ;

  render() 
    return (
      <div className="content">
        <div className="form-group col-md-3">
          <label htmlFor="name">Name</label>
          <input
            ref=this.nameRef
            type="text"
            autoComplete="off"
            className="form-control"
            id="name"
            value=this.state.name
            onChange=event => 
              this.cursor = event.target.selectionStart;
              this.handleInputChange("name", event.currentTarget.value);
            
          />
        </div>
      </div>
    );
  


export default App;

【讨论】:

【参考方案4】:

这是一个简单的解决方案。为我工作。

<Input
ref=input=>input && (input.input.selectionStart=input.input.selectionEnd=this.cursor)
value=this.state.inputtext 
onChange=(e)=>
this.cursor = e.target.selectionStart;
this.setState(inputtext: e.target.value)
/>

解释:

我们在这里所做的是将光标位置保存在 onChange() 中,现在当标签由于状态值的变化而重新渲染时,执行 ref 代码,并在 ref 代码内部恢复光标位置。

【讨论】:

【参考方案5】:

如果您使用的是 textarea,那么这里是基于 Daniel Loiterton's code 使用 TypeScript 的钩子:

interface IControlledTextArea 
    value: string
    onChange: ChangeEventHandler<HTMLTextAreaElement> | undefined
    [x: string]: any


const ControlledTextArea = ( value, onChange, ...rest : IControlledTextArea) => 
    const [cursor, setCursor] = useState(0)
    const ref = useRef(null)

    useEffect(() => 
        const input: any = ref.current
        if (input) 
            input.setSelectionRange(cursor, cursor)
        
    , [ref, cursor, value])

    const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => 
        setCursor(e.target.selectionStart)
        onChange && onChange(e)
    

    return <textarea ref=ref value=value onChange=handleChange ...rest />

【讨论】:

【参考方案6】:

我的光标总是跳到行尾。这个解决方案似乎解决了这个问题(来自 github):

import * as React from "react";
import * as ReactDOM from "react-dom";

class App extends React.Component<,  text: string > 
  private textarea: React.RefObject<HTMLTextAreaElement>;
  constructor(props) 
    super(props);
    this.state =  text: "" ;
    this.textarea = React.createRef();
  

  handleChange(e: React.ChangeEvent<HTMLTextAreaElement>) 
    const cursor = e.target.selectionStart;
    this.setState( text: e.target.value , () => 
      if (this.textarea.current != null)
        this.textarea.current.selectionEnd = cursor;
    );
  

  render() 
    return (
      <textarea
        ref=this.textarea
        value=this.state.text
        onChange=this.handleChange.bind(this)
      />
    );
  


ReactDOM.render(<App />, document.getElementById("root"));

【讨论】:

【参考方案7】:

这是我的解决方案

const Input = () => 
    const [val, setVal] = useState('');
    const inputEl = useRef(null);

    const handleInputChange = e => 
      const  value, selectionEnd  = e.target;
      const rightCharsCount = value.length - selectionEnd;
      const formattedValue = parseInt(value.replace(/\D/g, ''), 10).toLocaleString();
      const newPosition = formattedValue.length - rightCharsCount;

      setVal(formattedValue);

      setTimeout(() => 
        inputEl.current.setSelectionRange(newPosition, newPosition);
      , 0);
    ;

    return <input ref=inputEl value=val onChange=handleInputChange />;
;

【讨论】:

【参考方案8】:
// Here is a custom hook to overcome this problem:

import  useRef, useCallback, useLayoutEffect  from 'react'
/**
 * This hook overcomes this issue @link https://github.com/reduxjs/react-redux/issues/525
 * This is not an ideal solution. We need to figure out why the places where this hook is being used
 * the controlled InputText fields are losing their cursor position when being remounted to the DOM
 * @param Function callback - the onChangeCallback for the inputRef
 * @returns Function - the newCallback that fixes the cursor position from being reset
 */
const useControlledInputOnChangeCursorFix = callback => 
  const inputCursor = useRef(0)
  const inputRef = useRef(null)

  const newCallback = useCallback(
    e => 
      inputCursor.current = e.target.selectionStart
      if (e.target.type === 'text') 
        inputRef.current = e.target
      
      callback(e)
    ,
    [callback],
  )

  useLayoutEffect(() => 
    if (inputRef.current) 
      inputRef.current.setSelectionRange(inputCursor.current, inputCursor.current)
    
  )

  return newCallback


export default useControlledInputOnChangeCursorFix

// Usage:

import React,  useReducer, useCallback  from 'react'
import useControlledInputOnChangeCursorFix from '../path/to/hookFolder/useControlledInputOnChangeCursorFix'

// Mimics this.setState for a class Component
const setStateReducer = (state, action) => ( ...state, ...action )

const initialState =  street: '', address: '' 

const SomeComponent = props => 
  const [state, setState] = useReducer(setStateReducer, initialState)

  const handleOnChange = useControlledInputOnChangeCursorFix(
    useCallback(( target:  name, value  ) => 
      setState( [name]: value )
    , []),
  )

  const  street, address  = state

  return (
    <form>
      <input name='street' value=street onChange=handleOnChange />
      <input name='address' value=address onChange=handleOnChange />
    </form>
  )

【讨论】:

【参考方案9】:

我尝试了上述所有解决方案,但没有一个对我有用。相反,我更新了 onKeyUp React 合成事件类型上的 e.currentTarget.selectionStarte.currentTarget.selectionEnd。例如:

const [cursorState, updateCursorState] = useState();
const [formState, updateFormState] = useState( "email": "" );

const handleOnChange = (e) => 
    // Update your state & cursor state in your onChange handler
    updateCursorState(e.target.selectionStart);
    updateFormState(e.target.value);


<input
    name="email"
    value=formState.email
    onChange=(e) => handleOnChange(e)
    onKeyUp=(e) => 
        // You only need to update your select position in the onKeyUp handler:
        e.currentTarget.selectionStart = cursorState.cursorPosition;
        e.currentTarget.selectionEnd = cursorState.cursorPosition;
    
/>

另外,请注意selectionStartselectionEnd getter 不适用于email 类型的输入字段。

【讨论】:

【参考方案10】:

对于在react-native-web 中遇到此问题的任何人,这里是用 TypeScript 编写的解决方案

const CursorFixTextInput = React.forwardRef((props: TextInputProps, refInput: ForwardedRef<TextInput>) => 
    if(typeof refInput === "function") 
        console.warn("CursorFixTextInput needs a MutableRefObject as reference to work!");
        return <TextInput key="invalid-ref" ...props />;
    

    if(!("HTMLInputElement" in self)) 
        return <TextInput key="no-web" ...props />;
    

    const  value, onChange, ...restProps  = props;
    const defaultRefObject = useRef<TextInput>(null);
    const refObject: RefObject<TextInput> = refInput || defaultRefObject;
    const [ selection, setSelection ] = useState<SelectionState>(kInitialSelectionState);

    useEffect(() => 
        if(refObject.current instanceof HTMLInputElement) 
            refObject.current.setSelectionRange(selection.start, selection.end);
        
    , [ refObject, selection, value ]);

    return (
        <TextInput
            ref=refObject
            value=value
            onChange=event => 
                const eventTarget = event.target as any;
                if(eventTarget instanceof HTMLInputElement) 
                    setSelection(
                        start: eventTarget.selectionStart,
                        end: eventTarget.selectionEnd
                    );
                

                if(onChange) 
                    onChange(event);
                
            
            ...restProps
        />
    )
);

【讨论】:

以上是关于反应控制的输入光标跳跃的主要内容,如果未能解决你的问题,请参考以下文章

linux 从普通用户切换为管理员时用 su 命令为啥提示输入密码,按啥键光标都没反应。

反应本机文本输入,按下时更改光标位置

如何更改我的非输入标签的反应?

如何引导鼠标滚轮输入在光标下控制而不是聚焦?

js手动控制输入框的光标位置

控制input输入框光标的位置