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

[Menu][material-next] Use useMenu hook #38934

Merged
merged 36 commits into from
Oct 24, 2023

Conversation

mnajdova
Copy link
Member

@mnajdova mnajdova commented Sep 12, 2023

Part of #39521
Playground: https://deploy-preview-38934--material-ui.netlify.app/experiments/material-next/menu/

Things done in the PR:

  • converted to TypeScript
  • added usage of Base UI's useMenu, useDropdown & useMenuItem hooks
  • backported the old API that allows using the legacy API (Using the open prop instead of Dropdown context)

Things to be implemented (will require Base UI API changes, so I would leave them for a separate PR):

  • autoFocus disabling - I will try to find use-cases for this, I don't understand at this point what may be the use-case of having an opened menu with no item focused
  • variant - selectedMenu (in Base UI at this point, the menu items has no notion of selected, this will mean that we will need to change it to work similar as the select's option, I wonder if this is something that is worth implementing on Base UI's side, but it makes sense in my opinion... cc @michaldudak)

@mnajdova mnajdova added component: menu This is the name of the generic UI component, not the React module! proof of concept Studying and/or experimenting with a to be validated approach labels Sep 12, 2023
@mui-bot
Copy link

mui-bot commented Sep 12, 2023

Netlify deploy preview

https://deploy-preview-38934--material-ui.netlify.app/

@mui/material-next: parsed: +3.50% , gzip: +4.07%

Bundle size report

Details of bundle changes (Toolpad)
Details of bundle changes

Generated by 🚫 dangerJS against 33bd927

listboxRef: listboxRefProp,
onItemsChange,
id: idParam,
disabledItemsFocusable = true,
Copy link
Member Author

Choose a reason for hiding this comment

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

This option was needed so that we can keep a backward compatible API on Material UI, by default it works as before.

Copy link
Member Author

Choose a reason for hiding this comment

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

Same applies for the disableListWrap prop below.

onItemsChange,
id: idParam,
disabledItemsFocusable = true,
open: openParameter,
Copy link
Member Author

Choose a reason for hiding this comment

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

With this, I kind of made the hook to work without context too, I know we had a PR to make the prop required, but I don't see a better way in order to support both APIs. I am curious as to what are @michaldudak thoughts on this.

Copy link
Member

Choose a reason for hiding this comment

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

In #39284 I'm also adding the anchor prop to the menu back, so this may suggest that the solution we chose in #32088 wasn't the optimal one.

Anyway, now the question here also is if we're adding this prop back only to support the existing Material UI-style API, or is there a real need for it? If the former, maybe we shouldn't have spent so much time on the new implementation of Base UI, and should have just extracted the unstyled bits from the Material UI component without changing the API. I've always seen Base UI as a fresh start without the strong requirement to have a 100% compatible API with Material UI.


All these aside, I'm wondering if it would be possible to use both the useMenu and useDropdown in the Material Next's Menu (so that the open/close state is handled by the dropdown, while the list navigation is handled by the menu).

Copy link
Member Author

Choose a reason for hiding this comment

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

All these aside, I'm wondering if it would be possible to use both the useMenu and useDropdown in the Material Next's Menu (so that the open/close state is handled by the dropdown, while the list navigation is handled by the menu).

I don't think this is the optimal solution, especially considering that most of the UI libraries would like to offer both controlled and non-controlled open state for the component, which means all of them would need to use both hooks - if this can be replaced with just an additional argument in the useMenu hook, why wouldn't we do it.

Anyway, now the question here also is if we're adding this prop back only to support the existing Material UI-style API, or is there a real need for it?

In my opinion, this would benefit Joy UI too. Considering this, it's likely going to benefit other libraries that would depend on Base UI.

If the former, maybe we shouldn't have spent so much time on the new implementation of Base UI, and should have just extracted the unstyled bits from the Material UI component without changing the API. I've always seen Base UI as a fresh start without the strong requirement to have a 100% compatible API with Material UI.

Material UI is just one example of a UI library that already has an opinion on how things should work - this is why by using Base UI in it we discover things that can be improved. So far, Joy UI is adapting whatever Base UI has, which does not allow us to test a real-world use case most of the time. What I mean is, I don't think @siriwatknp would be opposed to these changes, but he won't necessarily push you to do them :D I am happy to try to refactor the component to use the useDropdown inside it too, but is that what we want to advertise users of the library to do? If yes, we should also add a demo about it on the Base UI's Menu page.

Copy link
Member

Choose a reason for hiding this comment

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

I agree that we don't know from real-world usage which pattern would be the best. But also, being "the best" could mean something else to different people, so this can be hard to measure. Surely, one option is to provide more ways of doing things, but this comes with a price - more APIs to support and possibly confused users not knowing which API to choose (and perhaps some even more confused, who mix different APIs together). So I'd be in favor of having one API and teaching developers how to use it rather than having many and allowing everyone to choose. This doesn't mean it must be the existing API with a separate Dropdown and Menu - maybe it's Material UI that got it right.

For now let's try using both useDropdown and useMenu in Material UI's Menu to get a feeling about the DX and we can discuss it afterward.

Copy link
Member Author

Choose a reason for hiding this comment

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

Here is the commit for using both hooks 1d6e378. The downside is that you need to create a wrapper component so that you can provide the dropdown context in the parent of the one that uses the useMenu hook.. Unless you had a different thought on how the two hooks can be combined. Seeing the changes in the Menu.js file, to me it is kind of ridiculous to ask people to do this, if we can easily support providing the open param. However, we can always add this as an additional API afterwards, so happy to try to see what will be the feedback.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for trying it out!

