Prevent click event from bubbling up in React and DOM event mix situations

We are offering a menu here. In case the menu is visible, users should be able to close it by clicking anywhere on the screen:

class Menu extends Component {
  componentWillMount() {
    document.addEventListener("click", this.handleClickOutside);
  }

  componentWillUnmount() {
    document.removeEventListener("click", this.handleClickOutside);
  }

  openModal = () => {
    this.props.showModal();
  };

  handleClickOutside = ({ target }) => {
    const { displayMenu, toggleMenu, displayModal } = this.props;

    if (displayMenu) {
      if (displayModal || this.node.contains(target)) {
        return;
      }
      toggleMenu();
    }
  };
  render() {
    return (
      <section ref={node => (this.node = node)}>
        <p>
          <button onClick={this.openModal}>open modal</button>
        </p>
        <p>
          <button onClick={this.openModal}>open modal</button>
        </p>
        <p>
          <button onClick={this.openModal}>open modal</button>
        </p>
      </section>
    );
  }
}

In the menu, users have the option to open a modal by clicking on a button within the menu. To close the modal, there are two options: click on the "close modal" button inside the modal itself, or click on the backdrop/overlay outside the modal:

class Modal extends Component {
  hideModal = () => {
    this.props.hideModal();
  };

  onOverlayClick = ({ target, currentTarget }) => {
    if (target === currentTarget) {
      this.hideModal();
    }
  };

  render() {
    return (
      <div className="modal-container" onClick={this.onOverlayClick}>
        <div className="modal">
          <button onClick={this.hideModal}>close modal</button>
        </div>
      </div>
    );
  }
}

When both the menu and modal are open, I want the modal to close only when the user clicks on the close modal button or modal overlay. The menu should remain open until the second click (when the modal is already closed). This condition should handle that scenario:

if (displayModal || this.node.contains(target)) {
  return;
}

If the displayModal is true, then nothing should happen. However, in my case, the hideModal function executes faster than toggleMenu, which causes issues as displayModal will already be set to false when handleClickOutside is called.

For a complete test case with an open menu and modal from the start, please visit:

https://codesandbox.io/s/reactredux-rkso6

Answer №1

Delving deeper into this topic, my recent investigation on a similar issue has led me to a more detailed explanation. If you're short on time, simply jump straight to the solutions below.

Possible Solutions

I have two solutions in mind - one offers a quick and easy fix, while the other is a cleaner approach that involves an additional click handler component.

1.) Quick Fix

In Modal.js, within the onOverlayClick function, incorporate stopImmediatePropagation as shown:

  onOverlayClick = e => {
    // Preventing click propagation in the React event system
    e.stopPropagation();
    // Stopping click propagation to the native document click listener in Menu.js
    e.nativeEvent.stopImmediatePropagation();
    if (e.target === e.currentTarget) {
      this.hideModal();
    }
  };

When there are two click listeners registered on the document - the top-level React listener and your listener in

Menu.js</code -, using <code>e.nativeEvent
accesses the native DOM event encapsulated by React. By employing stopImmediatePropagation, you can prevent the second listener from triggering, thereby avoiding unintended closure of the menu when attempting to close the modal. For further details, refer to the provided link.

Codesandbox Demo

2.) The Cleaner Approach

This solution simplifies matters by utilizing event.stopPropagation. All event handling, including the outside click handler, is managed by React, obviating the need for

document.addEventListener("click",...)
. The click-handler.jsx serves as a proxy intercepting all click events at the top level and directing them through the React event system to the designated components.

Create click-handler.jsx:

import React from "react";

export const clickListenerApi = { addClickListener, removeClickListener };

export const ClickHandler = ({ children }) => {
  return (
    <div 
        style={{ minHeight: "100vh" }} 
        onClick={e => { 
            clickListeners.forEach(cb => cb(e)); 
        }}
    >
      {children}
    </div>
  );
};

let clickListeners = [];

function addClickListener(cb) {
  clickListeners.push(cb);
}

