I have the following "simple" Modal
React component and I’m trying to make it close itself when I click outside the dialog
element i.e. the backdrop:
import { useState, useRef, useEffect } from "react";
export default function Modal({ isOpen, onClose, children }) {
const [isModalOpen, setModalOpen] = useState(isOpen);
const modalRef = useRef(null);
useEffect(() => {
setModalOpen(isOpen);
}, [isOpen]);
useEffect(() => {
const modalElement = modalRef.current;
if (modalElement) {
if (isModalOpen) {
modalElement.showModal();
} else {
modalElement.close();
}
}
}, [isModalOpen]);
const handleCloseModal = () => {
if (onClose) {
onClose();
}
setModalOpen(false);
};
const handleKeyDown = (e) => {
if (e.key === "Escape") {
handleCloseModal();
}
};
// TODO: this does not seem to be working
const handleOutsideClick = (e) => {
if (
modalRef.current &&
isModalOpen &&
e.target.className === "modal-container" // also tried !e.target.contains(modalRef.current)
) {
console.log("Clicked outside of the modal, closing!");
handleCloseModal();
}
};
return (
<div className="modal-container" onClick={handleOutsideClick}>
<dialog className="modal" ref={modalRef} onKeyDown={handleKeyDown}>
{children}
</dialog>
</div>
);
}
Unfortunately, this does not seem to do the trick. After a bit of debugging, I can see that the e.target
is not the "background" div
(i.e. modal-container
backdrop) but instead the modal
itself which is what I’m assuming is what’s preventing this from working properly. Is there some reason why the onClick
event is somehow "captured" by the dialog
element? Am I missing something here?
2
Answers
The issue you’re encountering is likely due to event bubbling and event delegation. When you click on an element within the modal (such as a button or input), the click event bubbles up the DOM tree. In your case, the click event eventually reaches the dialog element itself, which is capturing the click event and preventing it from reaching the backdrop
(modal-container)
element.To solve this problem, you can use the concept of event delegation. Instead of attaching the click event directly to the modal-container element, you can attach it to a higher-level element that encompasses both the modal and the backdrop. Then, you can check if the target of the click event is the backdrop itself or one of its children (i.e., the modal). If it’s the backdrop, you can close the modal.
Here’s how you can implement this approach:
In this modified version, the click event is attached to the modal-container element. When a click occurs, the
handleOutsideClick
function checks if the target of the click event is the modal-container itself. If it is, it means the click occurred outside the modal, and the modal can be closed. If the click occurred inside the modal, the event will bubble up to the dialog element, but it won’t trigger the closing of the modal.I apologize for the oversight. You are correct that the code I provided is very similar to yours, with the only difference being the condition in the handleOutsideClick function.
Since you’ve already tried checking for
e.target.className === "modal-container"
and!e.target.contains(modalRef.current)
, it seems like the issue might be related to the event propagation. When you click inside the dialog, the event bubbles up to the modal-container, but it seems like it’s not detected correctly as the backdrop click.One possible reason for this behavior could be related to how the dialog element interacts with event bubbling. The dialog element may prevent its parent (the modal-container) from receiving the click event, causing it to not register as a click outside the modal.
To address this issue, you can try stopping the propagation of the click event when it occurs inside the dialog element. This way, the click event won’t bubble up to the
modal-container
, and your check in handleOutsideClick will work as expected.Here’s how you can update your code to stop event propagation:
By adding the handleDialogClick function and attaching it to the dialog element’s onClick event, we prevent the click event from propagating up to the modal-container. This should ensure that clicks inside the dialog don’t trigger the closing of the modal when you intend to click on the backdrop.