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] Slots using render props #38362

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions docs/pages/experiments/base/render-props.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import * as React from 'react';
import { Menu } from '@mui/base/Menu';
import { MenuItem } from '@mui/base/MenuItem';
import { MenuButton, MenuButtonRootSlotProps } from '@mui/base/MenuButton';
import { Dropdown } from '@mui/base/Dropdown';
import IconButton from '@mui/joy/IconButton';
import MenuIcon from '@mui/icons-material/Menu';
import { CssVarsProvider } from '@mui/joy';

function WithRenderProp() {
return (
<Dropdown>
<MenuButton
renderRoot={(props) => (
<IconButton data-testid="hamburger-menu" {...props}>
<MenuIcon />
</IconButton>
)}
/>
<Menu>
<MenuItem>Profile</MenuItem>
<MenuItem>My account</MenuItem>
<MenuItem>Logout</MenuItem>
</Menu>
</Dropdown>
);
}

function WithSlotsAndSlotProps() {
return (
<Dropdown>
<MenuButton
slots={{
root: IconButton,
}}
slotProps={{
root: {
'data-testid': 'hamburger-menu',
} as any, // data attributes are not accepted by slotProps
}}
>
<MenuIcon />
</MenuButton>
<Menu>
<MenuItem>Profile</MenuItem>
<MenuItem>My account</MenuItem>
<MenuItem>Logout</MenuItem>
</Menu>
</Dropdown>
);
}

const MenuIconButton = React.forwardRef(function MenuIconButton(
props: MenuButtonRootSlotProps,
ref: React.ForwardedRef<HTMLButtonElement>,
) {
return (
<IconButton data-testid="hamburger-menu" {...props} ref={ref}>
{props.children}
</IconButton>
);
});

function WithSlots() {
return (
<Dropdown>
<MenuButton
slots={{
root: MenuIconButton,
}}
>
<MenuIcon />
</MenuButton>
<Menu>
<MenuItem>Profile</MenuItem>
<MenuItem>My account</MenuItem>
<MenuItem>Logout</MenuItem>
</Menu>
</Dropdown>
);
}

export default function RenderProps() {
return (
<CssVarsProvider>
<WithRenderProp />
<WithSlotsAndSlotProps />
<WithSlots />
Copy link
Member

Choose a reason for hiding this comment

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

I have accidentally noticed that the WithSlots demo doesn't behave like the other 2 examples:

Screen.Recording.2023-10-03.at.18.29.40.mov
Screen.Recording.2023-10-03.at.18.29.26.mov

The first 2 examples render the Menu in a popper, while the 3rd one doesn't.
triggerElement seems to be null for some reason:

if (open === true && triggerElement == null) {

@michaldudak any ideas?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch! The issue is that MenuIconButton does not forward the ref. I'll fix it.

Copy link
Member

Choose a reason for hiding this comment

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

Ahh, thanks for the explanation!

</CssVarsProvider>
);
}
17 changes: 13 additions & 4 deletions packages/mui-base/src/MenuButton/MenuButton.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
import { MenuButtonOwnerState, MenuButtonProps } from './MenuButton.types';
import { useSlotProps } from '../utils';
import { MenuButtonOwnerState, MenuButtonProps, MenuButtonRootSlotProps } from './MenuButton.types';
import { useSlotProps, WithOptionalOwnerState } from '../utils';
import { useMenuButton } from '../useMenuButton';
import { unstable_composeClasses as composeClasses } from '../composeClasses';
import { useClassNamesOverride } from '../utils/ClassNameConfigurator';
Expand Down Expand Up @@ -39,6 +39,7 @@ const MenuButton = React.forwardRef(function MenuButton(
slots = {},
slotProps = {},
focusableWhenDisabled = false,
renderRoot,
...other
} = props;

Expand All @@ -59,20 +60,28 @@ const MenuButton = React.forwardRef(function MenuButton(
const classes = useUtilityClasses(ownerState);

const Root = slots.root || 'button';
const rootProps = useSlotProps({
const rootProps: WithOptionalOwnerState<MenuButtonRootSlotProps> = useSlotProps({
elementType: Root,
getSlotProps: getRootProps,
externalForwardedProps: other,
externalSlotProps: slotProps.root,
additionalProps: {
ref: forwardedRef,
type: 'button',
children,
},
ownerState,
className: classes.root,
});

return <Root {...rootProps}>{children}</Root>;
// eslint-disable-next-line @typescript-eslint/naming-convention
const { ownerState: _, ...otherRootProps } = rootProps;

return renderRoot ? (
renderRoot(otherRootProps, ownerState)
) : (
<Root {...rootProps}>{children}</Root>
);
});

MenuButton.propTypes /* remove-proptypes */ = {
Expand Down
11 changes: 11 additions & 0 deletions packages/mui-base/src/MenuButton/MenuButton.types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { UseMenuButtonRootSlotProps } from '../useMenuButton';
import { SlotComponentProps } from '../utils/types';

export interface MenuButtonRootSlotPropsOverrides {}
Expand Down Expand Up @@ -35,6 +36,11 @@ export interface MenuButtonProps {
slotProps?: {
root?: SlotComponentProps<'button', MenuButtonRootSlotPropsOverrides, MenuButtonOwnerState>;
};

renderRoot?: (
props: Omit<MenuButtonRootSlotProps, 'ownerState'>,
ownerState: MenuButtonOwnerState,
) => React.JSX.Element;
}

export interface MenuButtonSlots {
Expand All @@ -50,3 +56,8 @@ export type MenuButtonOwnerState = MenuButtonProps & {
focusableWhenDisabled: boolean;
open: boolean;
};

export type MenuButtonRootSlotProps = UseMenuButtonRootSlotProps & {
ownerState: MenuButtonOwnerState;
children?: React.ReactNode;
};
3 changes: 2 additions & 1 deletion packages/mui-base/src/useMenuButton/useMenuButton.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export interface UseMenuButtonParameters {
rootRef?: React.Ref<HTMLElement>;
}

type UseMenuButtonRootSlotProps<ExternalProps = {}> = ExternalProps & UseMenuButtonRootSlotOwnProps;
export type UseMenuButtonRootSlotProps<ExternalProps = {}> = ExternalProps &
UseMenuButtonRootSlotOwnProps;

interface UseMenuButtonRootSlotOwnProps {
'aria-haspopup': 'menu';
Expand Down