-
-
Notifications
You must be signed in to change notification settings - Fork 32.4k
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
Conversation
|
return ( | ||
<MenuButtonContext.Provider value={contextValue}> | ||
<Root {...rootProps}>{label}</Root> | ||
{children} |
There was a problem hiding this comment.
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)?
{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' } }}
>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for using context
There was a problem hiding this 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?
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. |
There was a problem hiding this 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 👍
slots={{ root: MenuButton }} | ||
label="My account" |
There was a problem hiding this comment.
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:
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:
slots={{ root: MenuButton }} | |
label="My account" | |
trigger={<Button>My account</Button>} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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')( |
There was a problem hiding this comment.
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:
const MenuButton = styled('button')( | |
const Button = styled('button')( |
`, | ||
); | ||
|
||
const StyledMenuItem = styled(MenuItemUnstyled)( |
There was a problem hiding this comment.
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:
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
.
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.