Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Detect click outside multiple components

I'm trying to detect a click outside a set of components. I have multiple components called Block, inside an Artboard component. Inside each Block component, this is what I have as of now:

const handleClickOutside = (e) => {
  if (ref && !ref.contains(e.target)) {
    setSelected(false);
  }
}

So this works, but whenever I want to actually click on a block and select it, handleClickOutside() actually fires because I'm clicking outside every other block component.

 -------
|       |
|       |
|       |
 -------

^ This is a selected block

When I click outside of it, it will be deselected.

 -------
|       |
|       |
|       |
 -------

^ But when I click on this block, I'm also clicking
outside the beforementioned block, which causes the
handleClickOutside function to fire.

I simply want to be able to select a component, then deselect it by clicking outside of it, without affecting other block components.

like image 407
easybakeoven Avatar asked Aug 11 '26 17:08

easybakeoven


2 Answers

EDIT

There are a few simpler approaches than the one with multiple refs and state variables:

  1. One where className of DOM elements decides whether to select or deselect.
import React, { useState, useEffect, useRef } from 'react';

const WrapperComponent = ({ items }) => {
  const [selectedItem, setSelectedItem] = useState(null);
  const componentRef = useRef();

  useEffect(() => {
    const handleClickOutside = (event) => {
      if (componentRef.current) {
        if (!componentRef.current.contains(event.target)) {
          setSelectedItem(null); //Is click outside our wrapperDiv
        } else if (event.target.className === 'wrapperDiv') {
          setSelectedItem(null); //Is click directly on our wrapperDiv
        }
      }
    };

    document.addEventListener('click', handleClickOutside);
    return () => {
      document.removeEventListener('click', handleClickOutside);
    };
  }, []);

  const handleItemClick = (item) => {
    setSelectedItem(item);
  };

  return (
    <div ref={componentRef} className="wrapperDiv">
      {items.map((item, index) => (
        <div
          key={index}
          style={{
            cursor: 'pointer',
            background: selectedItem === item ? 'red' : 'blue',
            margin: '20px',
          }}
          onClick={(event) => {
            handleItemClick(item);
          }}
        >
          {item}
        </div>
      ))}
    </div>
  );
};

// Example usage:
const App = () => {
  const items = ['Item 1', 'Item 2', 'Item 3'];

  return (
    <div>
      <WrapperComponent items={items} />
    </div>
  );
};

export default App;

Here we know what class we want to really listen to clicks on, and based on that we are changing state. A ref is still kept for the wrapper component. The logic just check if current item clicked is outside the wrapper div or if it is the wrapper div itself. In both cases the selection is made to null.

  1. One where we prevent the event bubbling:
import React, { useState, useEffect, useRef } from 'react';

const WrapperComponent = ({ items }) => {
  const [selectedItem, setSelectedItem] = useState(null);
  const componentRef = useRef();

  useEffect(() => {
    const handleClickOutside = (event) => {
      if (componentRef.current) {
        setSelectedItem(null);
      }
    };

    document.addEventListener('mousedown', handleClickOutside);
    return () => {
      document.removeEventListener('mousedown', handleClickOutside);
    };
  }, []);

  const handleItemClick = (item) => {
    setSelectedItem(item);
  };

  return (
    <div ref={componentRef}>
      {items.map((item, index) => (
        <div
          key={index}
          style={{
            cursor: 'pointer',
            background: selectedItem === item ? 'red' : 'blue',
            margin: '20px',
          }}
          onClick={(event) => {
            event.stopPropagation();
            handleItemClick(item);
          }}
        >
          {item}
        </div>
      ))}
    </div>
  );
};

// Example usage:
const App = () => {
  const items = ['Item 1', 'Item 2', 'Item 3'];

  return (
    <div>
      <WrapperComponent items={items} />
    </div>
  );
};

export default App;

Here is a doc explaining the whole thing, but the gist is that we are not allowing events to go up from child components to parent.

I do not recommend to use this blindly as it can lead to all ancsetor event listeners to stop working and this can be a problem based on what we want.

Demo

like image 177
Tushar Shahi Avatar answered Aug 14 '26 06:08

Tushar Shahi


I am not sure if this is the correct way to do this but I have tried this on Codesandbox. And it works for the use case you define.

So in onClick handler of the Box component you call preventDefault and in the handler handleClickOutside you check whether e.defaultPrevented is true.

Here is the Codesandbox Link: https://codesandbox.io/s/headless-wind-tr1ns


Also if you can accommodate to use ref for the Box component. You can read this answer: How to open and close dropdown on btn-click, but in same time with outside click close?

Hope this helps!

Looking for a better answer as well. Thanks :)

like image 34
Yash Joshi Avatar answered Aug 14 '26 05:08

Yash Joshi



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!