Reputation: 1184
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?
Upvotes: 40
Views: 107894
Reputation: 11
In React 18.3 when manipulating a caret in a controlled Input or TextArea you can wrap your caret manipulation in a requestAnimationFrame()
import { useRef } from react;
const moveCaretBackOne = (elem) = {
const pos = elem.selectionStart;
elem.selectionStart = pos - 1;
}
export default App = () => {
const inputRef = useRef()
return => (
<input type="text" ref={inputRef}
onKeyDown={(e) => {
if (e.code === "ArrowLeft") {
requestAnimationFrame(() => {
moveCaretBackOne(inputRef.current)
})
}
}}
/>
)
}
Basically, what happens is that the caret position change occurs after the render. *edit: While I used a ref in my example, event.target is an equally viable means of access the Input or TextArea.
Upvotes: 1
Reputation: 1598
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.
Upvotes: 19
Reputation: 2909
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.
Upvotes: 1
Reputation: 4953
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}
/>
)
}
Upvotes: 3
Reputation: 28397
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
});
}
}
Upvotes: 52