function removeClickListener(cb) {
  clickListeners = clickListeners.filter(l => l !== cb);
}

Adapt Menu.js:

class Menu extends Component {

  componentDidMount() {
    clickListenerApi.addClickListener(this.handleClickOutside);
  }

  componentWillUnmount() {
    clickListenerApi.removeClickListener(this.handleClickOutside);
  }

  openModal = e => {
    e.stopPropagation();
    const { showModal } = this.props;
    showModal();
  };

  render() {... }
}

Update index.js:

const App = () => (
  <Provider store={store}>
    <ClickHandler>
      <Page />
    </ClickHandler>
  </Provider>
);

Demo with Codesandbox

Explanation:

If both the modal dialog and the menu are open, clicking outside the modal once triggers the correct behavior in your current code - closing both elements. This occurs because the document has already received the click event and prepares to execute your handleClickOutside function in Menu. Subsequently, preventing it via e.stopPropagation() in the onOverlayClick callback of Modal becomes impractical.

To comprehend the sequence of click event firing, we must grasp that React operates its own synthetic Event Handling system (1, 2). Key point being, React employs top-level event delegation by attaching a single listener to document encompassing all event types.

For instance, consider having a button

<button id="foo" onClick={...}>Click here</button>
nested somewhere in the DOM. Clicking the button kicks off a standard click event propagating up to document, then onward till reaching the DOM root. React intercepts this click event with its solitary listener placed at document, afterward navigating internal virtual DOM anew to identify and call any relevant click callbacks specified with onClick={...} in your components. Hence, your button's onClick logic gets executed subsequently.

An intriguing aspect is that by the time React processes these click events (now synthetic React events), the native click event traversed the complete capture/bubbling cycle in the DOM before fading out from the native DOM! This disparity between native click handlers (document.addEventListener) and React's onEvent traits in JSX makes their combination error-prone and intricate. Always prioritize React event handlers for consistent behavior.

Please explore the following resources for detailed insights:

  • Understanding React's Synthetic Event System (includes linked article)
  • ReactJS SyntheticEvent stopPropagation() only works with React events?
  • Additional Resource on React and DOM Events

Hope this guide proves beneficial to you.

Answer №2

To prevent the event from bubbling up to the parent, include a call to event.stopPropagation() within your onOverlayClick() function along with hiding the modal. Here's how you can implement it:

onOverlayClick = e => {
  e.stopPropagation();
  
  if (e.target === e.currentTarget) {
    this.hideModal();
  }
};

Answer №3

To ensure the displayMenu variable updates properly, simply introduce a small timeout:

