diff --git a/docs/pages/api-docs/list-item.json b/docs/pages/api-docs/list-item.json
index 4bb999290fccca..b746301afb1ee7 100644
--- a/docs/pages/api-docs/list-item.json
+++ b/docs/pages/api-docs/list-item.json
@@ -18,7 +18,8 @@
"disabled": { "type": { "name": "bool" } },
"disableGutters": { "type": { "name": "bool" } },
"divider": { "type": { "name": "bool" } },
- "selected": { "type": { "name": "bool" } }
+ "selected": { "type": { "name": "bool" } },
+ "sx": { "type": { "name": "object" } }
},
"name": "ListItem",
"styles": {
@@ -47,6 +48,6 @@
"filename": "/packages/material-ui/src/ListItem/ListItem.js",
"inheritance": null,
"demos": "
",
- "styledComponent": false,
+ "styledComponent": true,
"cssComponent": false
}
diff --git a/docs/translations/api-docs/list-item/list-item.json b/docs/translations/api-docs/list-item/list-item.json
index 7211bf1a4a805f..4a2ac4b25b1390 100644
--- a/docs/translations/api-docs/list-item/list-item.json
+++ b/docs/translations/api-docs/list-item/list-item.json
@@ -13,7 +13,8 @@
"disabled": "If true
, the component is disabled.",
"disableGutters": "If true
, the left and right padding is removed.",
"divider": "If true
, a 1px light border is added to the bottom of the list item.",
- "selected": "Use to apply selected styling."
+ "selected": "Use to apply selected styling.",
+ "sx": "The system prop that allows defining system overrides as well as additional CSS styles. See the `sx` page for more details."
},
"classDescriptions": {
"root": {
diff --git a/framer/scripts/framerConfig.js b/framer/scripts/framerConfig.js
index 333d914aa40920..7633d5ec56a0e5 100644
--- a/framer/scripts/framerConfig.js
+++ b/framer/scripts/framerConfig.js
@@ -198,7 +198,7 @@ export const componentSettings = {
template: 'icon_button.txt',
},
ListItem: {
- ignoredProps: ['children', 'ContainerComponent', 'ContainerProps'],
+ ignoredProps: ['children', 'ContainerComponent', 'ContainerProps', 'sx'],
propValues: {
width: 568,
height: 48,
diff --git a/packages/material-ui/src/List/List.test.js b/packages/material-ui/src/List/List.test.js
index 8cc6b3a76ccf82..d551f15b7c0ea3 100644
--- a/packages/material-ui/src/List/List.test.js
+++ b/packages/material-ui/src/List/List.test.js
@@ -3,7 +3,7 @@ import { expect } from 'chai';
import { getClasses, createMount, describeConformance, createClientRender } from 'test/utils';
import ListSubheader from '../ListSubheader';
import List from './List';
-import ListItem from '../ListItem';
+import ListItem, { listItemClasses } from '../ListItem';
describe('
', () => {
const mount = createMount();
@@ -76,8 +76,6 @@ describe('
', () => {
,
);
- const listItemClasses = getClasses();
-
const liItems = container.querySelectorAll('li');
for (let i = 0; i < liItems.length; i += 1) {
expect(liItems[i]).to.have.class(listItemClasses.dense);
diff --git a/packages/material-ui/src/ListItem/ListItem.d.ts b/packages/material-ui/src/ListItem/ListItem.d.ts
index 34ab17dfd365ba..fe886efae8f46e 100644
--- a/packages/material-ui/src/ListItem/ListItem.d.ts
+++ b/packages/material-ui/src/ListItem/ListItem.d.ts
@@ -1,4 +1,6 @@
import * as React from 'react';
+import { SxProps } from '@material-ui/system';
+import { Theme } from '@material-ui/core/styles';
import { ExtendButtonBase } from '../ButtonBase';
import { OverridableComponent, OverrideProps } from '../OverridableComponent';
@@ -83,6 +85,10 @@ export interface ListItemTypeMap {
* @default false
*/
selected?: boolean;
+ /**
+ * The system prop that allows defining system overrides as well as additional CSS styles.
+ */
+ sx?: SxProps;
};
defaultComponent: D;
}
diff --git a/packages/material-ui/src/ListItem/ListItem.js b/packages/material-ui/src/ListItem/ListItem.js
index 14bd49e57e0859..41381159504e3b 100644
--- a/packages/material-ui/src/ListItem/ListItem.js
+++ b/packages/material-ui/src/ListItem/ListItem.js
@@ -1,73 +1,117 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
-import { chainPropTypes, elementTypeAcceptingRef } from '@material-ui/utils';
-import withStyles from '../styles/withStyles';
+import { unstable_composeClasses as composeClasses } from '@material-ui/unstyled';
+import { deepmerge, chainPropTypes, elementTypeAcceptingRef } from '@material-ui/utils';
+import experimentalStyled from '../styles/experimentalStyled';
+import useThemeProps from '../styles/useThemeProps';
import { alpha } from '../styles/colorManipulator';
import ButtonBase from '../ButtonBase';
import isMuiElement from '../utils/isMuiElement';
import useEnhancedEffect from '../utils/useEnhancedEffect';
import useForkRef from '../utils/useForkRef';
import ListContext from '../List/ListContext';
+import listItemClasses, { getListItemUtilityClass } from './listItemClasses';
-export const styles = (theme) => ({
- /* Styles applied to the (normally root) `component` element. May be wrapped by a `container`. */
- root: {
- display: 'flex',
- justifyContent: 'flex-start',
- alignItems: 'center',
- position: 'relative',
- textDecoration: 'none',
- width: '100%',
- boxSizing: 'border-box',
- textAlign: 'left',
- paddingTop: 8,
- paddingBottom: 8,
- '&$focusVisible': {
- backgroundColor: theme.palette.action.focus,
- },
- '&$selected': {
- backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity),
- '&$focusVisible': {
- backgroundColor: alpha(
- theme.palette.primary.main,
- theme.palette.action.selectedOpacity + theme.palette.action.focusOpacity,
- ),
- },
- },
- '&$disabled': {
- opacity: theme.palette.action.disabledOpacity,
+const overridesResolver = (props, styles) => {
+ const { styleProps } = props;
+
+ return deepmerge(styles.root || {}, {
+ ...(styleProps.dense && styles.dense),
+ ...(styleProps.alignItems === 'flex-start' && styles.alignItemsFlexStart),
+ ...(styleProps.divider && styles.divider),
+ ...(!styleProps.disableGutters && styles.gutters),
+ ...(styleProps.button && styles.button),
+ ...(styleProps.hasSecondaryAction && styles.secondaryAction),
+ });
+};
+
+const useUtilityClasses = (styleProps) => {
+ const {
+ dense,
+ alignItems,
+ divider,
+ disableGutters,
+ hasSecondaryAction,
+ selected,
+ disabled,
+ button,
+ classes,
+ } = styleProps;
+
+ const slots = {
+ root: [
+ 'root',
+ dense && 'dense',
+ !disableGutters && 'gutters',
+ divider && 'divider',
+ disabled && 'disabled',
+ button && 'button',
+ alignItems === 'flex-start' && 'alignItemsFlexStart',
+ hasSecondaryAction && 'secondaryAction',
+ selected && 'selected',
+ ],
+ container: ['container'],
+ };
+
+ return composeClasses(slots, getListItemUtilityClass, classes);
+};
+
+const ListItemRoot = experimentalStyled(
+ 'div',
+ {},
+ {
+ name: 'MuiListItem',
+ slot: 'Root',
+ overridesResolver,
+ },
+)(({ theme, styleProps }) => ({
+ display: 'flex',
+ justifyContent: 'flex-start',
+ alignItems: 'center',
+ position: 'relative',
+ textDecoration: 'none',
+ width: '100%',
+ boxSizing: 'border-box',
+ textAlign: 'left',
+ paddingTop: 8,
+ paddingBottom: 8,
+ [`&.${listItemClasses.focusVisible}`]: {
+ backgroundColor: theme.palette.action.focus,
+ },
+ [`&.${listItemClasses.selected}`]: {
+ backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity),
+ [`&.${listItemClasses.focusVisible}`]: {
+ backgroundColor: alpha(
+ theme.palette.primary.main,
+ theme.palette.action.selectedOpacity + theme.palette.action.focusOpacity,
+ ),
},
},
- /* Styles applied to the container element if `children` includes `ListItemSecondaryAction`. */
- container: {
- position: 'relative',
+ [`&.${listItemClasses.disabled}`]: {
+ opacity: theme.palette.action.disabledOpacity,
},
- /* Pseudo-class applied to the `component`'s `focusVisibleClassName` prop if `button={true}`. */
- focusVisible: {},
/* Styles applied to the component element if dense. */
- dense: {
+ ...(styleProps.dense && {
paddingTop: 4,
paddingBottom: 4,
- },
+ }),
/* Styles applied to the component element if `alignItems="flex-start"`. */
- alignItemsFlexStart: {
+ ...(styleProps.alignItems === 'flex-start' && {
alignItems: 'flex-start',
- },
- /* Pseudo-class applied to the inner `component` element if `disabled={true}`. */
- disabled: {},
+ }),
/* Styles applied to the inner `component` element if `divider={true}`. */
- divider: {
+ ...(styleProps.divider && {
borderBottom: `1px solid ${theme.palette.divider}`,
backgroundClip: 'padding-box',
- },
+ }),
/* Styles applied to the inner `component` element unless `disableGutters={true}`. */
- gutters: {
+ ...(!styleProps.disableGutters && {
paddingLeft: 16,
paddingRight: 16,
- },
+ }),
/* Styles applied to the inner `component` element if `button={true}`. */
- button: {
+ ...(styleProps.button && {
transition: theme.transitions.create('background-color', {
duration: theme.transitions.duration.shortest,
}),
@@ -79,7 +123,7 @@ export const styles = (theme) => ({
backgroundColor: 'transparent',
},
},
- '&$selected:hover': {
+ [`&.${listItemClasses.selected}:hover`]: {
backgroundColor: alpha(
theme.palette.primary.main,
theme.palette.action.selectedOpacity + theme.palette.action.hoverOpacity,
@@ -89,27 +133,37 @@ export const styles = (theme) => ({
backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity),
},
},
- },
+ }),
/* Styles applied to the component element if `children` includes `ListItemSecondaryAction`. */
- secondaryAction: {
+ ...(styleProps.hasSecondaryAction && {
// Add some space to avoid collision as `ListItemSecondaryAction`
// is absolutely positioned.
paddingRight: 48,
+ }),
+}));
+
+const ListItemContainer = experimentalStyled(
+ 'li',
+ {},
+ {
+ name: 'MuiListItem',
+ slot: 'Container',
+ overridesResolver,
},
- /* Pseudo-class applied to the root element if `selected={true}`. */
- selected: {},
+)({
+ position: 'relative',
});
/**
* Uses an additional container component if `ListItemSecondaryAction` is the last child.
*/
-const ListItem = React.forwardRef(function ListItem(props, ref) {
+const ListItem = React.forwardRef(function ListItem(inProps, ref) {
+ const props = useThemeProps({ props: inProps, name: 'MuiListItem' });
const {
alignItems = 'center',
autoFocus = false,
button = false,
children: childrenProp,
- classes,
className,
component: componentProp,
ContainerComponent = 'li',
@@ -147,23 +201,25 @@ const ListItem = React.forwardRef(function ListItem(props, ref) {
const hasSecondaryAction =
children.length && isMuiElement(children[children.length - 1], ['ListItemSecondaryAction']);
+ const styleProps = {
+ ...props,
+ alignItems,
+ autoFocus,
+ button,
+ dense: childContext.dense,
+ disabled,
+ disableGutters,
+ divider,
+ hasSecondaryAction,
+ selected,
+ };
+
+ const classes = useUtilityClasses(styleProps);
+
const handleRef = useForkRef(listItemRef, ref);
const componentProps = {
- className: clsx(
- classes.root,
- {
- [classes.dense]: childContext.dense,
- [classes.gutters]: !disableGutters,
- [classes.divider]: divider,
- [classes.disabled]: disabled,
- [classes.button]: button,
- [classes.alignItemsFlexStart]: alignItems === 'flex-start',
- [classes.secondaryAction]: hasSecondaryAction,
- [classes.selected]: selected,
- },
- className,
- ),
+ className: clsx(classes.root, className),
disabled,
...other,
};
@@ -172,7 +228,11 @@ const ListItem = React.forwardRef(function ListItem(props, ref) {
if (button) {
componentProps.component = componentProp || 'div';
- componentProps.focusVisibleClassName = clsx(classes.focusVisible, focusVisibleClassName);
+ componentProps.focusVisibleClassName = clsx(
+ listItemClasses.focusVisible,
+ focusVisibleClassName,
+ );
+
Component = ButtonBase;
}
@@ -191,23 +251,26 @@ const ListItem = React.forwardRef(function ListItem(props, ref) {
return (
-
- {children}
+
+ {children}
+
{children.pop()}
-
+
);
}
return (
-
+
{children}
-
+
);
});
@@ -315,6 +378,10 @@ ListItem.propTypes = {
* @default false
*/
selected: PropTypes.bool,
+ /**
+ * The system prop that allows defining system overrides as well as additional CSS styles.
+ */
+ sx: PropTypes.object,
};
-export default withStyles(styles, { name: 'MuiListItem' })(ListItem);
+export default ListItem;
diff --git a/packages/material-ui/src/ListItem/ListItem.test.js b/packages/material-ui/src/ListItem/ListItem.test.js
index 0df997ecac18d6..edfdd359d709ae 100644
--- a/packages/material-ui/src/ListItem/ListItem.test.js
+++ b/packages/material-ui/src/ListItem/ListItem.test.js
@@ -2,9 +2,8 @@ import * as React from 'react';
import { expect } from 'chai';
import PropTypes from 'prop-types';
import {
- getClasses,
createMount,
- describeConformance,
+ describeConformanceV5,
act,
createClientRender,
fireEvent,
@@ -14,6 +13,7 @@ import ListItemText from '../ListItemText';
import ListItemSecondaryAction from '../ListItemSecondaryAction';
import ListItem from './ListItem';
import ListContext from '../List/ListContext';
+import classes from './listItemClasses';
const NoContent = React.forwardRef(() => {
return null;
@@ -22,17 +22,15 @@ const NoContent = React.forwardRef(() => {
describe('', () => {
const mount = createMount({ strict: true });
const render = createClientRender();
- let classes;
- before(() => {
- classes = getClasses();
- });
-
- describeConformance(, () => ({
+ describeConformanceV5(, () => ({
classes,
inheritComponent: 'li',
mount,
refInstanceof: window.HTMLLIElement,
+ muiName: 'MuiListItem',
+ testVariantProps: { dense: true },
+ skip: ['componentsProp'],
}));
it('should render with gutters classes', () => {
@@ -165,7 +163,7 @@ describe('', () => {
it('warns if it cant detect the secondary action properly', () => {
expect(() => {
PropTypes.checkPropTypes(
- ListItem.Naked.propTypes,
+ ListItem.propTypes,
{
classes: {},
children: [
diff --git a/packages/material-ui/src/ListItem/index.d.ts b/packages/material-ui/src/ListItem/index.d.ts
index d0ef0af7f5d03a..09a67b8bf4f13b 100644
--- a/packages/material-ui/src/ListItem/index.d.ts
+++ b/packages/material-ui/src/ListItem/index.d.ts
@@ -1,2 +1,4 @@
export { default } from './ListItem';
export * from './ListItem';
+export { default as listItemClasses } from './listItemClasses';
+export * from './listItemClasses';
diff --git a/packages/material-ui/src/ListItem/index.js b/packages/material-ui/src/ListItem/index.js
index 741aed2700bd01..eef1aaeeefcaea 100644
--- a/packages/material-ui/src/ListItem/index.js
+++ b/packages/material-ui/src/ListItem/index.js
@@ -1 +1,3 @@
export { default } from './ListItem';
+export { default as listItemClasses } from './listItemClasses';
+export * from './listItemClasses';
diff --git a/packages/material-ui/src/ListItem/listItemClasses.d.ts b/packages/material-ui/src/ListItem/listItemClasses.d.ts
new file mode 100644
index 00000000000000..ab5799a790488c
--- /dev/null
+++ b/packages/material-ui/src/ListItem/listItemClasses.d.ts
@@ -0,0 +1,19 @@
+export interface ListItemClasses {
+ root: string;
+ container: string;
+ focusVisible: string;
+ dense: string;
+ alignItemsFlexStart: string;
+ disabled: string;
+ divider: string;
+ gutters: string;
+ button: string;
+ secondaryAction: string;
+ selected: string;
+}
+
+declare const listItemClasses: ListItemClasses;
+
+export function getListItemUtilityClass(slot: string): string;
+
+export default listItemClasses;
diff --git a/packages/material-ui/src/ListItem/listItemClasses.js b/packages/material-ui/src/ListItem/listItemClasses.js
new file mode 100644
index 00000000000000..6892c8f5b2b950
--- /dev/null
+++ b/packages/material-ui/src/ListItem/listItemClasses.js
@@ -0,0 +1,21 @@
+import { generateUtilityClass, generateUtilityClasses } from '@material-ui/unstyled';
+
+export function getListItemUtilityClass(slot) {
+ return generateUtilityClass('MuiListItem', slot);
+}
+
+const listItemClasses = generateUtilityClasses('MuiListItem', [
+ 'root',
+ 'container',
+ 'focusVisible',
+ 'dense',
+ 'alignItemsFlexStart',
+ 'disabled',
+ 'divider',
+ 'gutters',
+ 'button',
+ 'secondaryAction',
+ 'selected',
+]);
+
+export default listItemClasses;