Riddle me this: how would you build a menu component that closes automatically and why?

#1

I have a menu component that opens when you click. I need it to close whenever the user clicks on option or anywhere else on the page. How would you solve this problem and what are the reasons for your approach? I can think of a couple but few seem best. The sub points are critiques of each approach.

  1. portal approach (RFC 287, ember-wormhole, ember-elsewhere)

    • The modal example is great, but I’m looking for something that does not require the user to explicitly acknowledge the menu
  2. click masking based on the portal approach

    • behaves as a context menu and requires CSS for behavior. Also, why do I need to add this filler div when all I want is to catch a globally bubbled event?
  3. global event registration / teardown by the menu component

    • nicely contained within the component but violates component contracts by introducing global click behavior
  4. click handler service to manage document.addEventListener approach in #2.

    • while fitting into Ember architecture, what significant value does this bring? From the perspective of event listening, window.document and Ember.Service are both long lived.
  5. a component in your application.hbs that wraps everything plus a service

    • cache invalidation issues and spreads the behavior across the app

Feel free to open a PR with a solve-your-idea branch in that repo!

1 Like
#2

I’ll defer Ember solutions to more experienced Ember devs here but a general question:

What solution brings the least amount of overhead to your project? A traditional event JS DOM listener, while an older solution, will probably create the least amount of headache in negotiating the rest of your files.
Especially if only needing to be fired once.

Then again, if you need other requirements for that component elsewhere in the project not layed out here - perhaps that would be redundant within the scope of the Ember app?

Hope this helps at all.

Carlo(BigRubyPy)

#3

I recently made a quicksearch type component which does something similar (but is based around an input and a list of options instead of a button and a list of options and just did this in my component:

  focusIn() {
    this.set('showResults', true);
  },

  focusOut(evt) {
    // if new focus target is not contained within this component, close the menu
    if(!this.element.contains(evt.relatedTarget)) {
      this.set('showResults', false);
    }
  },

No idea if this is a good approach or what you’re looking for, I just kinda threw it in because it worked for what I needed.

Also this only handles the case where it’s not clicking on an item though, for that I handle those explicitly with actions.

I’m also not sure how this appears from an accessibility perspective (probably not great) but I’d think that relying on focus instead of just click is better. :man_shrugging:

1 Like
#4

I’ve use https://www.emberobserver.com/addons/ember-click-outside. I think it would work great for what you want.

1 Like
#5

I’ve done this before. The problem is that it can quickly become overwhelming. Per the ember-basicdropdown docs:

there are two kinds of people. Those who think that dropdowns are an easy thing and those who have actually built one.

For that I would recommend using an addon such as ember-basic-dropdown and save yourself the headache of reinventing the wheel.

That said the bas pattern I have used in cases where a clean and fantastic addon was not available (lord have mercy on your soul if this is the case) is to attach and detach a global event handler.

import Component from ‘@ember/component’;

export default Component.extend({
  open() {
    this.set(‘isOpen’, true);
    this._clickHandler = (evt) => this.handleClickEvent(evt);
    document.addEventListener(‘click’, this._clickHandler, false);
  },

  close() {
    this.set(‘isOpen’, false);
    this.cleanup();
  },

  cleanup() {
    document.removeEventListener(‘click’, this._clickHandler, false);
    this._clickHandler = null;
  },

  willDestroyElement() {
    this._super(...arguments);
    this.cleanup();
  }
});

And then there is some gnarly CSS to make it work. I recomend just using the addon!

2 Likes
#6

Thanks for the responses all!

@BigRubyPy Great question to consider for any refactoring. For this use case, I’m optimizing for readability and expressiveness.

@dknutsen Neat idea. I’ve been hammering on this for a while thinking it must be a click handler but perhaps not.

@lvegerano @sukima I will check these addons out. Our application already has a few implementations, so perhaps one of these can solve the issue.

Agreed. Three cheers for shared conventions! Though mad-prototyping-development-life has spread the definition of this behavior across our app in at least 3 places. A little state here, a little mutablity there. All good

Oh yes. I’ve been learning how dreadful this is for large applications. You mean there are styles affecting this component’s behavior?

#7

I don’t have a better solution… but as a use-case: we recently built a little trivia game. Because it was so small, I just created a ‘ui-state’ service - and then anytime an answer was chosen (or any other type of non-menu button) was clicked (while the menu was open) - I set the currentMenu to null. It only ended up being 4 places (and not just clicking anywhere on the screen) - but I can see how that would get out of hand quickly. You have to think about scrolling / and resizing too. And what if there is a video playing or something that should also subscribe to menu UI type events. Yikes!

For fun: One non-code solution is to build a different interface. Our visual-designer wanted this slide up and down type of thing (which was cool) - but as an example / I explained that if the menu was a full-screen take-over… we wouldn’t have to keep track of that state. You would either have the menu open or you’d have to close it. And, it might not feel as fancy… but it sure changes the budget and the number of things to keep track of - if you’re trying to do something fast.

I didn’t win on the menu - but I did put all the video type stuff in a modal / which really helped out for keeping track of state.

1 Like
#8

It would seem to me that the state management of these edge cases would be best suited to be handled in a service object which offers a clear API. This way those 4+ places simply call a method on the service instead of worry about clearing any state. You could also expose edge case APIs for dealing with videos etc. This way the clean up and choice to change the menu state is the responsibility of the service and not the 4+ call sites.

Or did I mis understand your application structure?

#9

I definitely used a service.

Because it was so small, I just created a ‘ui-state’ service

: )