class Popup extends Component {
  closePopup = () => {
    const { closePopup } = this.props;
    setTimeout(() => {
      closePopup();
    }, 300); // You can also try with 150ms
  };
  ...

See it in action here

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What's the name of the auto-triggered dropdown menu feature?

Visit Amazon's official website Description: Within the 'Shop by Department' section- A drop-down menu that remains visible without requiring you to hover over it. However, when the browser is in a non-full-screen mode, the menu disappears ...

The error message "Uncaught ReferenceError: e is not defined" is popping up in my code when

As a beginner with React and Material-UI, I am encountering an error that I cannot seem to resolve. When I load a component using material-ui/data-grid, the data grid simply displays "An error occurred" in the app. However, when I load the component withou ...

Is there a way to prevent the arrow up and down events from functioning on the MUI Menu?

My menu features a text field and a button. The text field is of type number, so ideally when I click on it and use the arrow up and down keys, the value should increase and decrease. However, instead of that functionality, the menu items are being selecte ...

Tips for preventing text wrapping in off-canvas design

Seeking advice for my Web Application prototype - looking to prevent text wrapping in off-canvas menu paragraphs using CSS or JS. New to building this type of menu, I used an example from W3Schools to achieve the current design. <!DOCTYPE html> ...

Tips for aligning a div in the center of the screen while maintaining a fixed width and full height

I'm a CSS newbie looking to center a div on the screen at all times. I understand we can achieve this with positioning, but that typically requires fixed width and height. My goal is to have a fixed-width div without a set height. I want it to automat ...

Restrictions on webpage components within code

Is there a maximum limit to the number of HTML elements that a webpage can load on a browser without including CSS or scripts? ...

Concealing visuals for smaller screens

I've been struggling to hide a specific image on my website for mobile devices. I've experimented with various combinations of HTML and CSS code, but I just can't seem to make it work. I suspect the issue might have something to do with my d ...

Exploring React and Babel through VSCode debugging

I am currently working on a project that involves the following scripts: "dev": "babel-node --presets 'react,es2015' src/server.js" "start": "NODE_ENV=development babel-node --presets 'react,es2015' src/server.js" "build": "NODE_ENV=d ...

Encountering an issue with Material UI Dialog: 'offsetWidth' property of null cannot be read

As soon as I enclose the "form" element within a Material UI "Dialog," an error stating "Cannot read property 'offsetWidth' of null" arises. This prevents the automatic width adjustment of InputLabel from functioning properly. Everything works w ...

Trouble with Bootstrap accordion data-toggle function on mobile devices

I noticed that someone else had asked a similar question without receiving an answer. I am currently having issues with Bootstrap's data-toggle for an accordion, particularly on mobile devices (not just IOS but also on Chrome developer tools and my Ga ...

The issue in Docker arises when the standard_init_linux.go script fails on line 219, leading to an error where the user process cannot

Encountering a docker issue while executing the command docker-compose up -d --build. Upon execution, 3 containers namely app, database, and api are created under the application named innovation. However, there seems to be an error when trying to access t ...

Unable to pass extra parameters when using NextAuth

In my NextJS 13 app, I am utilizing NextAuth for authentication. Once the user is authenticated, the session initially returns name, email, and picture. My objective is to introduce an extra parameter called progress that will be updated as the user works. ...

Centering the logo using Material-UI's alignment feature

Need help centering a logo in my login form using Material-UI. Everything else is centered except for the logo, which is stuck to the left side of the card. I've tried adding align="center" and justify="center" under the img tag, but it's still ...

Issue with react-hook-form and Material-UI in React applications

I have been working on creating a form that includes a dynamic field with the option to show or hide it using a switch. My goal is to leverage both react-hook-forms, which offers features like automatically structuring data without manual input and the abi ...

Displaying the age figure in JSX code with a red and bold formatting

I have a webpage with a button labeled "Increase Age". Every time this button is clicked, the person's age increases. How can I ensure that once the age surpasses 10, it is displayed in bold text on a red background? This should be implemented below t ...

CSS - turn off inheritance for font styling

I am trying to figure out how to turn off the font:inherit property in Ionic's global CSS. This property is causing issues when I try to style text using ng-bind-html, as it adds unnecessary classes to elements like i and bold. I attempted to override ...

The div is struggling to contain the image, can anyone assist with this issue?

Greetings all! I am a beginner with CSS and currently working on my website. You can check it out at this link. I am facing an issue where the image on my site is not fitting properly and it keeps repeating. Can anyone guide me on how to fix this using CS ...

What could cause the cookie value in the dynamic route to differ?

After implementing a program with next js, I encountered an issue related to using cookies on dynamic pages. The problem is that the cookie value seems to be shared across different pages, when in fact each page should have its own unique cookie. My goal ...

Encountered an error: data.map is not functioning as expected in the React component

Hi there, I've encountered a small issue with my modal component. The error message I'm getting is: Uncaught TypeError: meatState.map is not a function. Any suggestions on what may be causing this problem? Your assistance would be greatly appreci ...

Instead of overlapping the content, my overlay shifts the entire page downwards

I am looking to create a dynamic user experience where clicking on an image triggers a <div> overlay that darkens the background. However, instead of simply appearing over the current screen, I want the overlay to push everything else beneath it. Be ...