2

I'm trying to mimic a header where if you click a dropdown menu, it will display the popup menu and upon clicking outside of the popup menu element, it'll disappear for multiple elements in the header.

Here's a vanilla pen that I modified but could not manage to get it working: https://codepen.io/anon/pen/JOGGzL

handleClick() {
    if (!this.state.popupVisible) {
      document.addEventListener('click', this.handleOutsideClick, false);
    } else {
      document.removeEventListener('click', this.handleOutsideClick, false);
    }

    this.setState(prevState => ({
       popupVisible: !prevState.popupVisible,
    }));
}

handleOutsideClick(e) {
    if (this.node.contains(e.target)) {
      return;
    }

    this.handleClick();
}

I've tried creating unique refs and passing in a parameter through handleClick and handleOutsideClick to differentiate between the two different popup buttons, but I'm running into an issue where it seems to be spawning a lot of EventListeners without removing them correctly.

What would be the most elegant way to toggle either one button at a time and deactivating all if the user clicks outside either popup elements? Would I have to create separate Components to handle this?

Thanks

3
  • I'm confused by your question. Your example seems to be working fine. What is your example doing/not doing that you want? Commented Nov 2, 2017 at 15:58
  • I think people are missing the section that I mention that I want it to toggle only one button at a time. Commented Nov 2, 2017 at 17:31
  • what does that mean? What is "it"? Toggle what? What does toggle do? Commented Nov 2, 2017 at 18:05

4 Answers 4

1

Rather then adding and removing the event in the actual handler, you could use react's lifecycle methods:

componentDidMount() {
    document.addEventListener('click', this.handleOutsideClick);
}

componentWillUnmount() {
    document.removeEventListener('click', this.handleOutsideClick);
}

Your handleOnClick method could do the standard toggle state stuff, but then handleOutsideClick could just always set it to false if its not the dropdown or contains the target:

handleOutsideClick(event) {
    if (this.node === event.target || !this.node.contains(event.target)) {
        this.setState(prevState => ({
             popupVisible: false
        }));
    }
}

The most elegant way to handle multiple pop ups would be to have a class that does all the logic you see above called Popover that renders a child component. You could reuse it in a parent component like this:

const MorePops = () => (
    <div>
        <Popover label={'label text'}>
            <div>{'Child'}</div>
        </Popover>
        <Popover label={'label text'}>
            <div>{'Child'}</div>
        </Popover>
        <Popover label={'label text'}>
            <div>{'Child'}</div>
        </Popover>
        <Popover label={'label text'}>
            <div>{'Child'}</div>
        </Popover>
    </div>
);
Sign up to request clarification or add additional context in comments.

Comments

1

As I understand, you want the popovers to be toggled independently from each other. The fastest way to do it is to move the popover to separate component.

class Popover extends React.Component {
    constructor() {
    super();

    this.handleClick = this.handleClick.bind(this);
    this.handleOutsideClick = this.handleOutsideClick.bind(this);

    this.state = {
      popupVisible: false
    };
  }

  handleClick() {
    if (!this.state.popupVisible) {
      // attach/remove event handler
      document.addEventListener('click', this.handleOutsideClick, false);
    } else {
      document.removeEventListener('click', this.handleOutsideClick, false);
    }

    this.setState(prevState => ({
       popupVisible: !prevState.popupVisible,
    }));
  }

  handleOutsideClick(e) {
    // ignore clicks on the component itself
    if (this.node.contains(e.target)) {
      return;
    }

    this.handleClick();
  }

  render() {
    return (
      <div className="popover-container" ref={node => {this.node=node; }}>
        <button onClick={this.handleClick}>
          Toggle Popover
        </button>
        { this.state.popupVisible && <div className="popover">I'm a popover!</div> }
      </div>
    )
  }
}

And here's your root component

class App extends React.Component {
  render() {
    return (
      <div>
        <Popover/>
        <Popover/>
      </div>
    );
   }
}

ReactDOM.render(<App />, document.getElementById('App'));

Comments

0

I would add removing all popups in the componentdidmount lifecycle method and remove the handleoutsideclick method:

componentDidMount() {
document.addEventListener('click', (e) => {
  console.log(e.target);
  if ([].slice.call(document.querySelectorAll('.popover-container button')).indexOf(e.target) === -1) {
        this.setState({
          popupVisible: false,
         });
       return;
     }
   }, true);
 }

Check this pen: https://codepen.io/Marouen/pen/vWLKKK

Comments

0

If you want to use a tiny component (466 Byte gzipped) that already exists for this functionality then you can check out this library react-outclick. It lets you detect events outside of a component/element.

The good thing about the library is that it also lets you detect clicks outside of a component and inside of another (required for nested popups and modals). It also supports detecting other types of events.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.