15

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"));
AnonymousUser
  • 690
  • 7
  • 26
Zeinab Malaki
  • 333
  • 2
  • 3
  • 7
  • Please add the relevant code snippets here instead of just linking to a third party site – Agney Nov 24 '19 at 13:15

10 Answers10

33

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>
Luis Puente
  • 469
  • 4
  • 3
10

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"));
Hasan Haghniya
  • 2,347
  • 4
  • 19
  • 29
  • I used this technique happily for a custom drop down. But code wise I might add a private variable to the class to hold the node value, to make this more TypeScript safe and relevant, the above writing to an unseen object on the class is very JavaScript-y. – Greg Apr 11 '23 at 12:02
4

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>
Clario
  • 137
  • 1
  • 8
1

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. :)

a7dc
  • 3,323
  • 7
  • 32
  • 50
1

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>
</>
Sam Houston
  • 3,413
  • 2
  • 30
  • 46
1

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);
      };
    }, []);
    
Yilmaz
  • 35,338
  • 10
  • 157
  • 202
0

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

marcostr
  • 9
  • 3
0

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;
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Nov 11 '21 at 14:19
0

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();
  };
-1

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;
}