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

COM-310: Refactor theming of MenuItem #1597

Merged
Merged
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
5 changes: 5 additions & 0 deletions .changeset/two-lions-promise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@comet/admin": major
---

`MenuItem` no longer supports props from MUI's `ListItem` but those from `ListItemButton` instead
15 changes: 13 additions & 2 deletions packages/admin/admin/src/mui/menu/CollapsibleItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,23 @@ const ListItem = styled("div", {
`,
);

const Item = styled(MenuItem, {
name: "CometAdminMenuCollapsibleItem",
slot: "menuItem",
overridesResolver(_, styles) {
return [styles.menuItem];
},
})();

export interface MenuLevel {
level?: 1 | 2;
}

type MenuChild = React.ReactElement<MenuItemRouterLinkProps>;

export interface MenuCollapsibleItemProps extends ThemedComponentBaseProps<{ root: "div"; listItem: "div" }>, MenuItemProps {
export interface MenuCollapsibleItemProps
extends Omit<MenuItemProps, "slotProps">,
ThemedComponentBaseProps<{ root: "div"; listItem: "div"; menuItem: typeof MenuItem }> {
children: MenuChild | MenuChild[];
openByDefault?: boolean;
openedIcon?: React.ReactNode;
Expand Down Expand Up @@ -100,13 +110,14 @@ export function MenuCollapsibleItem(inProps: MenuCollapsibleItemProps) {
return (
<Root ownerState={ownerState} {...slotProps?.root} {...otherProps}>
<ListItem ownerState={ownerState} {...slotProps?.listItem}>
<MenuItem
<Item
primary={primary}
secondary={secondary}
icon={icon}
level={level}
onClick={() => setOpen(!open)}
secondaryAction={open ? openedIcon : closedIcon}
{...slotProps?.menuItem}
/>
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
Expand Down
259 changes: 146 additions & 113 deletions packages/admin/admin/src/mui/menu/Item.tsx
Original file line number Diff line number Diff line change
@@ -1,144 +1,177 @@
import { ComponentsOverrides, ListItem, ListItemIcon, ListItemProps, ListItemText, Theme } from "@mui/material";
import { createStyles, WithStyles, withStyles } from "@mui/styles";
import { ListItemButton, ListItemButtonProps, ListItemIcon, ListItemText } from "@mui/material";
import { ComponentsOverrides, css, styled, Theme, useThemeProps } from "@mui/material/styles";
import { ThemedComponentBaseProps } from "helpers/ThemedComponentBaseProps";
import * as React from "react";

import { MenuLevel } from "./CollapsibleItem";
import { MenuContext } from "./Context";

export type MenuItemClassKey = "root" | "level1" | "level2" | "hasIcon" | "hasSecondaryText" | "hasSecondaryAction";

type OwnerState = Pick<MenuItemProps, "level" | "icon" | "secondary" | "secondaryAction">;

const colors = {
textLevel1: "#242424",
textLevel2: "#17181A",
};

const styles = (theme: Theme) =>
createStyles<MenuItemClassKey, MenuItemProps & MuiListItemProps>({
root: {
flexShrink: 0,
"&:after": {
content: "''",
position: "absolute",
top: 0,
right: 0,
bottom: 0,
width: 2,
},
"& [class*='MuiListItemIcon-root']": {
color: colors.textLevel1,
minWidth: 28,
},
"& [class*='MuiListItemText-inset']": {
paddingLeft: 28,
},
"& [class*='MuiSvgIcon-root']": {
fontSize: 16,
},
},
level1: {
borderBottom: `1px solid ${theme.palette.grey[50]}`,
boxSizing: "border-box",
color: colors.textLevel1,
paddingLeft: 20,
paddingRight: 20,
paddingTop: 16,
paddingBottom: 16,
"&[class*='Mui-selected']": {
backgroundColor: theme.palette.grey[50],
color: theme.palette.primary.main,
"&:after": {
backgroundColor: theme.palette.primary.main,
},
"& [class*='MuiListItemIcon-root']": {
color: theme.palette.primary.main,
},
},
"& [class*='MuiListItemText-primary']": {
fontWeight: theme.typography.fontWeightMedium,
fontSize: 16,
lineHeight: "20px",
},
},
level2: {
color: colors.textLevel2,
paddingLeft: 20,
paddingRight: 20,
paddingTop: 10,
paddingBottom: 10,
"&:last-child": {
borderBottom: `1px solid ${theme.palette.grey[50]}`,
boxSizing: "border-box",
},
"&[class*='Mui-selected']": {
backgroundColor: theme.palette.primary.main,
color: "#fff",
"&:after": {
backgroundColor: theme.palette.primary.dark,
},
"&:hover": {
backgroundColor: theme.palette.primary.dark,
},
"& [class*='MuiListItemText-primary']": {
fontWeight: theme.typography.fontWeightBold,
},
},
"& [class*='MuiListItemText-root']": {
margin: 0,
},
"& [class*='MuiListItemText-primary']": {
fontWeight: theme.typography.fontWeightRegular,
fontSize: 14,
lineHeight: "20px",
},
},
hasIcon: {},
hasSecondaryText: {},
hasSecondaryAction: {
paddingRight: 18,
},
});
const Root = styled(ListItemButton, {
name: "CometAdminMenuItem",
slot: "root",
overridesResolver({ ownerState }: { ownerState: OwnerState }, styles) {
return [
styles.root,
ownerState.level === 1 && styles.level1,
ownerState.level === 2 && styles.level2,
ownerState.icon && styles.hasIcon,
ownerState.secondaryAction && styles.hasSecondaryAction,
ownerState.secondary && styles.hasSecondaryText,
];
},
})<{ ownerState: OwnerState }>(
({ theme, ownerState }) => css`
flex-shrink: 0;
flex-grow: 0;

&:after {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 2px;
}

.MuiListItemIcon-root {
color: ${colors.textLevel1};
min-width: 28px;
}

.MuiListItemText-inset {
padding-left: 28px;
}

.MuiSvgIcon-root {
font-size: 16px;
}

${ownerState.level === 1 &&
css`
border-bottom: 1px solid ${theme.palette.grey[50]};
box-sizing: border-box;
color: ${colors.textLevel1};
padding-left: 20px;
padding-right: 20px;
padding-top: 16px;
padding-bottom: 16px;

&.Mui-selected {
background-color: ${theme.palette.grey[50]};
color: ${theme.palette.primary.main};

&:after {
background-color: ${theme.palette.primary.main};
}
.MuiListItemIcon-root {
color: ${theme.palette.primary.main};
}
}

.MuiListItemText-primary {
font-weight: ${theme.typography.fontWeightMedium};
font-size: 16px;
line-height: 20px;
}
`}

${ownerState.level === 2 &&
css`
color: ${colors.textLevel2};
padding-left: 20px;
padding-right: 20px;
padding-top: 10px;
padding-bottom: 10px;

&:last-child {
border-bottom: 1px solid ${theme.palette.grey[50]};
box-sizing: border-box;
}

&.Mui-selected {
background-color: ${theme.palette.primary.main};
color: #fff;

&:after {
background-color: ${theme.palette.primary.dark};
}
&:hover {
background-color: ${theme.palette.primary.dark};
}
& .MuiListItemText-primary {
font-weight: ${theme.typography.fontWeightBold};
}
}

export interface MenuItemProps extends MenuLevel {
.MuiListItemText-root {
margin: 0;
}

.MuiListItemText-primary {
font-weight: ${theme.typography.fontWeightRegular};
font-size: 14px;
line-height: 20px;
}
`};

${ownerState.secondaryAction &&
css`
padding-right: 18px;
`}
`,
);

export interface MenuItemProps extends ThemedComponentBaseProps<{ root: typeof ListItemButton }>, MenuLevel, ListItemButtonProps {
primary: React.ReactNode;
secondary?: React.ReactNode;
icon?: React.ReactElement;
secondaryAction?: React.ReactNode;
}

type MuiListItemProps = Pick<ListItemProps, Exclude<keyof ListItemProps, "innerRef" | "button">> & { component?: React.ElementType };

const Item: React.FC<WithStyles<typeof styles> & MenuItemProps & MuiListItemProps> = ({
classes,
primary,
secondary,
icon,
level = 1,
secondaryAction,
...otherProps
}) => {
const context = React.useContext(MenuContext);
if (!context) throw new Error("Could not find context for menu");
if (level > 2) throw new Error("Maximum nesting level of 2 exceeded.");
export function MenuItem(inProps: MenuItemProps) {
const {
primary,
secondary,
icon,
level = 1,
secondaryAction,
slotProps,
...otherProps
} = useThemeProps({
props: inProps,
name: "CometAdminMenuItem",
});

const ownerState: OwnerState = {
level,
icon,
secondaryAction,
secondary,
};

const hasIcon = !!icon;

const listItemClasses = [classes.root];
if (level === 1) listItemClasses.push(classes.level1);
if (level === 2) listItemClasses.push(classes.level2);
if (hasIcon) listItemClasses.push(classes.hasIcon);
if (secondary) listItemClasses.push(classes.hasSecondaryText);
if (secondaryAction) listItemClasses.push(classes.hasSecondaryAction);
const context = React.useContext(MenuContext);
if (!context) throw new Error("Could not find context for menu");
if (level > 2) throw new Error("Maximum nesting level of 2 exceeded.");

return (
<ListItem component="div" button classes={{ root: listItemClasses.join(" ") }} {...otherProps}>
<Root {...slotProps?.root} ownerState={ownerState} {...otherProps}>
{hasIcon && <ListItemIcon>{icon}</ListItemIcon>}
<ListItemText primary={primary} secondary={secondary} inset={!icon} />
{!!secondaryAction && secondaryAction}
</ListItem>
</Root>
);
};

export const MenuItem = withStyles(styles, { name: "CometAdminMenuItem" })(Item);
}

declare module "@mui/material/styles" {
interface ComponentNameToClassKey {
Expand Down
5 changes: 3 additions & 2 deletions packages/admin/admin/src/mui/menu/ItemAnchorLink.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ListItemProps } from "@mui/material/ListItem";
import { ListItemButtonProps } from "@mui/material";
import * as React from "react";

import { MenuItem, MenuItemProps } from "./Item";

export type MenuItemAnchorLinkProps = MenuItemProps & ListItemProps & React.HTMLProps<HTMLAnchorElement>;
export type MenuItemAnchorLinkProps = MenuItemProps & ListItemButtonProps & React.HTMLProps<HTMLAnchorElement>;

// @ts-expect-error "component"-property is used as described in the documentation https://mui.com/material-ui/react-list/, but type is missing in ListItemButtonProps
export const MenuItemAnchorLink: React.FC<MenuItemAnchorLinkProps> = (props) => <MenuItem selected={false} component="a" {...props} />;
Loading