Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[POC] MenuButton - Option 1 implementation #35671

Closed
wants to merge 2 commits into from

Conversation

michaldudak
Copy link
Member

@michaldudak michaldudak commented Dec 30, 2022

DO NOT MERGE

This is a playground for evaluating the API of a MenuButton component.
It implements Option 1 from #32088 (comment)

Playground: https://codesandbox.io/s/menubutton-option-1-byxysy

https://deploy-preview-35671--material-ui.netlify.app/experiments/base/menu/

This is a POC that should give a general idea of how the API can look like. It is by no means a complete implementation. There may be some rough edges or unimplemented features.
Do not analyze the implementation - it's quick and dirty.

@mui-bot
Copy link

mui-bot commented Dec 30, 2022

Messages
📖 Netlify deploy preview: https://deploy-preview-35671--material-ui.netlify.app/

Details of bundle changes

Generated by 🚫 dangerJS against d17b252

@michaldudak michaldudak added the proof of concept Studying and/or experimenting with a to be validated approach label Jan 2, 2023
return (
<MenuButtonContext.Provider value={contextValue}>
<Root {...rootProps}>{label}</Root>
{children}
Copy link
Member

@siriwatknp siriwatknp Jan 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the MenuButton pass these props (or from context)?

Suggested change
{children}
{React.cloneElement(children, { anchorEl, ref })}

One important reason of having a MenuButton component (in my opinion) is that developers don't have to declare the anchorEl and open at the top of the component. The usage should be like this:

<MenuButtonUnstyled
  slots={{ root: MenuButton }}
  label="My account"
>
  <MenuUnstyled
    slots={{ root: Popper, listbox: StyledListbox }}
    slotProps={{ root: { placement: 'bottom-start' }, listbox: { id: 'simple-menu' } }}
  >

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having {React.cloneElement(children, { anchorEl, ref })} prevents developers from having intermediate components between MenuButton and Menu (or a Fragment). I'll try to achieve this with a context instead.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for using context

@michaldudak michaldudak added the package: base-ui Specific to @mui/base label Jan 3, 2023
Copy link
Member

@siriwatknp siriwatknp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! Can you also add a useMenuButton hook?

@michaldudak
Copy link
Member Author

I'd like to focus on implementing other options first so we can evaluate them before going deeper into any of them. Ultimately, after we choose a winning option, I'll create both a component and a hook.

@oliviertassinari oliviertassinari added the component: menu This is the name of the generic UI component, not the React module! label Jan 9, 2023
Copy link
Member

@oliviertassinari oliviertassinari left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise, no real concerns with this direction. it sounds fair 👍

Comment on lines +142 to +143
slots={{ root: MenuButton }}
label="My account"
Copy link
Member

@oliviertassinari oliviertassinari Jan 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of slots feels strange. Aren't the slot means to replace existing implementation for customization use cases? In this case, I would imagine that what's rendered as a menu trigger will most of the time be "userland". So I think that someone like this:

Suggested change
slots={{ root: MenuButton }}
label="My account"
trigger={<MenuButton>My account</MenuButton>}

would make more sense. Also notice that it's an element, not a component in this proposal. Why? For the extra flexibility needed when dealing with userland APIs. By "userland", I mean API that is used by the product teams in the normal course of building a product using a design system. This is to be distinguished from the API used by the product infrastructure team, the one building the design system with MUI Base.

Actually, in the demo, I think that this terminology would be clearer like this, to signal it can be any button:

Suggested change
slots={{ root: MenuButton }}
label="My account"
trigger={<Button>My account</Button>}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would imagine that what's rendered as a menu trigger will most of the time be "userland"

Why do you think developers will want to use something different than button by default? Do you think Menu is different in this regard than, let's say, a Select?

Copy link
Member

@oliviertassinari oliviertassinari Jan 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you think developers will want to use something different than button by default? Do you think Menu is different in this regard than, let's say, a Select?

I'm more thinking about the following use cases:

      <MenuButtonUnstyled
        slots={{ root: Button }}
        label="My account"
        variant="outlined"
        size="small"
      >

I think that at the very least label should be renamed to children:

      <MenuButtonUnstyled
        slots={{ root: Button }}
        variant="outlined"
        size="small"
      >
        My account
      </MenuButtonUnstyled>

It could be clearer with this to match the terminology used in Material UI / Joy UI slots={{ root: Button }} -> component. But it's still the same concept so I guess people will learn about it. To me, it's also that it doesn't look very practical to write if it's a common use case.


An even more flexible option (it would use React.cloneElement under the hood) could be:

      <MenuTrigger>
        <Button variant="outlined" size="small">
          My account
        </Button>
      >

with one downside: more boilerplate, which developers can abstract if they want (easy), but they can choose, we don't decide for them.

}
}, [isOpen]);

const Root = slots.root || 'button';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only thing I am not sure here is the name root. I would imagine the MenuButton to have slots menu and button, or if we want to be more generic menu and trigger. Considering the menu is the children element, I would rename the slot.root to slot.trigger.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, it makes sense. With a small caveat - we usually forward extra props to the root slot. I wonder where they should go in this case. I'd say the button slot, but this will have to be well documented.

Copy link
Member

@siriwatknp siriwatknp Jan 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, isn't it better to keep only the root slot for the MenuButtonUnstyled for consistency?

From my understanding, MUI Base provides slots prop for inserting components (usually with styles). The root refers to the topmost DOM of the component. In this case, the MenuButtonUnstyled has only one DOM (default as button) so I am not sure why it needs menu slot.

Changing it to trigger will make the concept inconsistent and developers have to remember one more thing.

`,
);

const MenuButton = styled('button')(
Copy link
Member

@oliviertassinari oliviertassinari Jan 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that it would be better to call it Button to reenforce that developers can directly use the button from their design system:

Suggested change
const MenuButton = styled('button')(
const Button = styled('button')(

`,
);

const StyledMenuItem = styled(MenuItemUnstyled)(
Copy link
Member

@oliviertassinari oliviertassinari Jan 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we don't need the Styled prefix if the base component has Unstyled in its name? This could be done here and in all the docs:

Suggested change
const StyledMenuItem = styled(MenuItemUnstyled)(
const MenuItem = styled(MenuItemUnstyled)(

The convention could be, anything with Unstyled needs to be styled to be used. The rest is ready to use, like Portal.

@michaldudak michaldudak closed this Feb 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: menu This is the name of the generic UI component, not the React module! package: base-ui Specific to @mui/base proof of concept Studying and/or experimenting with a to be validated approach
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants