35

I've created a mobile dropdown menu that toggles open and closed based on state. Once it's open, I would like the user to be able to close the dropdown by clicking anywhere outside the ul.

I'm setting the tabIndex attribute on the ul to 0, which gives the ul "focus". I've also added an onBlur event to the ul that triggers the state change (dropdownExpanded = false) that hides the ul.

<ul tabIndex="0" onBlur={this.hideDropdownMenu}>
  <li onClick={this.handlePageNavigation}>Page 1</li>
  <li onClick={this.handlePageNavigation}>Page 2</li>
  <li onClick={this.handlePageNavigation}>Page 3</li>
</ul>

However, when I implement this fix, the onClick events that I have on each li element fail to fire.

I know something is going on with the event bubbling, but I am at a lose as to how to fix it. Can anyone help?

NOTE:

I know you can create a transparent div below the ul that spans the entire viewport and then just add an onClick even to that div that will change the state, but I read about this tabIndex/focus solution on Stack Overflow and I'd really like to get it working.

Here is a more complete view of the code (the dropdown is for users to select their home country, which updates the ui):

const mapStateToProps = (state) => {
    return {
        lang: state.lang
    }
}

const mapDispatchToProps = (dispatch) => {
    return { actions: bindActionCreators({ changeLang }, dispatch) };
}

class Header extends Component {
    constructor() {
        super();

        this.state = {
          langListExpanded: false
        }

        this.handleLangChange = this.handleLangChange.bind(this);
        this.toggleLangMenu = this.toggleLangMenu.bind(this);
        this.hideLangMenu = this.hideLangMenu.bind(this);

    }

    toggleLangMenu (){
      this.setState({
        langListExpanded: !this.state.langListExpanded
      });
    }

    hideLangMenu (){
      this.setState({
        langListExpanded: false
      });
    }


    handleLangChange(e) {
        let newLang = e.target.attributes['0'].value;
        let urlSegment = window.location.pathname.substr(7);

        // blast it to shared state
        this.props.actions.changeLang( newLang );
        // update browser route to change locale, but stay where they are at
        browserHistory.push(`/${ newLang }/${ urlSegment }`);
        //close dropdown menu
        this.hideLangMenu();
    }

    compileAvailableLocales() {
        let locales = availableLangs;
        let selectedLang = this.props.lang;

        let markup = _.map(locales, (loc) => {
            let readableName = language[ selectedLang ].navigation.locales[ loc ];

            return (
                <li
                  key={ loc }
                  value={ loc }
                  onMouseDown={ this.handleLangChange }>
                    { readableName }
                </li>
            );
        });

        return markup;
    }


    render() {
        let localeMarkup = this.compileAvailableLocales();
        return (
            <section className="header row expanded">
              < Navigation />
            <section className="locale_selection">
                  <button
                    className="btn-locale"
                    onClick={this.toggleLangMenu}>
                    {this.props.lang}
                  </button>
                  <ul
                      className={this.state.langListExpanded ? "mobile_open" : " "}
                      value={ this.props.lang }
                      tabIndex="0"
                      onBlur={this.hideLangMenu}>
                  >
                      { localeMarkup }
                  </ul>

                </section>
            </section>
        )
    }
}

3 Answers3

70

Try using onMouseDown instead of onClick.

Stephen L
  • 2,273
  • 11
  • 16
  • 2
    Thanks, Stephen. I tried that. And that does make the events on the li fire. But now the onBlur event doesn't fire when you click away from the ul. – Jordan England-Nelson May 23 '17 at 18:28
  • Can you show some more of your code? I tested it in [this jsfiddle](https://jsfiddle.net/69z2wepo/79294/) and while there is no code to show and hide the menu, it seems as if adding the onMouseDown handler does not directly interfere with the onBlur handler. – Stephen L May 23 '17 at 18:40
  • Thanks for that link, Stephen. I've add the code for the entire component. – Jordan England-Nelson May 23 '17 at 19:17
  • 1
    It's possible that your dropdown is never gaining focus and thus never loses it, which would trigger the callback. I believe [this fiddle](https://jsfiddle.net/69z2wepo/79299/) does what you're trying to do. The important changes are on line 18 and 67. – Stephen L May 23 '17 at 20:13
  • Yup! That seemed to be what was going on. You solution worked. Thank agains, Stephen! – Jordan England-Nelson May 23 '17 at 23:24
  • This was very helpful thank you, it works fine with react-native-web – Mike Vargas Jul 07 '22 at 23:03
  • I love you dude <3 – Markus Ethur Apr 18 '23 at 20:49
11

The point is the onBlur is triggering a re-render which seems to lead the browser to do not follow up with the onClick: https://github.com/facebook/react/issues/4210

But if you check the onBlur event you can find some info about what's happening, event.relatedTarget is populated and you can use these info to detect when the onBlur is actually triggered by the onClick and chain whatever you need to do.

Max Favilli
  • 6,161
  • 3
  • 42
  • 62
  • 2
    Using event.relatedTarget is especially useful for interactive elements that don't require the onClick handler such as links. – whirish Jun 19 '20 at 23:34
  • Hey, could you look into this? https://stackoverflow.com/questions/63686646/stop-handleblur-from-doing-anything –  Sep 01 '20 at 12:59
  • 4
    `event.relatedTarget` on an onBlur-event is only populated if something new is getting focus. That requires the list elements to be focusable, which may or may not be suitable. https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent/relatedTarget – Per Enström Nov 03 '20 at 07:51
  • When dealing with objects that are expected to get focus (like inputs or links), ```event.relatedTarget?.click()``` does the trick. – Jakob Schödl Jun 02 '22 at 14:17
1

I just ran into this with an array of breadcrumb links, where an onBlur handler was causing a rerender, preventing the link click from working. The actual problem was that react was regenerating the link elements every time, so when it rerendered, it swapped the link out from under the mouse, which caused the browser to ignore the click.

The fix was to add key properties to my links, so that react would reuse the same DOM elements.

<ol>
    {props.breadcrumbs.map(crumb => (
        <li key={crumb.url}>
            <Link to={crumb.url} >
                {crumb.label}
            </Link>
        </li>
    ))}
</ol>
frodo2975
  • 10,340
  • 3
  • 34
  • 41