The downside is that you need to create a wrapper component so that you can provide the dropdown context in the parent of the one that uses the useMenu hook.

Yup, that's unfortunately necessary if you want to keep the existing API.

Seeing the changes in the Menu.js file, to me it is kind of ridiculous to ask people to do this, if we can easily support providing the open param

Let's consider we expose the open parameter and not require the Dropdown to be present. Wouldn't this require developers to handle things like restoring focus on the menu button when a menu is closed or setting up aria-controls and aria-labelledby attributes on their own?

Copy link
Member Author

Choose a reason for hiding this comment

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

Wouldn't this require developers to handle things like restoring focus on the menu button when a menu is closed or setting up aria-controls and aria-labelledby attributes on their own?

You are right, this would not be a regression for Material UI, as it worked like that before, but it may be surprising for other people trying the hook... We can try to go as is for now and consider adding the API later if we see more people wanting to use the menu as a standalone component - I can't think of valid use-cases apart from the selfish backporting API for Material UI :)

I will update the PR to make it closer to mergable state then without changing the Base UI API further then just adding the disabledItemsFocusable option that was missing.

});

// TODO: This test is failing
// it('should open during the initial mount', () => {
Copy link
Member Author

Choose a reason for hiding this comment

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

@michaldudak this test is failing for some reason. From what I could see, the difference is that this MenuItem is not using the useMenuItem hook. I am not sure if this is the cause for the fail tough.

Copy link
Member

Choose a reason for hiding this comment

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

At first glance it seems that the MenuItem doesn't get the autoFocus prop from anywhere.

@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged label Sep 28, 2023
@DiegoAndai
Copy link
Member

@mnajdova, would it be possible to open a separate PR to copy the Menu files to material-next and merge that first? That way, the diff will be much easier to review, especially with a complex refactor like this one.

@mnajdova
Copy link
Member Author

mnajdova commented Oct 4, 2023

@mnajdova, would it be possible to open a separate PR to copy the Menu files to material-next and merge that first? That way, the diff will be much easier to review, especially with a complex refactor like this one.

Right, I am doing it 👍 So simple, yet so smart strategy for uncovering the diffs :D

@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged label Oct 10, 2023
@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged label Oct 17, 2023
@mnajdova mnajdova marked this pull request as ready for review October 18, 2023 09:06
Copy link
Member

@DiegoAndai DiegoAndai left a comment

Choose a reason for hiding this comment

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

Hey! left some questions/comments 😊

packages/mui-base/src/useMenu/useMenu.ts Outdated Show resolved Hide resolved
Comment on lines 171 to 173
act(() => {
screen.getByRole('menu').focus();
});
Copy link
Member

Choose a reason for hiding this comment

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

Was this required because of autoFocus? Let's add a TODO to remove it if that's the case.

packages/mui-material-next/src/Menu/Menu.test.js Outdated Show resolved Hide resolved
packages/mui-material-next/src/Menu/Menu.test.js Outdated Show resolved Hide resolved
packages/mui-material-next/src/Menu/Menu.test.js Outdated Show resolved Hide resolved
packages/mui-material-next/src/MenuItem/MenuItem.js Outdated Show resolved Hide resolved
packages/mui-material-next/src/MenuItem/MenuItem.js Outdated Show resolved Hide resolved
packages/mui-material-next/src/MenuItem/MenuItem.js Outdated Show resolved Hide resolved

describe('<MenuItem />', () => {
const { render } = createRenderer();
const { render } = createRenderer({ clock: 'fake' });
Copy link
Member

Choose a reason for hiding this comment

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

Why this change?

Copy link
Member Author

Choose a reason for hiding this comment

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

Because some tests were failing. I believe it's again related to how Base UI' compound components work, but I may be wrong. I will dig deeper.

Btw, did you notice the same on the Select component? It's a bit strange why the component needs to render twice. Maybe it's because of the Dropdown context :\

Copy link
Member

Choose a reason for hiding this comment

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

Btw, did you notice the same on the Select component? It's a bit strange why the component needs to render twice. Maybe it's because of the Dropdown context :\

I haven't had to change to {clock: 'fake'}

I have noticed extra renderings but haven't dug deep into that yet. I will let you know if I figure something out.

@mnajdova
Copy link
Member Author

Thanks for the review @DiegoAndai. I will investigate in more detail why the rendering/focusing is different now. I assume it may be related to the Dropdown wrapping the Menu, or how I use the Base UI's hooks. I will request a new review once I have an explanation about this.

packages/mui-material-next/src/Menu/Menu.test.tsx Outdated Show resolved Hide resolved
packages/mui-material-next/src/Menu/Menu.test.tsx Outdated Show resolved Hide resolved
packages/mui-material-next/src/Menu/Menu.tsx Outdated Show resolved Hide resolved
packages/mui-material-next/src/Menu/Menu.tsx Outdated Show resolved Hide resolved
packages/mui-material-next/src/Menu/Menu.tsx Outdated Show resolved Hide resolved
packages/mui-material-next/src/MenuItem/MenuItem.test.tsx Outdated Show resolved Hide resolved
@mnajdova
Copy link
Member Author

@DiegoAndai I think we can resolve the focusing issue in the tests separately, I can see that in Base UI too the keyboard tests are focusing the first item always before testing some keyboard event. I've added this things in the issue #39521

Copy link
Member

@DiegoAndai DiegoAndai left a comment

Choose a reason for hiding this comment

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

LGTM 🚀

Great effort to maintain the legacy API 🙌🏼

@mnajdova mnajdova merged commit c82e4a8 into mui:master Oct 24, 2023
22 checks passed
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! 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.

None yet

4 participants