7
votes

I have multiple custom build dropdown component of react on a page. I trigger the list item to open using setState

toggleDropdown = (id) => {
        this.setState(prevState => ({
            [`dropdown${name}`]: !prevState[`dropdown${id}`] //dropdownA, dropdownB, dropdownC and so on
        }))
    }

this will also toggle it if the dropdown was clicked when the menu is open. But then I have few more dropdowns, other dropdown won't close if I open a dropdown, how to solve this? I do a "hacky" way mixing react with jquery in componentWillMount, binding click event on body, check if the dropdown item is visible, if yes close it.

My question is, is there any better practice to avoid using jquery?

3
can you provide an example of your app.js? - Yuri Ramos

3 Answers

21
votes

My suggestion is that you use the synthetic events onFocus and onBlur to trigger open/close state. onFocus will trigger when the element is clicked and onBlur will trigger when "unfocusing" (clicking outside). See docs.

Also tabIndex attribute/prop is needed for focus/blur to work on non input type elements.

I can recommend looking at the source of react-select and how they handle focusing/bluring.

Here is an example, you can see a demo of it here

import React from "react";

class Dropdown extends React.Component {
  state = {
    open: false
  };

  toggleDropdown() {
    this.setState({ open: !this.state.open });
  }

  render() {
    return (
      <div
        style={{ border: "1px solid #CCC" }}
        onBlur={() => this.toggleDropdown()}
        onFocus={() => this.toggleDropdown()}
        tabIndex="0"
      >
        Dropdown
        {this.state.open && (
          <div>
            <div>Red</div>
            <div>Green</div>
            <div>Blue</div>
          </div>
        )}
      </div>
    );
  }
}

export default Dropdown;
3
votes

You could use the react-onclickoutside library that abstracts the body event handling for you. You just have to wrap your component in their onClickOutside higher order component that will execute the handleClickOutside callback any time the click happens outside of the drop down. That way you can have a private "open" state in all dropdowns and don't care about how the handling is achieved.

The code might look like that:

import React, { Component } from 'react'
import onClickOutside from 'react-onclickoutside'

class Dropdown extends Component {
    constructor() {
        this.state = {open: false}
    }

    // Method automatically executed by the library.
    handleClickOutside() {
        this.setState({open: false})
    }

    render() { ... }
}

export default onClickOutside(Dropdown)
0
votes

First we create the state with the open value being false so the dropdown is closed as default. We will then add an event listener for click that will close the dropdown and remove the event listener after the dropdown is closed. We then add an onClick event to our button to change the state from false to true using toggleOpen(). We'll then wrap our content in an if/or to check that the content opens:

`class Dropdown extends React.Component {

     constructor() {
         super();

         this.state = {
             open: false
         }
         this.toggleOpen = this.toggleOpen.bind(this);
         this.toggleClosed = this.toggleClosed.bind(this);
     }

    toggleOpen() {
        this.setState({ open: true }, () => {
            document.addEventListener('click', this.toggleClosed)
        })
    }

   toggleClosed() {
       this.setState({ open: false}, () => {
           document.removeEventListener('click', this.toggleClosed)
       })
   }

   render() {
       return(
           <div>
               <button onClick=({ this.toggleOpen })>
                   Dropdown
               </button>
               {
                    this.state.open
                    ? (
                          <span> Dropdown</span><br /><br />
                          <span> content</span><br /><br />
                      ) :
                      (
                          null
                      )
        </div>
   }
   }`