Reputation: 439
I have created a basic modal using react without any library and it works perfectly, now when I click outside of the modal, I want to close the modal.
here is the CodeSandbox live preview
my index.js:
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
class App extends React.Component {
constructor() {
super();
this.state = {
showModal: false
};
}
handleClick = () => {
this.setState(prevState => ({
showModal: !prevState.showModal
}));
};
render() {
return (
<>
<button onClick={this.handleClick}>Open Modal</button>
{this.state.showModal && (
<div className="modal">
I'm a modal!
<button onClick={() => this.handleClick()}>close modal</button>
</div>
)}
</>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
Upvotes: 24
Views: 90697
Reputation: 1
import { useRef, useState } from "react";
export const Modal = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
const modalContainerRef = useRef(null);
const modalRef = useRef(null);
const handleModal = (e: React.MouseEvent) => {
if (modalContainerRef.current === e.target) {
setIsModalOpen(false);
} else if (modalRef.current === e.target) {
e.stopPropagation();
}
};
return (
<>
{isModalOpen && (
<section
className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-70"
ref={modalContainerRef}
onClick={handleModal}
>
<div
className="flex h-[30%] w-[80%] flex-col items-center gap-6 rounded-2xl bg-white md:h-[30%] md:w-[450px]"
ref={modalRef}
>
</div>
</section>
)}
</>
);
};
Upvotes: 0
Reputation: 49729
I will explain with functional components:
first create ref to get a reference to the modal
element
import { useEffect, useState, useRef } from "react";
const [isModalOpen,setIsModalOpen]=useState(false)
const modalEl = useRef();
<div className="modal" ref={modalEl} >
I'm a modal!
<button onClick={() => this.handleClick()}>close modal</button>
</div>
second in useEffect
create an event handler to detect an event outside the modal element. For this we need to implement capture phase
on an element. (explained here: What is event bubbling and capturing? ). Basically, we are going to register an event handler so that when the browser detects any event, browser will start to look for the event handlers from the top parent HTML element and if it finds it, it will call it.
useEffect(() => {
const handler = (event) => {
if (!modalEl.current) {
return;
}
// if click was not inside of the element. "!" means not
// in other words, if click is outside the modal element
if (!modalEl.current.contains(event.target)) {
setIsModalOpen(false);
}
};
// the key is using the `true` option
// `true` will enable the `capture` phase of event handling by browser
document.addEventListener("click", handler, true);
return () => {
document.removeEventListener("click", handler);
};
}, []);
Upvotes: 7
Reputation: 2545
Without using ref
, it would be a little tricky
Watch this CodeSandBox
Or
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
class App extends React.Component {
constructor() {
super();
this.state = {
showModal: false
};
}
handleClick = () => {
if (!this.state.showModal) {
document.addEventListener("click", this.handleOutsideClick, false);
} else {
document.removeEventListener("click", this.handleOutsideClick, false);
}
this.setState(prevState => ({
showModal: !prevState.showModal
}));
};
handleOutsideClick = e => {
if (!this.node.contains(e.target)) this.handleClick();
};
render() {
return (
<div
ref={node => {
this.node = node;
}}
>
<button onClick={this.handleClick}>Open Modal</button>
{this.state.showModal && (
<div className="modal">
I'm a modal!
<button onClick={() => this.handleClick()}>close modal</button>
</div>
)}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
Upvotes: 11
Reputation: 3661
This worked for me:
const [showModal, setShowModal] = React.useState(false)
React.useEffect(() => {
document.body.addEventListener('click', () => {
setShowModal(false)
})
})
return <>
<Modal
style={{ display: showModal ? 'block' : 'none'}}
onClick={(e) => e.stopPropagation()}
/>
<button onClick={(e) => {
e.stopPropagation()
setShowModal(true)
}}>Show Modal</button>
</>
Upvotes: 2
Reputation: 319
You can check the event.target.className if it's contain the parent class you can close the Modal as below, in case you clicked inside the popup div it will not closed:
handleClick = () => {
if (e.target.className === "PARENT_CLASS") {
this.setState(prevState => ({
showModal: false
}));
}
// You should use e.stopPropagation to prevent looping
e.stopPropagation();
};
Upvotes: 0
Reputation: 71
Use the following onClick method,
<div className='modal-backdrop' onClick={(e) => {
if (e.target.className === 'modal-backdrop') {
setShowModal(false)
}
}}></div>
<div className="modal">
<div>I'm a modal!</div>
<button onClick={() => setShowModal(false)}>close modal</button>
</div>
</div>
.modal-backdrop {
position: absolute;
top: 0;
left: 0;
background: #252424cc;
height: 100%;
width: 100vw;
}
Upvotes: -1
Reputation: 342
You can do it by creating a div for the modal backdrop which sits adjacent to the modal body. Make it cover the whole screen using position absolute and 100% height and width values.
That way the modal body is sitting over the backdrop. If you click on the modal body nothing happens because the backdrop is not receiving the click event. But if you click on the backdrop, you can handle the click event and close the modal.
The key thing is that the modal backdrop does not wrap the modal body but sits next to it. If it wraps the body then any click on the backdrop or the body will close the modal.
const {useState} = React;
const Modal = () => {
const [showModal,setShowModal] = useState(false)
return (
<React.Fragment>
<button onClick={ () => setShowModal(true) }>Open Modal</button>
{ showModal && (
<React.Fragment>
<div className='modal-backdrop' onClick={() => setShowModal(false)}></div>
<div className="modal">
<div>I'm a modal!</div>
<button onClick={() => setShowModal(false)}>close modal</button>
</div>
</React.Fragment>
)}
</React.Fragment>
);
}
ReactDOM.render(
<Modal />,
document.getElementById("react")
);
.modal-backdrop {
position: absolute;
top: 0;
left: 0;
background: #252424cc;
height: 100%;
width: 100vw;
}
.modal {
position: relative;
width: 70%;
background-color: white;
border-radius: 10px;
padding: 20px;
margin:20px auto;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Upvotes: 9
Reputation: 609
The easiest way to get this is to call the closeModal function in the wrapper and stop propagation in the actual modal
For example
<ModalWrapper onClick={closeModal} >
<InnerModal onClick={e => e.stopPropagation()} />
</ModalWrapper>
Upvotes: 47
Reputation: 1
This is how I solved it: BTW, I"m a junior dev, so check it, GL.
In index.html:
<div id="root"></div>
<div id="modal-root"></div>
In index.js:
ReactDOM.render(
<React.StrictMode>
<ModalBase />
</React.StrictMode>,
document.getElementById("modal-root")
);
In the App.js:
const [showModal, setShowModal] = useState(false);
{showModal && (
<ModalBase setShowModal={setShowModal}>
{/*Your modal goes here*/}
<YourModal setShowModal={setShowModal} />
</ModalBase>
In the modal container:
import React, { useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom";
const modalRoot: HTMLElement | null = document.getElementById("modal-root");
const Modal: React.FC<{
children: React.ReactNode;
setShowModal: React.Dispatch<boolean>;
}> = ({ children, setShowModal }) => {
const [el] = useState(document.createElement("div"));
const outClick = useRef(el);
useEffect(() => {
const handleOutsideClick = (
e: React.MouseEvent<HTMLDivElement, MouseEvent> | MouseEvent
) => {
const { current } = outClick;
console.log(current.childNodes[0], e.target);
if (current.childNodes[0] === e.target) {
setShowModal(false);
}
};
if (modalRoot) {
modalRoot.appendChild(el);
outClick.current?.addEventListener(
"click",
(e) => handleOutsideClick(e),
false
);
}
return () => {
if (modalRoot) {
modalRoot.removeChild(el);
el.removeEventListener("click", (e) => handleOutsideClick(e), false);
}
};
}, [el, setShowModal]);
return ReactDOM.createPortal(children, el);
};
export default Modal;
Upvotes: 0
Reputation: 19
This works for me:
Need to use e.stopPropagation
to prevent loop
handleClick = e => {
if (this.state.showModal) {
this.closeModal();
return;
}
this.setState({ showModal: true });
e.stopPropagation();
document.addEventListener("click", this.closeModal);
};
then:
closeModal = () => {
this.setState({ showModal: false });
document.removeEventListener("click", this.closeModal);
};
Hope will help
Upvotes: 1
Reputation: 3426
Please see the attached Codesandbox for a working example.
You were almost there. Firstly, you need to do a callback function in your handleClick()
that will add a closeMenu
method to the document:
handleClick = event => {
event.preventDefault();
this.setState({ showModal: true }, () => {
document.addEventListener("click", this.closeMenu);
});
};
And then toggle the state inside closeMenu()
:
closeMenu = () => {
this.setState({ menuOpen: false }, () => {
document.removeEventListener('click', this.closeMenu);
});
}
Any time you click outside of the component, then it'll close it. :)
Upvotes: 1