36

I have a textarea in React that I want to turn into a "notepad". Which means I want the "tab" key to indent instead of unfocus. I looked at this answer, but I can't get it to work with React. Here is my code:

handleKeyDown(event) {
    if (event.keyCode === 9) { // tab was pressed
        event.preventDefault();
        var val = this.state.scriptString,
            start = event.target.selectionStart,
            end = event.target.selectionEnd;

        this.setState({"scriptString": val.substring(0, start) + '\t' + val.substring(end)});
        // This line doesn't work. The caret position is always at the end of the line
        this.refs.input.selectionStart = this.refs.input.selectionEnd = start + 1;
    }
}
onScriptChange(event) {
   this.setState({scriptString: event.target.value});
}
render() {
    return (
        <textarea rows="30" cols="100" 
                  ref="input"
                  onKeyDown={this.handleKeyDown.bind(this)}
                  onChange={this.onScriptChange.bind(this)} 
                  value={this.state.scriptString}/>
    )
}

When I run this code, even if I press the "tab" key in the middle of the string, my cursor always appears at the end of the string instead. Anyone knows how to correctly set the cursor position?

Community
  • 1
  • 1
Discombobulous
  • 1,112
  • 2
  • 14
  • 25

4 Answers4

49

You have to change the cursor position after the state has been updated(setState() does not immediately mutate this.state)

In order to do that, you have to wrap this.refs.input.selectionStart = this.refs.input.selectionEnd = start + 1; in a function and pass it as the second argument to setState (callback).

handleKeyDown(event) {
      if (event.keyCode === 9) { // tab was pressed
          event.preventDefault();
          var val = this.state.scriptString,
          start = event.target.selectionStart,
          end = event.target.selectionEnd;
          this.setState(
              {
                  "scriptString": val.substring(0, start) + '\t' + val.substring(end)
              },
              () => {
                  this.refs.input.selectionStart = this.refs.input.selectionEnd = start + 1
              });
      }
 }

jsfiddle

QoP
  • 27,388
  • 16
  • 74
  • 74
  • fiddle seems broken – ey dee ey em Oct 17 '17 at 05:05
  • 2
    I sometimes find it necessary to setTimeout: `setTimeout(() => { this.refs.input.selectionStart = this.refs.input.selectionEnd = start + 1 }, 250)` – John Nov 12 '19 at 20:37
  • I had to call this.refs.input.focus(); in the setState callback before this.refs.input.selectionStart to make it work. – Hyperdingo Aug 10 '20 at 10:25
  • There isn't a second argument to `setState` anymore in React 18 with functional components as far as I can tell. – Lance Sep 19 '22 at 11:21
18

For anyone looking for a quick React Hooks (16.8+) cursor position example:

import React, { useRef } from 'react';

export default () => {
  const textareaRef = useRef(); 
  const cursorPosition = 0;

  return <textarea
    ref={textareaRef}
    onBlur={() => textareaRef.current.setSelectionRange(cursorPosition, cursorPosition)}
  />

}

In this example, setSelectionRange is used to set the cursor position to the value of cursorPosition when the input is no longer focused.

For more information about useRef, you can refer to React's official doc's Hook Part.

Chris Dolphin
  • 1,578
  • 16
  • 28
3

Here's a solution in a hooks-style architecture. My recommendation is to change the textarea value and selectionStart immediately on tab insertion.

import React, { useRef } from "react"

const CodeTextArea = ({ onChange, value, error }) => {
  const textArea = useRef()
  return (
      <textarea
        ref={textArea}
        onKeyDown={e => {
          if (e.key === "Tab") {
            e.preventDefault()

            const { selectionStart, selectionEnd } = e.target

            const newValue =
              value.substring(0, selectionStart) +
              "  " +
              value.substring(selectionEnd)

            onChange(newValue)
            if (textArea.current) {
              textArea.current.value = newValue
              textArea.current.selectionStart = textArea.current.selectionEnd =
                selectionStart + 2
            }
          }
        }}
        onChange={e => onChange(e.target.value)}
        value={value}
      />
  )
}
seveibar
  • 4,403
  • 4
  • 30
  • 31
1

In React 15 best option is something like that:

class CursorForm extends Component {

  constructor(props) {
    super(props);
    this.state = {value: ''};
  }

  handleChange = event => {
    // Custom set cursor on zero text position in input text field
    event.target.selectionStart = 0 
    event.target.selectionEnd = 0

    this.setState({value: event.target.value})
  }

  render () {
    return (
      <form>
        <input type="text" value={this.state.value} onChange={this.handleChange} />
      </form>
    )  
  }

}

You can get full control of cursor position by event.target.selectionStart and event.target.selectionEnd values without any access to real DOM tree.

Vivek Kumar
  • 2,625
  • 2
  • 25
  • 33