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.
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 have a blur event on a text input that toggles on a button (renders it with React). When you click on the button, the blur event fires on the text input which removes the button from the DOM, but the button click event is not fired. I tried wrapping the code that removes the button from the DOM in a 100ms timeout and it works but feels hacky, is there a better way?
Here's the gist of the code:
const blurHandler = e => {
setShowInput(false); //this prevents buttonHandler from being fired
// setTimeout(() => setShowInput(false), 100); // this works with 100ms, but not with 50ms for example
}
const buttonHandler = e => {
console.log('Button clicked!');
}
const focusHandler = e => {
setShowInput(true);
}
return (
<div>
<input type="text" onFocus={focusHandler} onBlur={blurHandler} >
{showInput && <button onClick={buttonHandler}>Apply to all</button>}
</div>
)
This is easy, you just need to use a different event trigger. onClick is too late, it won't fire until after the textarea blur happens, but onMouseDown will fire before it:
const btn = document.getElementById('btn');
const txt = document.getElementById('txt');
txt.onblur = () => {console.log("Textarea blur fired")}
txt.onfocus = () => {console.log("Textarea focus fired")}
btn.onclick = () => {console.log("Button onClick fired")}
btn.onmousedown = () => {console.log("Button mouseDown fired")}
btn.onmouseup = () => {console.log("Button mouseUp fired")}
<textarea id="txt">Focus me first</textarea>
<button id="btn">Click me next</button>
Since you conditionally render the button with:
{showInput && <button onClick={buttonHandler}>Apply to all</button>}
As soon as you set showInput to a falsy value, it gets removed from the DOM. You have couple of options:
Option 1 (If you don't have to remove the button from the DOM)
return (
<div>
<input type="text" onFocus={focusHandler} onBlur={blurHandler} >
<button style={{opacity: showInput ? 1 : 0}} onClick={buttonHandler}>Apply to all</button>}
</div>
)
Setting opacity 0 will hide the button, but won't remove it from the dom.
Option 2 (If you have to remove the button from the DOM)
const btnClickedRef = useRef(false)
const buttonHandler = e => {
console.log('Button clicked!');
btnClickedRef.current = true
}
return (
<div>
<input type="text" onFocus={focusHandler} onBlur={blurHandler} >
{showInput && btnClickedRef.current && <button onClick={buttonHandler}>Apply to all</button>}
</div>
)
at the current point, this code works, but when the user clicks to hide the menu, the useClickOutside fires too, the menu toggles off and on again... would there any way to fix that so when clicks outside it closes but when clicks the button it toggles on/off ?
const useClickOutside = (ref, handler) => {
useEffect(() => {
const clickHandler = (event) => {
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener('mousedown', clickHandler);
return () => {
document.removeEventListener('mousedown', clickHandler);
};
});
};
const Settings = () => {
const ref = useRef();
const [toggle, setToggle] = useState(false);
useClickOutside(ref, () => setToggle(false));
return (
<div className='settings'>
<button onClick={() => setToggle(!toggle)} className='settings__button'>
Menu
</button>
{toggle && (
<div ref={ref} className='settings__panel'>
<Link className='settings__links' to='/user/settings'>
Your Profile
</Link>
<Link className='settings__links' to='/user/settings'>
Todos history
</Link>
<Link className='settings__links' to='/user/settings'>
Settings
</Link>
<Link className='settings__links' value={'Logout'} to='/user/login'>
Logout
</Link>
</div>
)}
</div>
);
};
You might consider adding a onBlur event on the .settings div with a tabIndex=0.
You can then then capture blurs of the div and test if the event came from within the div or not.
const onBlur = (e: FocusEvent < HTMLElement > ) => {
if (opened?) {
const element = e.relatedTarget;
if (element == null) {
// dropdown was blured because window lost focused. probably close.
} else if (element != e.currentTarget) {
if (!e.currentTarget.contains(element as Node)) {
// blured element is not in .settings. close
}
}
}
};
If you want to get fancy you can also add a keydown and close on escape.
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
// close!
}
);
Here is a code sandbox that implements these items.
You could make use of event stopPropagation. Add the call event.stopPropagation() to your onClick handler function that hides the menu.
<button
onClick={(e) => {
e.stopPropagation();
setToggle(!toggle);
}}
className='settings__button'
>
Menu
</button>
This will prevent that the onClick event bubbles upwards to the next event listener which would be your onClickOutside listener.
UPDATE:
This will only work if your event listener is listening for onClick events. Your inline onClick event listener will stop the propagation of the event of type click only.
document.addEventListener('click', clickHandler);
return () => {
document.removeEventListener('click', clickHandler);
};
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?
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>
)