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;