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

[base] Refactor the compound components building blocks #36400

Merged
merged 51 commits into from
Apr 11, 2023

Conversation

michaldudak
Copy link
Member

@michaldudak michaldudak commented Mar 2, 2023

Breaking changes

Menu

  • MenuUnstyledContext is replaced by MenuProvider. The value to pass to the provider is returned by the useMenu hook.
  • MenuUnstyled's onClose prop is replaced by onOpenChange. It has the open parameter and is called when a menu is opened or closed

Select

  • SelectUnstyledContext is replaced by SelectProvider. The value to pass to the provider is returned by the useSelect hook.
  • SelectUnstyled's popup is permanently mounted.
  • The defaultOpen prop was added to the SelectUnstyled. The open/close state can now be controlled or uncontrolled, as a value.

Tabs

  • TabsContext is replaced by TabsProvider. The value to pass to the provider is returned by the useTabs hook.
  • To deselect all tabs, pass in null to Tabs' value prop, instead of false. This is consistent with how Select works.
  • The value prop is still technically not mandatory on TabUnstyled and TabPanel, but when omitted, the contents of the selected tab panel will not be rendered during SSR.

This started as a rewrite of Unstyled Tabs and related components to use context instead of cloneElement but turned into a significant refactoring of all the compound components: tabs, select, and menu.

I've identified two cross-cutting concerns that I extracted to their own hooks: navigation within the list (useListbox, likely needing a rename to useList or useListNavigation, and useListItem) and determining the children of a compound component (useCompoundParent and useCompoundItem).

Notable improvements

  • The tab can now access information about its order amongst other tabs.
  • It's possible to wrap tabs in custom components or fragments (tabs don't have to be direct children of a TabPanel)
  • Focus management and keyboard navigation logic are centralized and common to all list-like components.

Demos

To do

  • setting aria-controls on tabs
  • setting aria-labelledby on tab panels
  • renaming useListbox
  • changing the code of useSelect, useOption, use Menu, and useMenuItem to use the new hooks
  • general cleanup of code

Closes #35084
Closes #34393
Closes #33663
Closes #36799
Closes #35795

@michaldudak michaldudak added component: tabs This is the name of the generic UI component, not the React module! package: base-ui Specific to @mui/base enhancement This is not a bug, nor a new feature labels Mar 2, 2023
@mui-bot
Copy link

mui-bot commented Mar 2, 2023

Netlify deploy preview

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

@material-ui/unstyled: parsed: +1.45% , gzip: +0.39%
@mui/material-next: parsed: +8.27% , gzip: +8.31%
@mui/joy: parsed: +0.84% , gzip: +0.99%

Bundle size report

Details of bundle changes

Generated by 🚫 dangerJS against 6070647

@@ -235,5 +235,6 @@ export default function useButton(parameters: UseButtonParameters): UseButtonRet
setFocusVisible,
disabled,
active,
ref: handleRef,
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 realized that it was impossible to use two hooks (useListItem and useButton) on the same element, as the refs returned by the hooks' getRootProps would overwrite one another. Having a ref returned separately allows us to combine them.

@@ -244,19 +245,51 @@ function handleKeyDown<TOption>(
};

case 'ArrowUp':
// TODO: extend current selection with Shift modifier
if (orientation !== 'vertical') {
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 may also add vertical-reverse

Copy link
Member

Choose a reason for hiding this comment

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

I would to see a use-case for it :)

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

@mnajdova mnajdova left a comment

Choose a reason for hiding this comment

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

Thanks for the examples, they are very useful to reviewing the functionality. The new hooks make sense. I am not sure about the focusManagement being in the useListBox, I feel like it should be part of the hook/component using the useListBox but I understand there will be lots of repetitions this way, so let's go with it :)

packages/mui-base/src/TabUnstyled/TabUnstyled.types.ts Outdated Show resolved Hide resolved
@@ -17,11 +17,11 @@ export interface TabsUnstyledOwnProps {
* The value of the currently selected `Tab`.
* If you don't want any selected `Tab`, you can set this prop to `false`.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* If you don't want any selected `Tab`, you can set this prop to `false`.
* If you don't want any selected `Tab`, you can set this prop to `null`.

Is this correct with the current changes?

@@ -244,19 +245,51 @@ function handleKeyDown<TOption>(
};

case 'ArrowUp':
// TODO: extend current selection with Shift modifier
if (orientation !== 'vertical') {
Copy link
Member

Choose a reason for hiding this comment

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

I would to see a use-case for it :)

*/
tabsContextValue: TabsContextValue;
contextValue: TabsProviderValue;
Copy link
Member

Choose a reason for hiding this comment

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

👍

packages/mui-base/src/utils/useCompound.ts Show resolved Hide resolved
@michaldudak michaldudak force-pushed the tabs-with-context branch 3 times, most recently from 3467ae8 to f4df169 Compare March 30, 2023 11:54
@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged label Apr 7, 2023
@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged label Apr 11, 2023
Copy link
Member

@mnajdova mnajdova left a comment

Choose a reason for hiding this comment

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

Not related to this changes issues:

docs/pages/base/api/menu-unstyled.json Show resolved Hide resolved
@@ -1,6 +1,61 @@
{
"parameters": {},
"returnValue": {},
"returnValue": {
Copy link
Member

Choose a reason for hiding this comment

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

Finally, the return value is here :)

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'm curious why parameters aren't picked up, though. Let's investigate in another PR.

Copy link
Member

Choose a reason for hiding this comment

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

I'm curious why parameters aren't picked up, though. Let's investigate in another PR.

Addressed in #37034

"description": "false<br>&#124;&nbsp;number<br>&#124;&nbsp;string"
}
},
"defaultValue": { "type": { "name": "union", "description": "number<br>&#124;&nbsp;string" } },
Copy link
Member

Choose a reason for hiding this comment

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

Replacing false with null makes sense 👍

return [...selectedValues, item];
}

// Truncate the selection to the limit (discard items with lower indexes).
Copy link
Member

Choose a reason for hiding this comment

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

I see, but selectionMode: 'none' | 'one' | 'multiple' makes much more sense when reading the code, instead of figuring out that infinite is a valid number option for the multiple use-case. We can also name the options: selectionMode: 'none' | 'single' | 'multiple'

Copy link
Member

@mnajdova mnajdova left a comment

Choose a reason for hiding this comment

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

🚢 it

@michaldudak
Copy link
Member Author

Base UI: https://deploy-preview-36400--material-ui.netlify.app/base/react-menu/ when choosing menu item by clicking, we should not be seeing the focusable indicator on the trigger element

I've created a PR to address this: #36847

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking change component: menu This is the name of the generic UI component, not the React module! component: select This is the name of the generic UI component, not the React module! component: tabs This is the name of the generic UI component, not the React module! enhancement This is not a bug, nor a new feature package: base-ui Specific to @mui/base
Projects
Status: Done
5 participants