Zeinab Malaki
Zeinab Malaki

Reputation: 439

React close modal on click outside

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

Answers (11)

TylerDev
TylerDev

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

Yilmaz
Yilmaz

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

Hasan Haghniya
Hasan Haghniya

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

Sam Houston
Sam Houston

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

hussam alhunaiti
hussam alhunaiti

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

Syed Saad Ullah Shah
Syed Saad Ullah Shah

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

Clario
Clario

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

Luis Puente
Luis Puente

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

Alon Fabio
Alon Fabio

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

marcostr
marcostr

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

a7dc
a7dc

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

Related Questions