Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(material): add the
MenuItem
component
- Loading branch information
Showing
9 changed files
with
384 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@suid/material": minor | ||
--- | ||
|
||
Add the `MenuItem` component |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,255 @@ | ||
import { MenuItemTypeMap } from "."; | ||
import ButtonBase from "../ButtonBase"; | ||
import { dividerClasses } from "../Divider"; | ||
import ListContext from "../List/ListContext"; | ||
import { listItemIconClasses } from "../ListItemIcon"; | ||
import { listItemTextClasses } from "../ListItemText"; | ||
import styled, { skipRootProps } from "../styles/styled"; | ||
import { getMenuItemUtilityClass } from "./menuItemClasses"; | ||
import menuItemClasses from "./menuItemClasses"; | ||
import createComponentFactory from "@suid/base/createComponentFactory"; | ||
import { alpha } from "@suid/system"; | ||
import createRef from "@suid/system/createRef"; | ||
import { InPropsOf } from "@suid/types"; | ||
import clsx from "clsx"; | ||
import { | ||
useContext, | ||
splitProps, | ||
mergeProps, | ||
createRenderEffect, | ||
} from "solid-js"; | ||
|
||
const $ = createComponentFactory<MenuItemTypeMap>()({ | ||
name: "MuiMenuItem", | ||
selfPropNames: [ | ||
"autoFocus", | ||
"classes", | ||
"dense", | ||
"disabled", | ||
"disableGutters", | ||
"divider", | ||
"selected", | ||
], | ||
utilityClass: getMenuItemUtilityClass, | ||
slotClasses: (ownerState) => ({ | ||
root: [ | ||
"root", | ||
ownerState.dense && "dense", | ||
ownerState.disabled && "disabled", | ||
!ownerState.disableGutters && "gutters", | ||
ownerState.divider && "divider", | ||
ownerState.selected && "selected", | ||
], | ||
}), | ||
}); | ||
|
||
const MenuItemRoot = styled(ButtonBase, { | ||
skipProps: skipRootProps.filter((v) => v !== "classes"), | ||
name: "MuiMenuItem", | ||
slot: "Root", | ||
overridesResolver: (props, styles) => { | ||
const { ownerState } = props; | ||
|
||
return [ | ||
styles.root, | ||
ownerState.dense && styles.dense, | ||
ownerState.divider && styles.divider, | ||
!ownerState.disableGutters && styles.gutters, | ||
]; | ||
}, | ||
})< | ||
InPropsOf<MenuItemTypeMap> & { | ||
dense: boolean; | ||
divider: boolean; | ||
disableGutters: boolean; | ||
} | ||
>(({ theme, ownerState }) => ({ | ||
...theme.typography.body1, | ||
display: "flex", | ||
justifyContent: "flex-start", | ||
alignItems: "center", | ||
position: "relative", | ||
textDecoration: "none", | ||
minHeight: 48, | ||
paddingTop: 6, | ||
paddingBottom: 6, | ||
boxSizing: "border-box", | ||
whiteSpace: "nowrap", | ||
...(!ownerState.disableGutters && { | ||
paddingLeft: 16, | ||
paddingRight: 16, | ||
}), | ||
...(ownerState.divider && { | ||
borderBottom: `1px solid ${theme.palette.divider}`, | ||
backgroundClip: "padding-box", | ||
}), | ||
"&:hover": { | ||
textDecoration: "none", | ||
backgroundColor: theme.palette.action.hover, | ||
// Reset on touch devices, it doesn't add specificity | ||
"@media (hover: none)": { | ||
backgroundColor: "transparent", | ||
}, | ||
}, | ||
[`&.${menuItemClasses.selected}`]: { | ||
backgroundColor: alpha( | ||
theme.palette.primary.main, | ||
theme.palette.action.selectedOpacity | ||
), | ||
[`&.${menuItemClasses.focusVisible}`]: { | ||
backgroundColor: alpha( | ||
theme.palette.primary.main, | ||
theme.palette.action.selectedOpacity + theme.palette.action.focusOpacity | ||
), | ||
}, | ||
}, | ||
[`&.${menuItemClasses.selected}:hover`]: { | ||
backgroundColor: alpha( | ||
theme.palette.primary.main, | ||
theme.palette.action.selectedOpacity + theme.palette.action.hoverOpacity | ||
), | ||
// Reset on touch devices, it doesn't add specificity | ||
"@media (hover: none)": { | ||
backgroundColor: alpha( | ||
theme.palette.primary.main, | ||
theme.palette.action.selectedOpacity | ||
), | ||
}, | ||
}, | ||
[`&.${menuItemClasses.focusVisible}`]: { | ||
backgroundColor: theme.palette.action.focus, | ||
}, | ||
[`&.${menuItemClasses.disabled}`]: { | ||
opacity: theme.palette.action.disabledOpacity, | ||
}, | ||
[`& + .${dividerClasses.root}`]: { | ||
marginTop: theme.spacing(1), | ||
marginBottom: theme.spacing(1), | ||
}, | ||
[`& + .${dividerClasses.inset}`]: { | ||
marginLeft: 52, | ||
}, | ||
[`& .${listItemTextClasses.root}`]: { | ||
marginTop: 0, | ||
marginBottom: 0, | ||
}, | ||
[`& .${listItemTextClasses.inset}`]: { | ||
paddingLeft: 36, | ||
}, | ||
[`& .${listItemIconClasses.root}`]: { | ||
minWidth: 36, | ||
}, | ||
...(!ownerState.dense && { | ||
[theme.breakpoints.up("sm")]: { | ||
minHeight: "auto", | ||
}, | ||
}), | ||
...(ownerState.dense && { | ||
minHeight: 32, // https://material.io/components/menus#specs > Dense | ||
paddingTop: 4, | ||
paddingBottom: 4, | ||
...theme.typography.body2, | ||
[`& .${listItemIconClasses.root} svg`]: { | ||
fontSize: "1.25rem", | ||
}, | ||
}), | ||
})); | ||
|
||
/** | ||
* | ||
* Demos: | ||
* | ||
* - [Menus](https://mui.com/components/menus/) | ||
* | ||
* API: | ||
* | ||
* - [MenuItem API](https://mui.com/api/menu-item/) | ||
* - inherits [ButtonBase API](https://mui.com/api/button-base/) | ||
*/ | ||
const MenuItem = $.defineComponent(function MenuItem(inProps) { | ||
const menuItemRef = createRef<HTMLElement>(inProps); | ||
const props = $.useThemeProps({ props: inProps }); | ||
const [, other] = splitProps(props, [ | ||
"autoFocus", | ||
"component", | ||
"dense", | ||
"divider", | ||
"disableGutters", | ||
"focusVisibleClassName", | ||
"role", | ||
"tabIndex", | ||
]); | ||
|
||
const baseProps = mergeProps( | ||
{ | ||
autoFocus: false, | ||
component: "li", | ||
dense: false, | ||
divider: false, | ||
disableGutters: false, | ||
role: "menuitem", | ||
}, | ||
props | ||
); | ||
|
||
const context = useContext(ListContext); | ||
const childContext = { | ||
get dense() { | ||
return baseProps.dense || context.dense || false; | ||
}, | ||
get disableGutters() { | ||
return baseProps.disableGutters; | ||
}, | ||
}; | ||
|
||
createRenderEffect(() => { | ||
if (baseProps.autoFocus) { | ||
if (menuItemRef.current) { | ||
menuItemRef.current.focus(); | ||
} else if (process.env.NODE_ENV !== "production") { | ||
console.error( | ||
"MUI: Unable to set focus to a MenuItem whose component has not been rendered." | ||
); | ||
} | ||
} | ||
}, [baseProps.autoFocus]); | ||
|
||
const ownerState = mergeProps(props, { | ||
get dense() { | ||
return childContext.dense; | ||
}, | ||
get divider() { | ||
return baseProps.divider; | ||
}, | ||
get disableGutters() { | ||
return baseProps.disableGutters; | ||
}, | ||
}); | ||
|
||
const classes = $.useClasses(props); | ||
const tabIndex = () => { | ||
if (!props.disabled) { | ||
return props.tabIndex !== undefined ? props.tabIndex : -1; | ||
} | ||
}; | ||
|
||
return ( | ||
<ListContext.Provider value={childContext}> | ||
<MenuItemRoot | ||
ref={menuItemRef} | ||
role={baseProps.role} | ||
tabIndex={tabIndex()} | ||
component={baseProps.component} | ||
focusVisibleClassName={clsx( | ||
ownerState.classes?.focusVisible, | ||
props.focusVisibleClassName | ||
)} | ||
{...other} | ||
ownerState={ownerState} | ||
classes={classes} | ||
/> | ||
</ListContext.Provider> | ||
); | ||
}); | ||
|
||
export default MenuItem; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import { ExtendButtonBaseTypeMap } from "../ButtonBase"; | ||
import { Theme } from "../styles"; | ||
import { MenuItemClasses } from "./menuItemClasses"; | ||
import { SxProps } from "@suid/system"; | ||
import * as ST from "@suid/types"; | ||
|
||
export type MenuItemTypeMap<P = {}, D extends ST.ElementType = "li"> = { | ||
name: "MuiMenuItem"; | ||
defaultPropNames: | ||
| "autoFocus" | ||
| "dense" | ||
| "disabled" | ||
| "disableGutters" | ||
| "divider" | ||
| "selected"; | ||
selfProps: { | ||
/** | ||
* If `true`, the list item is focused during the first mount. | ||
* Focus will also be triggered if the value changes from false to true. | ||
* @default false | ||
*/ | ||
autoFocus?: boolean; | ||
|
||
/** | ||
* Override or extend the styles applied to the component. | ||
*/ | ||
classes?: Partial<MenuItemClasses>; | ||
|
||
/** | ||
* If `true`, compact vertical padding designed for keyboard and mouse input is used. | ||
* The prop defaults to the value inherited from the parent Menu component. | ||
* @default false | ||
*/ | ||
dense?: boolean; | ||
|
||
/** | ||
* If `true`, the component is disabled. | ||
* @default false | ||
*/ | ||
disabled?: boolean; | ||
|
||
/** | ||
* If `true`, the left and right padding is removed. | ||
* @default false | ||
*/ | ||
disableGutters?: boolean; | ||
|
||
/** | ||
* If `true`, a 1px light border is added to the bottom of the menu item. | ||
* @default false | ||
*/ | ||
divider?: boolean; | ||
|
||
/** | ||
* Use to apply selected styling. | ||
* @default false | ||
*/ | ||
selected?: boolean; | ||
|
||
/** | ||
* The system prop that allows defining system overrides as well as additional CSS styles. | ||
*/ | ||
sx?: SxProps<Theme>; | ||
}; | ||
} & ExtendButtonBaseTypeMap<{ | ||
props: P & MenuItemTypeMap["selfProps"]; | ||
defaultComponent: D; | ||
}>; | ||
|
||
export type MenuItemProps< | ||
D extends ST.ElementType = MenuItemTypeMap["defaultComponent"], | ||
P = {} | ||
> = ST.OverrideProps<MenuItemTypeMap<P, D>, D>; | ||
|
||
export default MenuItemProps; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export { default } from "./MenuItem"; | ||
export * from "./MenuItem"; | ||
|
||
export * from "./menuItemClasses"; | ||
export { default as menuItemClasses } from "./menuItemClasses"; | ||
|
||
export * from "./MenuItemProps"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import generateUtilityClass from "@suid/base/generateUtilityClass"; | ||
import generateUtilityClasses from "@suid/base/generateUtilityClasses"; | ||
|
||
export interface MenuItemClasses { | ||
/** Styles applied to the root element. */ | ||
root: string; | ||
/** State class applied to the root element if keyboard focused. */ | ||
focusVisible: string; | ||
/** Styles applied to the root element if dense. */ | ||
dense: string; | ||
/** State class applied to the root element if `disabled={true}`. */ | ||
disabled: string; | ||
/** Styles applied to the root element if `divider={true}`. */ | ||
divider: string; | ||
/** Styles applied to the inner `component` element unless `disableGutters={true}`. */ | ||
gutters: string; | ||
/** State class applied to the root element if `selected={true}`. */ | ||
selected: string; | ||
} | ||
|
||
export type MenuItemClassKey = keyof MenuItemClasses; | ||
|
||
export function getMenuItemUtilityClass(slot: string): string { | ||
return generateUtilityClass("MuiMenuItem", slot); | ||
} | ||
|
||
const menuItemClasses: MenuItemClasses = generateUtilityClasses("MuiMenuItem", [ | ||
"root", | ||
"focusVisible", | ||
"dense", | ||
"disabled", | ||
"divider", | ||
"gutters", | ||
"selected", | ||
]); | ||
|
||
export default menuItemClasses; |
Oops, something went wrong.