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);
};
Related
I have this DropdownMenu component that works just fine. Basically, if you click on "Click me" button you'll see a DropdownMenu then if you either click outside the DropdownMenu or select an option by clicking on it the Dropdown disappears and this is expected.
My issue is that I want to be able to toggle the Dropdown Menu when clicking on "Click me" button, as of right now if I click on "Click me" button the menu shows up, then if I click again on "Click me" button the DropdownMenu won't go away (I want to toggle it). Can anyone point me in the right direction and tell me what I'm missing or need to do please? Thanks a lot in advance!
Here's a LIVE DEMO of my code, and here are the 2 functions that handle the toggle for my DropdownMenu and handle the ClickOutside.
const handleToggle = () => setOpen((isOpen) => !isOpen);
const handleClickOutside = (evt: MouseEvent) =>
!menuListRef?.current?.contains(evt.target as HTMLElement) &&
setOpen(false);
You should define a ref also on your button:
const buttonRef = useRef<HTMLDivElement | null>() as React.MutableRefObject<HTMLInputElement>;
...
<StyledMenuButton onClick={handleToggle} ref={buttonRef}>
And add a check for it not to be the clicked element in handleClickOutside:
const handleClickOutside = (evt: MouseEvent) => {
if (!menuListRef?.current?.contains(evt.target as HTMLElement) &&
!buttonRef?.current?.contains(evt.target as HTMLElement)) {
setOpen(false);
}
}
Working sandbox.
const buttonRef = useRef<HTMLDivElement | null>() as React.MutableRefObject<HTMLInputElement>;
.
.
.
const handleClickOutside = (evt: MouseEvent) => {
const dropdownClicked = menuListRef?.current?.contains(
evt.target as HTMLElement
);
const buttonToOpenClicked = buttonRef?.current?.contains(
evt.target as HTMLElement
);
// If nether the button or dropdown menu has been clicked
// You haven't clicked "outside"
if (!dropdownClicked && !buttonToOpenClicked) {
console.log("Clicked outside");
setOpen(false);
}
};
.
.
.
<StyledMenuButton onClick={handleToggle} ref={buttonRef}>
Since adding an event listener to the document to close the navigation on an outside click, the burger menu has gone from 100% effective to activating the active class's to being less effective and not always creating the event. Any help will be much appreciated.
const burger = document.querySelector('.nav-mob-menu');
const mobNav = document.querySelector('.mobile-nav-side');
burger.addEventListener('click', () => {
mobNav.classList.toggle('active');
burger.classList.toggle('active');
})
document.querySelectorAll('.links').forEach(e =>
e.addEventListener('click', () => {
burger.classList.remove('active');
mobNav.classList.remove('active');
}))
document.addEventListener('click', (e) => {
if(e.target.id != 'nav-mob-menu' && e.target.id != 'mobile-nav-side'){
mobNav.classList.remove('active');
burger.classList.remove('active');
}
})
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>
)
How to make the touchMove event be interrupted if the finger goes beyond the bounds of the object to which the event is attached? And when interrupting, call another function.
I assume that I need to somehow determine the location of the object on which the event occurs and when exiting these coordinates somehow interrupt the event. But I can't find how to do this in React using useRef and how to interrupt the event.
const Scrollable = (props) => {
const items = props.items;
let ref = useRef();
const touchStarts = (e) => {...}
const touchEnd = (e) => {...}
const touchMove = (e) => {
if (ref && ref.current && !ref.current.contains(e.target)) {
return;
}
e.preventDefault();
...
}
useEffect(() => {
document.addEventListener("touchmove", touchMove);
...
return () => {
document.removeEventListener("touchmove", touchMove);
...
};
});
return (
<div>
<div
ref={ref}
onTouchStart={touchStarts}
onTouchMove={touchMove}
onTouchEnd={touchEnd}
>
</div>
</div>
);
}
const touchMove =(e) => {
//Inside the touch event, we use the ref hook to take the coordinates of the object.
let posa = ref.current.getBoundingClientRect();
//Next, we check the coordinates of the touch using clientX / Y checking that the touch is inside the object.
if(e.touches[0].clientX > posa.left &&
e.touches[0].clientX < posa.right &&
e.touches[0].clientY > posa.top &&
e.touches[0].clientY < posa.bottom ) {
ourFunc()
}
//If the condition is not met, we call the function that should be triggered when the finger is released.
else {
stopFunc()
}
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.