GavinJoyce / ember-headlessui

https://gavinjoyce.github.io/ember-headlessui/
Other
92 stars 34 forks source link

Implement `<Menu>` component #7

Open GavinJoyce opened 3 years ago

GavinJoyce commented 3 years ago

This component should try to have as close to the same behaviour and API as the Vue and React original versions in https://github.com/tailwindlabs/headlessui.

TODO:

achambers commented 3 years ago

Hey @GavinJoyce I'm trying to understand the context behind the menu/item-element component and how it relates to the menu/item component. What's the rationale behind it and the need for the two distinct components?

GavinJoyce commented 3 years ago

I don't remember the details. I seem to recall it was needed to keep parity with the Vue version, something to do with needing to have the flexibility of using a yielded item property on the element itself, eg:

<items.Item as |item|>
  <item.Element data-test-is-selected={{item.isActive}}>
    Item A
  </item.Element>
</items.Item>
achambers commented 3 years ago

I don't remember the details. I seem to recall it was needed to keep parity with the Vue version, something to do with needing to have the flexibility of using a yielded item property on the element itself, eg:

<items.Item as |item|>
  <item.Element data-test-is-selected={{item.isActive}}>
    Item A
  </item.Element>
</items.Item>

Hmm. Ok. I’ll take a look at the vue version to try and understand the rationale. On the face of it it looked like an unnecessary extra layer but I’m sure there was a reason behind it. I’ll try and uncover it, for my own knowledge if nothing else.

Thanks for the heads up man.

GavinJoyce commented 3 years ago

👍 I'd be delighted to remove the menu/item-element if we can, it's a pretty ugly API.

achambers commented 3 years ago

👍 I'd be delighted to remove the menu/item-element if we can, it's a pretty ugly API.

Yeh, I think we can. I can't see anything in the Vue component (https://headlessui.dev/vue/menu) that references the concept and I can't make out in my mind why we'd need it (could totally be missing something).

The only thing that seems close to this is this section which talks about rendering as template but I don't think it's really what you were going for. Is something we might want to look at implementing though.

We're going to be needing to use the Menu shortly and the Item.Element API is a bit annoying so I'll look at seeing what modifications I can make at that point, unless you get to it first. 👍 🙌

GavinJoyce commented 3 years ago

Nice. It's possible that the Vue API changed since I last looked at it late last year. It's also possible that I made it more complex that it needed to be. Either way, 🍺 if you manage to remove the need for it

achambers commented 3 years ago

Nice. It's possible that the Vue API changed since I last looked at it late last year.

Absolutely. I get the feeling you started this repo when the headlessui work was pretty much in its infancy.

All good my man, I'll see what I can do when I shift my focus to that component 👍

alexlafroscia commented 3 years ago

I spent a while yesterday thinking about the "menu item" vs. "menu element" problem and whether there's a way to remove the need for the element component. Here's how I'm wrapping my head around it right now:

From looking at the implementation of the React components (and I'm assuming the Vue components are similar), they are able to look up the first child element of the "menu item" and automatically configure it with the attributes it needs, without needing a specific wrapper element. Ember doesn't give us any kind of API for a component to dynamically look at/configure it's children before they are rendered.

What could be done is changing the "menu element" component into a "menu element" modifier, that the user of Menu can attach to whatever existing DOM node they want to consider to be the menu element. This might be cleaner from an API perspective, but we lose the benefits of the declarative API we have for configuring the "menu element" right now.

So, if an API like this might be better:

<Menu as |menu|>
  <menu.Items as |items|>
    <items.Item as |item|>
      <span {{item.setElement}} class={{if menu.isActive "bg-blue"}}>
        I am the menu item!
      </span>
    </items.Item>
  </menu.Items>
</Menu>

and we're OK with imperatively setting attributes and event listeners, we could make that API change!

Unfortunately I don't think there's any sort of compositional API for combining modifiers, so we'd need to essentially re-implement the {{on}} modifier (which might not be that bad, and maybe there's some way we could import the implementation to consume it from JS? That seems like a hack, though)

alexlafroscia commented 3 years ago

BTW

Type to select

This is done as of #57

GavinJoyce commented 3 years ago

Thanks for digging into this @alexlafroscia and for refreshing my memory on why I initially added two components for each item.

If we can't find a way to collapse the definition of an item into a single component, I personally don't see much of a difference between these two APIs:

two components:

<Menu as |menu|>
  <menu.Items as |items|>
    <items.Item as |item|>
      <item.Element @tagName="span" class={{if menu.isActive "bg-blue"}}>
        I am the menu item!
      </item.Element>
    </items.Item>
  </menu.Items>
</Menu>

one component and one modifier:

<Menu as |menu|>
  <menu.Items as |items|>
    <items.Item as |item|>
      <span {{item.setElement}} class={{if menu.isActive "bg-blue"}}>
        I am the menu item!
      </span>
    </items.Item>
  </menu.Items>
</Menu>

but we lose the benefits of the declarative API we have for configuring the "menu element" right now.

If there's a downside to using a modifier, perhaps the existing API is preferable?

alexlafroscia commented 3 years ago

If there's a downside to using a modifier, perhaps the existing API is preferable?

I think you're right!

One API clean-up thing that we could look into is doing something more like this:

<Menu as |menu|>
  <menu.Items>
    <menu.Item as |item|>
      <menu.ItemElement @tagName="span" class={{if menu.isActive "bg-blue"}}>
        I am the menu item!
      </menu.ItemElement>
    </menu.Item>
  </menu.Items>
</Menu>

(Specific names certainly can be bike-shedded)

There's a few nice things here: