I have an <input> element, and I want to show content (div) below the input if it has focus.
Also, on every click on the input, I want to hide/show the div.
The problem is that if the input is not focused, than clicking the input will trigger both onClick and onFocus events, so onFocusHandler will run first so the menu will appear but immidately after that onClickHandler will run and hide it.
This is the code (that doesn't work):
import React from 'react';
const MyComp = props => {
/*
...
state: focused, showContent (booleans)
...
*/
const onFocusHandler = () => {
showContent(true);
setFocused(true);
}
const onClickHandler = () => {
if (focused) {
showContent(false);
} else {
showContent(true);
}
}
return (
<>
<input
onFocus={onFocusHandler}
onClick={onClickHandler}
/>
{
showContent &&
<div>MyContent</div>
}
</>
);
};
export default MyComp;
How can I solve this issue?
This is a very odd desired behavior as an active toggle is rather opposed to an element being focused or not. At first I couldn't see any clear way to achieve the behavior you desire. But I thought it could be achieved.
Use the onMouseDown handler instead of the onClick handler.
Use onFocus handler to toggle on the extra content.
Use onBlur handler to toggle off the extra content.
Code:
const MyComp = (props) => {
const [showContent, setShowContent] = useState(false);
const onFocusHandler = () => {
setShowContent(true);
};
const onBlurHandler = () => {
setShowContent(false);
};
const onClickHandler = () => {
setShowContent((show) => !show);
};
return (
<>
<input
onBlur={onBlurHandler}
onFocus={onFocusHandler}
onMouseDown={onClickHandler}
/>
{showContent && <div>MyContent</div>}
</>
);
};
Note: It should be noted that onMouseDown isn't a true replacement to onClick. With a click event the mouse down and up have to occur in the same element, so if. user accidentally clicks into the input but then drags out and releases, the onClick event should not fire. This may be ok though since the focus event fires before the click event so the input may get focus and toggle the content on anyway. Maybe this quirk is acceptable for your use-case.
You should you onBlur event to setContent to false. There is no need to use the onClick handler for that.
Related
I want create a button click handler to reveal the answer text "Animal". So far I have <button onClick={this.revealAnswer}>Reveal answer</button> and the handler
revealAnswer = () => { };
What information should I put in the handler?
Use the useState hook.
const [reveal, setReveal] = useState(false);
const revealAnswer = () => {
setReveal(reveal => !reveal)
}
...
return (
...
{reveal && text}
...
)
You can add a state variable (boolean type) to the component, something like const [revealAnswer, setRevealAnswer] = useState(false). Here, 'false' being the default value.
In the handler, you can then update the state every time the button gets clicked.
const revealAnswer = () => {
setRevealAnswer(!revealAnswer);
};
And in your JSX you should have condition based on this variable revealAnswer. Eg:
...
{ revealAnswer && textValue }
Hope that helps!
I have a MUI v4 Slider (specifically used as a range slider: https://v4.mui.com/components/slider/#range-slider) component inside an expandable component in a form, however, the onChange handler for the Slider component immediately propagates up into the parent and triggers the onClick handler which controls the hide/show.
In the child:
import { Slider } from '#material-ui/core';
export const MySliderComponent = ({ setSliderValue }) => {
let onChange = (e, value) => {
e.stopPropagation();
setSliderValue(value);
}
return <Slider onChange={onChange} />
}
In the parent:
let [expanded, setExpanded] = useState(false);
let toggle = (e) => setExpanded(!expanded);
return (
<React.Fragment>
<div className={'control'} onClick={toggle}>Label Text</div>
<div hidden={!expanded}>
<MySliderComponent />
</div>
</React.Fragment>
);
Points:
when I click inside the slider component, but not on the slider control, it does not trigger the toggle in the parent
when I click on the slider control, the event immediately (on mouse down) triggers the toggle on the parent
throwing a e.preventDefault() in the onChange handler has no effect
using Material UI v4 (no I can't migrate to 5)
I don't understand why the onChange would trigger the parent's onClick. How do I prevent this, or otherwise include a Slider in expandable content at all?
Edit:
After further debugging I found that if I removed the call to setSliderValue, that the parent did not collapse/hide the expanded content. Then I checked the state of expanded and it seems to be resetting without a call to setExpanded. So it looks like the parent component is re-rendering, and wiping out the state of the useState hook each time.
Following solution worked for me in a simmilar problem:
Give your parent component a unique id (for readability)
div id="parent" className={'control'} onClick={toggle}
Modify the parent's onClick handler (toggle):
let toggle = (e) => {
if (wasParentCLicked()) setExpanded(!expanded);
function wasParentCLicked() {
try {
if (e.target.id === "parent") return true;
} catch(error) {
}
return false;
}
}
For further help refer to the official documentation of the Event.target API: https://developer.mozilla.org/en-US/docs/Web/API/Event/target
I have a simple Modal component and a simple Tooltip component. Both of them can be opened by clicking on the triggering button and dismissed by clicking outside. To detect clicking outside, I use this simple hook:
const useClickAway = (
ref: Ref,
condition: boolean,
handler: Handler
): void => {
useEffect(() => {
const listener = (e: Event) => {
if (!ref.current || ref.current.contains(e.target as Node)) {
return;
}
handler(e);
};
if (condition) {
document.addEventListener('mouseup', listener);
document.addEventListener('touchend', listener);
}
return () => {
document.removeEventListener('mouseup', listener);
document.removeEventListener('touchend', listener);
};
}, [ref, handler, condition]);
};
And this is how I use it:
/*
* ref - reference to the modal container
* isOpen - The modal state.
* handleClose - Handler that closes the modal.
*/
useClickAway(ref, isOpen, handleClose)
It has been working fine so far, but the issue appeared when I try to render my tooltip (which uses Portal to render it into the body element, instead of the react tree) inside this Modal.
When I open the modal and then open the tooltip inside it, clicking on the tooltip is causing the modal to close. Because clicking on the tooltip is considered as clicking outside for the Modal.
Can anyone provide clean solution to this problem?
I believe you can take advantage of forwardRef to pass a ref that is defined in the Modal Tooltip shared parent.
Here is how I would do it:
First, I would re-write both Tooltip and Modal to accept an optional external prop ref. such as:
const Tooltip = React.forwardRef((props, ref) => {
const tooltipLocalRef = useRef(null);
const tooltipRef = ref || tooltipLocalRef;
//
});
// usage:
const tooltipRef = useRef(null);
<Tooltip ref={tooltipRef} anotherProps={someValue} />
Same goes for Modal component, but, in addition to the ref that we'll use for the Modal itself, we'll also send the tooltipRef as an extra prop.
const tooltipRef = useRef(null);
const modalRef = useRef(null);
<Tooltip ref={tooltipRef} anotherProp={someValue} />
<Modal ref={modalRef} tooltipRef={tooltipRef} anotherProp={someValue} />
By doing that, I believe we can check against the click outside the modal and make an exception for when that target is within the tooltipRef.current Node.
An extra work on the modal handleClose handler:
function handleClose(e) {
if (!props.tooltipRef.current || props.tooltipRef.current.contains(e.target as Node)) {
return;
}
setModalOpen(false)
}
I haven't tested that, let me know how it turns out.
In React a have a search bar which shows up after I click "Open" button. Search bar appears smoothly because I use translateX property. Then I would like to use focus() on the input element in this search bar. I tried to use refs. Focus appears but it appears as soon as I press the button and stays all the way the search bar moves from one part of the screen to the other(rememeber, it because I use translateX). So it all rides before my eyes. How to make focus appers after the search bar 'reached the destination' on the screen? I tried
if (props.visibleSearchBar) {
ReactDOM.findDOMNode(inputRef.current).focus()
}
instead of
if (props.visibleSearchBar) {
inputRef.current.focus();
} // same story
const Search = (props) => {
let inputRef = useRef(null);
if (props.visibleSearchBar) {
inputRef.current.focus();
}
return (
<div>
<input ref={inputRef} />
</div >
)
You can disable the input initially, and enable it after the transition ends.
something like this, (it is not tested).
const [disabled, setDisabledState] = useState(true)
useEffect(() => {
const callback = () => {
setDisableState(false)
inputRef.current.focus();
}
inputRef.current.addEventListener('transitionend', callback)
return () => inputRef.current.removeEventListener('transitioned', callback)
}, [])
return (
<div>
<input ref={inputRef} disabled={disabled}/>
</div >
)
I have implemented the click-outside hook to close my menu component on mousedown on the document:
const useClickOutside = onClickOutside => {
const ref = useRef(null);
useEffect(
() => {
const handleClickOutside = e => {
if (ref.current && !ref.current.contains(e.target)) {
onClickOutside(e);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
},
[onClickOutside, ref]
);
return ref;
};
The menu has an input which attaches an onBlur event handler:
const Input = ({ onValueEnter }) => {
const [value, setValue] = useState("");
const handleValueChange = e => setValue(e.target.value);
const handleBlur = () => onValueEnter(value);
return (
<input onBlur={handleBlur} value={value} onChange={handleValueChange} />
);
};
const Menu = ({ onClose }) => {
const ref = useClickOutside(onClose);
return (
<div ref={ref} className="menu">
<h1>Enter value</h1>
<Input onValueEnter={handleValueEnter} />
</div>
);
};
The problem is that the onBlur event on the input never fires if I have focus inside the input and click outside the menu. Codesandbox example is here.
Apparently since react has implemented its own event delegation system by attaching events to the top level instead of the actual dom nodes, global event handlers (like those registered with document.addEventListener) run before the react event handlers (github issue).
So my question is, how to work around this problem? Is it even possible to somehow make the onBlur event handler run first and then run the global event handler?
EDIT: I am already using a hack with setTimeout inside the onClose to temporarily make it work but I would really like to avoid it and find a better solution instead.
I hacked something together to make it work. It revolves around using forwardRef and imperativeHandle to access the value from child when closing. I don't know if that solves your question though.
https://codesandbox.io/embed/zw7jjopqq4