My main objective is to indent the space within the text area by a particular amount once I click the Tab button.
I am using React JS and Bootstrap. I have created a bootstrap text area in the render function like so.
<textarea class="col-12 form-control-lg" id="exampleFormControlTextarea1"placeholder="Write some Lyrics" rows="50" onKeyDown={this.useTab()} value={this.state.lyrics}
onChange={e => this.setState({ lyrics : e.target.value },()=>{
this.updateSongs("Body")
})}
</textarea>
Outside my render function I am running the useTab() method.
useTab(e) {
console.log(e.keyCode); //press TAB and get the keyCode
}
I get the below error on running this code.
Unhandled Rejection (TypeError): Cannot read property 'keyCode' of undefined
I did refer to the below solution but was still unable to fix the error.
ReactJS handle tab character in textarea
Do I have to bind the function in the constructor? I'm not really sure why the event is not being captured and what I seem to be missing here.
Please follow this example:
class App extends React.Component {
constructor() {
super();
this.textAreaRef = React.createRef();
this.state = {
lyrics: ""
};
}
onChange = event => {
this.setState({
lyrics: event.target.value
});
};
// Using arrow function so no need to bind 'this'
onKeyDown = event => {
// 'event.key' will return the key as a string: 'Tab'
// 'event.keyCode' will return the key code as a number: Tab = '9'
// You can use either of them
if (event.keyCode === 9) {
// Prevent the default action to not lose focus when tab
event.preventDefault();
// Get the cursor position
const { selectionStart, selectionEnd } = event.target;
// update the state
this.setState(
prevState => ({
lyrics:
prevState.lyrics.substring(0, selectionStart) +
"\t" + // '\t' = tab, size can be change by CSS
prevState.lyrics.substring(selectionEnd)
}),
// update the cursor position after the state is updated
() => {
this.textAreaRef.current.selectionStart = this.textAreaRef.current.selectionEnd =
selectionStart + 1;
}
);
}
};
render() {
return (
<div className="App">
<textarea
ref={this.textAreaRef}
className="col-12 form-control-lg"
placeholder="Write some Lyrics"
rows="50"
value={this.state.lyrics}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
/>
</div>
);
}
}
ReactDOM.render( <App /> , document.getElementById('root'))
.App {
padding: 0 .5rem;
}
/* control tab size */
textarea {
tab-size: 8;
}
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
You need to add an event listener for your useTab function:
document.addEventListener('keydown', useTab);
Edit:
Instead of an event listener, you can use the onKeyDown handler in your input
useTab = (event) => {
event.preventDefault();
console.log(event.key)
}
render: function(){
return(
<div>
<input onKeyDown={this.useTab} />
</div>
);
}
Related
I have a react app, and i am trying to build a focus trapper element, that lets the user tab through elements normally but won't let you focus outside their container.
What works
I am doing so by rendering a first and last "bounder" to sandwich the actual content between two focusable divs that should pass the focus forwards or backwards based on the direction they received it from.
the code for the container:
export class QKeyBinder
extends ComponentSync<QKeyBinder_Props, State> {
private firstTabBinder: React.RefObject<HTMLDivElement> = React.createRef();
private lastTabBinder: React.RefObject<HTMLDivElement> = React.createRef();
protected deriveStateFromProps(nextProps: QKeyBinder_Props): State {
return {};
}
private renderFirstTabBounder() {
return <div
tabIndex={0}
ref={this.firstTabBinder}
className={'q-key-binder__tab-binder'}
role={'tab-binder'}
onKeyDown={(e) => {
if (e.key === 'Tab' && e.shiftKey) {
e.preventDefault();
stopPropagation(e);
return this.lastTabBinder.current!.focus();
}
}}/>;
}
private renderLastTabBounder() {
return <div
tabIndex={0}
ref={this.lastTabBinder}
className={'q-key-binder__tab-binder'}
role={'tab-binder'}
onKeyDown={(e) => {
if (e.key === 'Tab' && !e.shiftKey) {
e.preventDefault();
stopPropagation(e);
return this.firstTabBinder.current!.focus();
}
}}/>;
}
render() {
const className = _className('q-key-binder', this.props.className);
return <div className={className}>
{this.renderFirstTabBounder()}
{this.props.children}
{this.renderLastTabBounder()}
</div>;
}
}
As you can see, i have it working by pressing tab again.
I want the bounders to have a onFocus handler to pass the focus along once they get it.
What didn't work
Since i can't know beforehand who the next focusable element is, I tried dispatching a keyboard event, e.g:
onFocus={(e}=>{
document.body.dispatchEvent(new KeyboardEvent('keypress',{key:'Tab'}))
}}
Dispatching the event on the body.document, the e.target, the body, the window, none of these work.
Just can't seem to simulate another tab press, or find a way to focus the next element without depending on a selector, or a wrapper, which causes extra complexity.
Any help would be much appreciated!
I am making a very simple autocomplete section in a react application.
Code as follows,
index.js
import React from 'react';
import { render } from 'react-dom';
import Autocomplete from './Autocomplete';
const styles = {
fontFamily: 'sans-serif',
textAlign: 'left',
};
const items = ['Moscow', 'Ufa', 'Tver', 'Alma ata'];
function onAutoCompleteHandle(val) {
alert(val);
}
const App = () => (
<div style={styles}>
<Autocomplete items={items} onAutocomplete={onAutoCompleteHandle.bind(this)}/>
</div>
);
render(<App />, document.getElementById('root'));
autocomplete.js
Render method:
const showSuggest = {
display: this.state.show ? 'block' : 'none',
}
return (
<div>
<input type="text"
className="autoCompleteInput"
onChange={this.handleChange}
ref={input => { this.textInput = input; }}
onClick={this.handleClick}
onKeyPress={this.handleKeyPress}
/>
<span className='suggestWrapper' style={showSuggest}>
<ul className='suggestAutocomplete'>
{this.state.items.map((item, i) => {
return
<li key={i} onClick={this.selectSuggestion}>
{item}
</li>
})}
</ul>
</span>
</div>
)
Complete working example: https://codesandbox.io/s/react-autocomplete-nbo3n
Steps to reproduce in the above given sandbox example:
-> Click on the input box.
-> Enter single alphabet eg.., a .
-> This gives the two items as result Ufa, Alma ata .
-> Press the down arrow key in keyboard.
As nothing happens here, unable to select any of the dropdown items.
As of now things work only if we move the mouse over the dropdown and select any item but I am in the need to implement the same behaviour for key down and enter.
Expected behaviour:
-> On keydown/keyup should be able to navigate the dropdown list items.
-> On click enter key on an item then that item should be the selected one.
I have tried assigning ref={input => { this.textInput = input; }} to the ul list items suggestAutocomplete but that also doesn't help..
Really I am stuck with this for very very long time. I humbly request you to consider this question.
It is also okay if you change this existing code, but I need to have both mouse selection and keyboard selection as well in the autocomplete..
Initialize a state with value -1
Add an event on keyDown
something like this:
handleKeyDown(e) {
if (e.keyCode == 40) { //down
this.setState({active: ++this.state.active})
} else if (e.keyCode == 38) { //up
this.setState({active: --this.state.active})
} else if (e.keyCode == 13) { //enter
e.preventDefault();
if (this.state.active > -1) {
this.selectSuggestion();
}
}
};
On click, reset the state to -1 again.
Check this: https://codesandbox.io/s/react-autocomplete-cf2yd
My field onClick event toggles a dropdown, the onFocus event opens it.
When the onFocus event is fired the onClick event is fired afterwards and closes the newly opened dropdown.
How can I prevent firing on Click in Case onFocus fired?
preventDefault and stopPropagation do not work, both events are always fired
<TextInputV2
label={label}
onChange={handleInputOnChange}
onClick={handleOnClick}
onFocus={handleOnFocus}
onKeyUp={handleInputOnKeyUp}
readOnly={!searchable}
value={inputValue}
/>
.......
const handleOnFocus = (event: React.FocusEvent): void => {
if (!isOpen) {
changeIsOpen(true)
}
}
const handleOnClick = (event: React.SyntheticEvent): void => {
if (!searchable) {
toggleOpen()
}
}
You will want to change onClick to onMouseDown. Since event order is
mousedown
change (on focused input)
blur (on focused element)
focus
mouseup
click
dblclick
from: this answer
You want to preventDefault/stoPropagation BEFORE the focus event, which means you have to use "onMouseDown" to properly stop it before the focus event get triggered.
In your case it would be:
<TextInputV2
label={label}
onChange={handleInputOnChange}
onMouseDown={handleOnClick}
onFocus={handleOnFocus}
onKeyUp={handleInputOnKeyUp}
readOnly={!searchable}
value={inputValue}
/>
const handleOnClick = (event) => {
event.preventDefault()
event.stopPropagation()
if (!searchable) {
toggleOpen()
}
}
https://jsfiddle.net/reactjs/69z2wepo/
class Hello extends React.Component {
constructor(props) {
super(props);
this.state = {
text: 'hello'
}
this.lastFocus = 0;
}
handleOnClick(ev) {
const now = new Date().getTime();
console.log('diff since lastFocus');
console.log(now - this.lastFocus);
if (now - this.lastFocus < 200) {
return;
}
const newText = this.state.text + 'c';
this.setState({text:newText})
}
handleOnFocus(ev) {
this.lastFocus = new Date().getTime();
const newText = this.state.text + 'f';
this.setState({text:newText});
}
render() {
return <div>
<input name="" id="" cols="30" rows="10"
value={this.state.text}
onClick={this.handleOnClick.bind(this)}
onFocus={this.handleOnFocus.bind(this)}
></input>
</div>;
}
}
ReactDOM.render(
<Hello name="World" />,
document.getElementById('container')
);
You store the time of your lastFocus -- not in this.state because that updates asynchronously and you cannot rely on that being updated in the onClick handler by calling setState in the onFocus handler. You put it directly on the instance and update it directly.
You can just use a rule of thumb that says if the last focus was within 200ms, then this onClick handler is from the same event as the onFocus handler, and therefore not run the rest of your onClick handler.
My fiddle is not obviously your entire use case, I'm just adding f on focus and c on click to the input text.
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?
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
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.
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}
/>
)
}
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.
I am trying to create a text area element, where on load it should display a "1. ". The user can then type a sentence and hit return. Upon return it should render a "2." in the next line. When a user is on a line that has no text and clicks backspace, it should delete the number and return the focus to the previous number point. To illustrate this: User is on line "2." -> They press backspace which removes the "2." bullet point. -> returns them to the last character of line "1."
So far i have figured out this much:
const React = require('react');
const TextArea = React.createClass({
getInitialState: function() {
return {
textAreaVal: '1. '
};
},
editTextArea: function(value) {
this.setState({
textAreaVal: value
});
},
render: function() {
return (
<div className={"container"}>
<textarea autoFocus className={"proposal-textarea"} wrap="hard" defaultValue ={this.state.textAreaVal}
onChange={this.editTextArea} />
</div>
);
},
});
module.exports = TextArea;
Does anyone have any thoughts on the best way I can accomplish this?
What you're looking for is Reacts onKeyDown event.
Same way you have onChange set up, set up a function for onKeyDown that sends to this.handleKeyDown(event). Within that function, test event.charCode to determine which key was pressed (enter should be 13 and backspace should be 8), and then apply the necessary actions as needed.
EDIT: Moving my comment to the answer block;
To handle the incrementing number, simply add a secondary state element, lineNumber. Initialize it to 1 at start. Whenever you detect a keypress of Enter, increment lineNumber and append "\n" + this.state.lineNumber + ". " to your textAreaVal.
Well, look at this fiddle
const { Component, PropTypes } = React;
class NumberedTextArea extends Component {
constructor() {
super();
this._onKeyDown = this._onKeyDown.bind(this);
this.state = {
counter: 2,
text: `1. `
}
}
_onKeyDown(e) {
console.log(e.keyCode);
if (e.keyCode ===13) {
console.log(this.refs.text.value);
this.refs.text.value = `${this.refs.text.value}\n${this.state.counter++}. `;
e.preventDefault();
e.stopPropagation();
}
}
render() {
const style = {
height: 300,
width: 200
};
return (
<textarea ref="text" onKeyDown={this._onKeyDown} style={style}>
{this.state.text}
</textarea>
);
}
}
ReactDOM.render(
<NumberedTextArea />,
document.getElementById('root')
);