I have this code structure:
2 React select's wrapped with a div, (it's on element which is then mapped over a list)
<div onContextMenu={openContextMenu}>
<Select />
<Select />
</div>
The issue is when I click right mouse button to open the context menu select menu is being open!
As I understand React-select uses onMouseDown event to open is dropdown menu
How do I prevent it from opening ?
EDIT:
I've got a solution!
Make the <Select /> controlled by using menuIsOpen prop.
Utilize onClick event (Which only registers the left click)
export const ControlledSelect = (props) => {
const ref = useRef(null);
const [ visible, setVisible ] = useState(false);
useOutsideClick(ref, () => {
setVisible(false);
});
const handleClick = () => {
setVisible(true);
};
return (
<div ref={ ref } onClick={ handleClick }>
<Select { ...props } menuIsOpen={ visible } />
</div>
);
};
And a custom useOutsideClick hook, which runs a callback when you click outside the component.
export function useOutsideClick(ref, callback) {
const handleClick = (e) => {
if (ref.current && !ref.current.contains(e.target)) {
callback();
}
};
useEffect(() => {
document.addEventListener('click', handleClick);
return () => {
document.removeEventListener('click', handleClick);
};
});
}
Related
I am trying to create a function close modal when click outside but I am keep getting this error:
TypeError: node.current is not a function
Here is my following code in MemberCard.js:
const [modalStatus, setModalStatus] = useState(false);
const node = useRef(null);
const openModal = () => {
setModalStatus(!modalStatus);
};
const handleClick = (e) => {
if (node.current(e.target)) {
return;
}
// outside click
setModalStatus(false);
};
useEffect(() => {
document.addEventListener("mousedown", handleClick);
return () => {
document.removeEventListener("mousedown", handleClick);
};
}, []);
return (
<div className="member-card">
<div className="member-edit" onClick={openModal}>
<Symlink />
</div>
{modalStatus && (
<TeamStatusModal
active={modalStatus}
ref={node}
tab={tab}
member={member}
/>
)}
...
}
Here is my modal that I open after click:
const TeamStatusModal = (props) => {
const { active, tab, member, ref } = props;
console.log(ref);
return (
<div
className={`team-status-modal-container ${active ? "ACTIVE_CLASS" : ""}`}
>
<button className="status">
<ProfileIcon /> <span>View Profile</span>
</button>
<hr />
<button className="status">
<MessageIcon /> <span>Message Me</span>
</button>
</div>
);
};
How can I implement this feature?
In react, there are some good libraries that can help you with modals, one of them is called react-modal, you can give it a check.
If you want to implement a modal by yourself, we can follow some steps.
First we need to define a context, because the modal state needs to be accesed by more than one component or page in your app.
In the context, you could store the modal in a isModalOpen state, and add functions to manipulate it, such as openModal and closeModal. It really depends on the amount of features you want to add to this implementation.
Finally, you make the context globally accessible wrapping your app around a provider.
an example implementation
const ModalContext = createContext({})
export const ModalContextProvider = ({children}) => {
const [isModalOpen, setIsModalOpen] = useState(false)
const toggleModalState = () => {
setIsModalOpen(state => !state)
}
return <ModalContext.Provider value={{isModalOpen, toggleModalState}}>{children}<ModalContext.Provider>
}
export const useModal = () => {
return useContext(ModalContext)
}
Now the modal will be available globally
Let's say we have a component Accordion that has an internal state isOpen, so you can close and open this component.
We now want to have a parent component that also has a state isOpen and has button. In this component, we have 2 times Accordion and we are passing to Accordion isOpen and we want that if the parent changes state isOpen Accordion accept this.
All component are functional components
const Accordion = ({ isOpen: parentIsOpen = false }) => {
const [isOpen, setIsOpen] = useState(parentIsOpen);
const handleSetIsOpen = () => setIsOpen(!isOpen);
return (
<div>
I'm open: {isOpen}
<button onClick={handleSetIsOpen}>toggle isOpen child</button>
</div>
);
};
const MasterComponent = () => {
const [isOpen, setIsOpen] = useState(false);
const handleSetIsOpen = () => setIsOpen(!isOpen);
return (
<div>
<button onClick={handleSetIsOpen}>toggle isOpen parent</button>
<Accordion isOpen={isOpen} />
<Accordion isOpen={isOpen} />
</div>
);
};
In this case above Accordion will take on first render as the initial state parent isOpen prop. In case we press the button toggle isOpen parent we will change the parent state but children will not be updated.
To fix this we can use useEffect
const Accordion = ({ isOpen: parentIsOpen = false }) => {
const [isOpen, setIsOpen] = useState(parentIsOpen);
const handleSetIsOpen = () => setIsOpen(!isOpen);
useEffect(() => {
if (parentIsOpen !== isOpen) {
setIsOpen(parentIsOpen);
}
}, [parentIsOpen]);
return (
<div>
I'm open: {isOpen}
<button onClick={handleSetIsOpen}>toggle isOpen child</button>
</div>
);
};
const MasterComponent = () => {
const [isOpen, setIsOpen] = useState(false);
const handleSetIsOpen = () => setIsOpen(!isOpen);
return (
<div>
<button onClick={handleSetIsOpen}>toggle isOpen parent</button>
<Accordion isOpen={isOpen} />
<Accordion isOpen={isOpen} />
</div>
);
};
in this case, children will be properly updated when a parent changes isOpen state.
There is one issue with this:
"React Hook useEffect has a missing dependency: 'isOpen'. Either include it or remove the dependency array react-hooks/exhaustive-deps"
So how to remove this issue that esLint is complaining and we do not want to put isOpen in this since it will cause bug.
in case we add isOpen into the array like this:
useEffect(() => {
if (parentIsOpen !== isOpen) {
setIsOpen(parentIsOpen);
}
}, [parentIsOpen, isOpen]);
We will have then a situation where we will click on the internal button in accordion and update the internal state then useEffect will run and see that parent has a different state than the child and will immediately set the old state.
So basically you have a loop where the accordion will never be open then.
The question is what is the best way to update the child state based on the parent state?
Please do not suggest to put all-state in parent and pass props without child state. this will not work since both Accordions in this example have to have their own state and be able to open and close in an independent way, but yet if parent says close or open it should listen to that.
Thank you!
Actually I would say this is way to do it
const Accordion = ({ isOpen: parentIsOpen = false }) => {
const [isOpen, setIsOpen] = useState(parentIsOpen);
const handleSetIsOpen = () => setIsOpen(!isOpen);
useEffect(() => {
setIsOpen(parentIsOpen);
}, [parentIsOpen]);
return (
<div>
I'm open: {isOpen}
<button onClick={handleSetIsOpen}>toggle isOpen child</button>
</div>
);
};
const MasterComponent = () => {
const [isOpen, setIsOpen] = useState(false);
const handleSetIsOpen = () => setIsOpen(!isOpen);
return (
<div>
<button onClick={handleSetIsOpen}>toggle isOpen parent</button>
<Accordion isOpen={isOpen} />
<Accordion isOpen={isOpen} />
</div>
);
};
So just remove state check in a child component, let him update the state but since is updated with the same value it will not rerender or do some expensive behavior.
Tested it today and with a check, if states are different or without is the same, react takes care to not trigger rerender if the state that is updated is the same as before.
What you’re saying not to suggest is in fact the solution I would offer… You’ll need state to control isOpen for the parent component. Also, you should have separate methods in the parent that control state for each accordion, passed along to each accordion in props…
Not sure why you want separate state for the child components. I believe something like this would suffice.
const MasterComponent = () => {
const [isOpen, setIsOpen] = useState(false);
const [isOpen1, setIsOpen1] = useState(false);
const [isOpen2, setIsOpen2] = useState(false);
const handleParentClose = () => {
setIsOpen(false);
setIsOpen1(false);
setIsOpen2(false);
}
return (
<div>
<button onClick={handleParentClose}>toggle isOpen parent</button>
<Accordion isOpen={isOpen1} setIsOpen={setIsOpen1} />
<Accordion isOpen={isOpen2} setIsOpen={setIsOpen2} />
</div>
);
};
const Accordion = props => {
return (
<div>
I'm open: {props.isOpen}
<button onClick={props.setIsOpen}>toggle isOpen child</button>
</div>
);
}
This doesn't include code for actual visibility toggle, but the state is there, including state that closes accordions on parent close.
I have a search input and showing the result like popover on bottom.
https://prnt.sc/sg11m9
const AutoComplete = connectAutoComplete(
({ hits, currentRefinement, refine }) => {
const [focus, isFocus] = useState(false);
return(
<div>
<input
type="search"
placeholder="Search movies"
value={currentRefinement}
onChange={event => refine(event.currentTarget.value)}
onFocus={() => isFocus(true)}
onBlur={() => isFocus(false)}
/>
{
focus && currentRefinement ?
( <><ul onMouseOver={() => isFocus(true)} >
{hits.map(hit => (
<li key={hit.objectID}>
<Link to={`/movies/${hit.slug}`}><Highlight tagName="mark" hit={hit} attribute="title" /></Link>
</li>
))}
</ul>
<CustomStateResults /></>
) : ''
}
</div>
)
}
);
I set a state for onFocus and onBlur attribute,so that the popover shows when if its in focus and hide when its onBlur.
but my problem is, I have a link on popover, If I click the link it triggers the onBlur, so technically the popover hides and doesnt go to link.
How can I click on the links inside wihtout triggering the onblur and I also need to hide the popover when you click anywhere the DOM or if youre not onfocus on textbox?
I just solved it!
the onBlur function is not good.
It should be, when you click outside the component that will trigger the setstate value of focus into false.
heres the code
function useOnClickOutside(ref, handler) {
useEffect(
() => {
const listener = event => {
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
},
[ref, handler]
);
}
const AutoComplete = connectAutoComplete(
({ hits, currentRefinement, refine }) => {
const [focus, isFocus] = useState(false);
const ref = useRef();
useOnClickOutside(ref, () => isFocus(false));
return(
<div>
<input
type="search"
placeholder="Search movies"
value={currentRefinement}
onChange={event => refine(event.currentTarget.value)}
onFocus={() => isFocus(true)}
/>
{
focus && currentRefinement ?
( <><ul ref={ref}>
{hits.map(hit => (
<li key={hit.objectID}>
<Link to={`/movies/${hit.slug}`}><Highlight tagName="mark" hit={hit} attribute="title" /></Link>
</li>
))}
</ul>
<CustomStateResults /></>
) : ''
}
</div>
)
}
);
Suggestions and tips are still welcome. Thanks!
I am using hook in component to manage modal state.
(Class version of component reproduce the problem)
handleClick will open modal and handleModalClose should close.
I send handleModalClose to Modal component and with console.log could see, that it is processed, but the isModalOpen state not changed (the same for callback setState).
When I am trying to invoke it with setTimeout - state changes and Modal is closing.
Why the hell the state not changes when I invoke changing from child???
const [isModalOpen, setModalOpen] = useState(false);
const handleClick = () => {
setModalOpen(true);
// setTimeout(() => handleModalClose, 10000);
};
const handleModalClose = () => {
console.log('!!!!!!!!handleModalClose');
setModalOpen(false);
};
return (
<div onClick={handleClick}>
{isModalOpen && <Modal closeModal={handleModalClose} />}
</div>
);
and here is extract from Modal
const Modal = (props) => {
const { closeModal } = props;
return (
<>
<div className="modal">
<form className="" onSubmit={handleSubmit(onSubmit)}>
<button type="button" className="button_grey button_cancel_modal" onClick={closeModal}>
</button>
PROBLEM SOLVED. e.stopPropagation() - added.
const handleModalClose = (e) => {
e.stopPropagation();
console.log('!!!!!!!!handleModalClose');
setModalOpen(false);
};
Modal was closed and instantly reopen by bubbling w/o this.
Aimed functionality:
When a user clicks a button, a list shows. When he clicks outside the list, it closes and the button should receive focus. (following accessibility guidelines)
What I tried:
const hideList = () => {
// This closes the list
setListHidden(true);
// This takes a ref, which is forwarded to <Button/>, and focuses it
button.current.focus();
}
<Button
ref={button}
/>
Problem:
When I examined the scope of hideList function, found that ref gets the proper reference to button every where but inside the click event handler, it's {current: null}.
The console outputs: Cannot read property 'focus' of null
Example:
https://codepen.io/moaaz_bs/pen/zQjoLK
- click on the button and then click outside and review the console.
Since you are already using hooks in your App, the only change you need to make is to use useRef instead of createRef to generate a ref to the list.
const Button = React.forwardRef((props, ref) => {
return (
<button
onClick={props.toggleList}
ref={ref}
>
button
</button>
);
})
const List = (props) => {
const list = React.useRef();
handleClick = (e) => {
const clickIsOutsideList = !list.current.contains(e.target);
console.log(list, clickIsOutsideList);
if (clickIsOutsideList) {
props.hideList();
}
}
React.useEffect(function addClickHandler() {
document.addEventListener('click', handleClick);
}, []);
return (
<ul ref={list}>
<li>item</li>
<li>item</li>
<li>item</li>
</ul>
);
}
const App = () => {
const [ListHidden, setListHidden] = React.useState(true);
const button = React.useRef();
const toggleList = () => {
setListHidden(!ListHidden);
}
const hideList = () => {
setListHidden(true);
button.current.focus();
}
return (
<div className="App">
<Button
toggleList={toggleList}
ref={button}
/>
{
!ListHidden &&
<List hideList={hideList} />
}
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
Working demo
The reason that you need it is because on every render of your Functional component, a new ref will be generated if you make use of React.createRef whereas useRef is implemented such that it generates a ref when its called the first time and returns the same reference anytime in future re-renders.
P.S. A a thumb rule, you can say that useRef should be used when you
want to have refs within functional components whereas createRef
should be used within class components.
Create your ref
this.button = React.createRef();
Add Ref to your DOM element
ref={this.button}
Use the Ref as per requirement
this.button.current.focus();
Complete code using forwarding-refs
const Button = React.forwardRef((props, ref) => {
return (
<button
onClick={props.toggleList}
ref={ref}
>
button
</button>
);
})
const List = (props) => {
const list = React.createRef();
handleClick = (e) => {
const clickIsOutsideList = !list.current.contains(e.target);
if (clickIsOutsideList) {
props.hideList();
}
}
React.useEffect(function addClickHandler() {
document.addEventListener('click', handleClick);
return function clearClickHandler() {
document.removeEventListener('click', handleClick);
}
}, []);
return (
<ul ref={list}>
<li>item</li>
<li>item</li>
<li>item</li>
</ul>
);
}
const button = React.createRef();
const App = () => {
const [ListHidden, setListHidden] = React.useState(true);
const toggleList = () => {
setListHidden(!ListHidden);
}
const hideList = () => {
setListHidden(true);
console.log(button)
button.current.focus();
}
return (
<div className="App">
<Button
toggleList={toggleList}
ref={button}
/>
{
!ListHidden &&
<List hideList={hideList} />
}
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));