I have the following class component
...
constructor(props) {
super(props);
...
this.currentlyEditedInput = React.createRef()
}
...
onClick(id, column) {
return (event) => {
...
let { clientX, clientY } = event;
let repeatClick = ( true /*repeat needed*/) ? function() {
let click = new MouseEvent("click", {
clientX,
clientY
});
console.log(this.currentlyEditedInput.current.firstChild.tagName) // INPUT
console.log(this.currentlyEditedInput.current.firstChild.dispatchEvent) // function dispatchEvent()
this.currentlyEditedInput.current.firstChild.dispatchEvent(click) // nothing happens
} : undefined;
...
this.setState(/*new state*/, repeatClick); // when state is updated new input is rendered
...
}
}
...
render() {
...
return (
...
<TableCell
className={classes.cell}
key={column.name}
onClick={ this.onClick(row.id, column) }
>
...
<Input
ref={this.currentlyEditedInput}
autoFocus
...
/>
...
</TableCell>
...
)
}
When a table cell is clicked a new input with some value appears inside it, but the cursor is in the end of the input so the user has to click one more time. I want to make the cursor appear where the user clicks. So I dispatch the same click event in the callback (second argument of setState), but calling dispatchEvent does not seem to change anything.
May be this task should be solved in a completely different way. What is the correct way to do it in React?
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!
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 am trying to find a way to detect middle click event in React JS but so far haven't succeeded in doing so.
In Chrome React's Synthetic Click event does show the button clicked ->
mouseClickEvent.button === 0 // Left
mouseClickEvent.button === 1 // Middle but it does not execute the code at all
mouseClickEvent.button === 2 // Right (There is also onContextMenu with event.preventDefault() )
Please share your views.
If you are using a stateless component:
JS
const mouseDownHandler = ( event ) => {
if( event.button === 1 ) {
// do something on middle mouse button click
}
}
JSX
<div onMouseDown={mouseDownHandler}>Click me</div>
Hope this helps.
You can add a mouseDown event and then detect the middle button click like:
handleMouseDown = (event) => {
if(event.button === 1) {
// do something on middle mouse button click
}
}
You code might look like:
class App extends React.Component {
constructor() {
super();
this.onMouseDown = this.onMouseDown.bind(this);
}
componentDidMount() {
document.addEventListener('mousedown', this.onMouseDown);
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.onMouseDown);
}
onMouseDown(event) {
if (event.button === 1) {
// do something on middle mouse button click
}
}
render() {
// ...
}
}
You can find more information on MouseEvent.button here:
https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
Be careful. Using mousedown won't always get you the behavior you want. A "click" is both a mousedown and a mouseup where the x and y values haven't changed. Ideally, your solution would store the x and y values on a mousedown and when mouseup occurs, you would measure to make sure they're in the same spot.
Even better than mousedown would be pointerdown. This configures compatibility with "touch" and "pen" events as well as "mouse" events. I highly recommend this method if pointer events are compatible with your app's compatible browsers.
The modern way of doing it is through the onAuxClick event:
import Card from 'react-bootstrap/Card';
import React, { Component } from 'react';
export class MyComponent extends Component {
onAuxClick(event) {
if (event.button === 1) {
// Middle mouse button has been clicked! Do what you will with it...
}
}
render() {
return (
<Card onAuxClick={this.onAuxClick.bind(this)}>
</Card>
);
}
You can use React Synthetic event as described below
<div tabIndex={1} onMouseDown={event => { console.log(event)}}>
Click me
</div>
You can keep onClick. In React, you have access to nativeEvent property from where you can read which button was pressed:
const clickHandler = (evt) => {
if (e.nativeEvent.button === 1) {
...
}
}
return (
<a onClick={clickHandler}>test</a>
)
I'm jumping in on a pretty big React JS project which is using react-data-grid to display a bunch of editable data. Right now, you have to click an Update button to send changes to the server. My task at hand is to create auto-save functionality like so:
User selects cell to edit text
User changes text
User either moves to another cell or clicks away from data-grid
Changes are persisted to the server
Here's what I've tried:
onBlur event on each column. The event will fire, but it seems like the event was attached to a div and not the underlying input control. Therefore, I don't have access to the cell's values at the time this event is fired.
onCellDeselected on the <ReactDataGrid> component itself. It seems like this method is fired immediately upon render, and it only gets fired subsequent times when moving to another cell. If I'm editing the last cell and click away from the data-grid, this callback isn't fired.
Using react-data-grid, how can I effectively gain access to an editable cell's content when the user finishes editing?
The commits on react-data-grid are handled by the EditorContainer. The commit logic is simple. An editor commits a value when:
The editor unmounts
Enter is pressed
Tab is pressed
In some cases when the arrows are pressed (will skip this part is it may not be necessary for you, you can look at the logic for this on the EditorContainer)
Based on that the way I would recommend to do the autosave is:
Create an an EditorWrapper (HOC) the editors where you want auto save to be turned on
const editorWrapper(WrappedEditor) => {
return class EditorWrapper extends Component {
constructor(props) {
base(props);
this._changeCommitted = false;
this.handleKeyDown.bind(this);
}
handleKeyDown({ key, stopPropagation }) {
if (key === 'Tab' || key === 'Enter') {
stopPropagation();
this.save();
this.props.onCommit({ key });
this._changeCommitted = true;
}
// If you need the logic for the arrows too, check the editorContainer
}
save() {
// Save logic.
}
hasEscapeBeenPressed() {
let pressed = false;
let escapeKey = 27;
if (window.event) {
if (window.event.keyCode === escapeKey) {
pressed = true;
} else if (window.event.which === escapeKey) {
pressed = true;
}
}
return pressed;
}
componentWillUnmount() {
if (!this._changeCommitted && !this.hasEscapeBeenPressed()) {
this.save();
}
}
render() {
return (
<div onKeyDown={this.handleKeyDown}>
<WrappedComponent {...this.props} />
</div>);
}
}
}
When exporting you editor just wrap them with the EditorWrapper
const Editor = ({ name }) => <div>{ name }</div>
export default EditorWrapper(Editor);
Use one of the start or stop event callback handlers at the DataGrid level like onCellEditCommit
<DataGrid
onCellEditCommit={({ id, field, value }, event) => {
...
}
/>
or a valueSetter for a single the column definition:
const columns: GridColDef[] = [
{
valueSetter: (params: GridValueSetterParams) => {
// params.row contains the current row model
// params.value contains the entered value
},
},
];
<DataGrid columns={columns} />
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')
);