diff --git a/docs/pages/api-docs/icon-button.json b/docs/pages/api-docs/icon-button.json index 1bc67c034b8109..8092039c153db0 100644 --- a/docs/pages/api-docs/icon-button.json +++ b/docs/pages/api-docs/icon-button.json @@ -22,7 +22,8 @@ "size": { "type": { "name": "enum", "description": "'medium'
| 'small'" }, "default": "'medium'" - } + }, + "sx": { "type": { "name": "object" } } }, "name": "IconButton", "styles": { @@ -45,6 +46,6 @@ "filename": "/packages/material-ui/src/IconButton/IconButton.js", "inheritance": { "component": "ButtonBase", "pathname": "/api/button-base/" }, "demos": "", - "styledComponent": false, + "styledComponent": true, "cssComponent": false } diff --git a/docs/translations/api-docs/icon-button/icon-button.json b/docs/translations/api-docs/icon-button/icon-button.json index c0f3dfce888fb8..55bf8da7da0fce 100644 --- a/docs/translations/api-docs/icon-button/icon-button.json +++ b/docs/translations/api-docs/icon-button/icon-button.json @@ -8,7 +8,8 @@ "disableFocusRipple": "If true, the keyboard focus ripple is disabled.", "disableRipple": "If true, the ripple effect is disabled.
⚠️ Without a ripple there is no styling for :focus-visible by default. Be sure to highlight the element by applying separate styles with the .Mui-focusedVisible class.", "edge": "If given, uses a negative margin to counteract the padding on one side (this is often helpful for aligning the left or right side of the icon with content above or below, without ruining the border size and shape).", - "size": "The size of the component. small is equivalent to the dense button styling." + "size": "The size of the component. small is equivalent to the dense button 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": { "description": "Styles applied to the root element." }, diff --git a/framer/scripts/framerConfig.js b/framer/scripts/framerConfig.js index 333d914aa40920..f1a893d0dda255 100644 --- a/framer/scripts/framerConfig.js +++ b/framer/scripts/framerConfig.js @@ -186,7 +186,7 @@ export const componentSettings = { template: 'icon.txt', }, IconButton: { - ignoredProps: ['children', 'edge', 'disableRipple', 'disableFocusRipple'], + ignoredProps: ['children', 'edge', 'disableRipple', 'disableFocusRipple', 'sx'], propValues: { icon: "'favorite'", iconTheme: 'Filled', diff --git a/packages/material-ui/src/IconButton/IconButton.d.ts b/packages/material-ui/src/IconButton/IconButton.d.ts index e3193b624dee27..081203a9aa0ae4 100644 --- a/packages/material-ui/src/IconButton/IconButton.d.ts +++ b/packages/material-ui/src/IconButton/IconButton.d.ts @@ -1,4 +1,6 @@ -import { PropTypes } from '..'; +import * as React from 'react'; +import { SxProps } from '@material-ui/system'; +import { PropTypes, Theme } from '..'; import { ExtendButtonBase, ExtendButtonBaseTypeMap } from '../ButtonBase'; import { OverrideProps } from '../OverridableComponent'; @@ -63,6 +65,10 @@ export type IconButtonTypeMap< * @default 'medium' */ size?: 'small' | 'medium'; + /** + * 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/IconButton/IconButton.js b/packages/material-ui/src/IconButton/IconButton.js index c4f9b1e3f85cd2..968ea6b95c8ed2 100644 --- a/packages/material-ui/src/IconButton/IconButton.js +++ b/packages/material-ui/src/IconButton/IconButton.js @@ -1,15 +1,54 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; -import { chainPropTypes } from '@material-ui/utils'; -import withStyles from '../styles/withStyles'; +import { chainPropTypes, deepmerge } from '@material-ui/utils'; +import { unstable_composeClasses as composeClasses } from '@material-ui/unstyled'; +import experimentalStyled from '../styles/experimentalStyled'; +import useThemeProps from '../styles/useThemeProps'; import { alpha } from '../styles/colorManipulator'; import ButtonBase from '../ButtonBase'; import capitalize from '../utils/capitalize'; +import iconButtonClasses, { getIconButtonUtilityClass } from './iconButtonClasses'; -export const styles = (theme) => ({ - /* Styles applied to the root element. */ - root: { +const overridesResolver = (props, styles) => { + const { styleProps } = props; + + return deepmerge(styles.root || {}, { + ...(styleProps.color !== 'default' && styles[`color${capitalize(styleProps.color)}`]), + ...(styleProps.edge && styles[`edge${capitalize(styleProps.edge)}`]), + ...styles[`size${capitalize(styleProps.size)}`], + [`& .${iconButtonClasses.label}`]: styles.label, + }); +}; + +const useUtilityClasses = (styleProps) => { + const { classes, disabled, color, edge, size } = styleProps; + + const slots = { + root: [ + 'root', + disabled && 'disabled', + color !== 'default' && `color${capitalize(color)}`, + edge && `edge${capitalize(edge)}`, + `size${capitalize(size)}`, + ], + label: ['label'], + }; + + return composeClasses(slots, getIconButtonUtilityClass, classes); +}; + +const IconButtonRoot = experimentalStyled( + ButtonBase, + {}, + { + name: 'MuiIconButton', + slot: 'Root', + overridesResolver, + }, +)( + ({ theme, styleProps }) => ({ + /* Styles applied to the root element. */ textAlign: 'center', flex: '0 0 auto', fontSize: theme.typography.pxToRem(24), @@ -27,76 +66,79 @@ export const styles = (theme) => ({ backgroundColor: 'transparent', }, }, - '&$disabled': { - backgroundColor: 'transparent', - color: theme.palette.action.disabled, - }, - }, - /* Styles applied to the root element if `edge="start"`. */ - edgeStart: { - marginLeft: -12, - '$sizeSmall&': { - marginLeft: -3, - }, - }, - /* Styles applied to the root element if `edge="end"`. */ - edgeEnd: { - marginRight: -12, - '$sizeSmall&': { - marginRight: -3, - }, - }, - /* Styles applied to the root element if `color="inherit"`. */ - colorInherit: { - color: 'inherit', - }, - /* Styles applied to the root element if `color="primary"`. */ - colorPrimary: { - color: theme.palette.primary.main, - '&:hover': { - backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.hoverOpacity), - // Reset on touch devices, it doesn't add specificity - '@media (hover: none)': { - backgroundColor: 'transparent', + /* Styles applied to the root element if `edge="start"`. */ + ...(styleProps.edge === 'start' && { + marginLeft: styleProps.size === 'small' ? -3 : -12, + }), + /* Styles applied to the root element if `edge="end"`. */ + ...(styleProps.edge === 'end' && { + marginRight: styleProps.size === 'small' ? -3 : -12, + }), + }), + ({ theme, styleProps }) => ({ + /* Styles applied to the root element if `color="inherit"`. */ + ...(styleProps.color === 'inherit' && { + color: 'inherit', + }), + /* Styles applied to the root element if `color="primary"`. */ + ...(styleProps.color === 'primary' && { + color: theme.palette.primary.main, + '&:hover': { + backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.hoverOpacity), + // Reset on touch devices, it doesn't add specificity + '@media (hover: none)': { + backgroundColor: 'transparent', + }, }, - }, - }, - /* Styles applied to the root element if `color="secondary"`. */ - colorSecondary: { - color: theme.palette.secondary.main, - '&:hover': { - backgroundColor: alpha(theme.palette.secondary.main, theme.palette.action.hoverOpacity), - // Reset on touch devices, it doesn't add specificity - '@media (hover: none)': { - backgroundColor: 'transparent', + }), + /* Styles applied to the root element if `color="secondary"`. */ + ...(styleProps.color === 'secondary' && { + color: theme.palette.secondary.main, + '&:hover': { + backgroundColor: alpha(theme.palette.secondary.main, theme.palette.action.hoverOpacity), + // Reset on touch devices, it doesn't add specificity + '@media (hover: none)': { + backgroundColor: 'transparent', + }, }, + }), + /* Styles applied to the root element if `size="small"`. */ + ...(styleProps.size === 'small' && { + padding: 3, + fontSize: theme.typography.pxToRem(18), + }), + /* Styles applied to the root element if `disabled={true}`. */ + [`&.${iconButtonClasses.disabled}`]: { + backgroundColor: 'transparent', + color: theme.palette.action.disabled, }, + }), +); + +const IconButtonLabel = experimentalStyled( + 'span', + {}, + { + name: 'MuiIconButton', + slot: 'Label', }, - /* Pseudo-class applied to the root element if `disabled={true}`. */ - disabled: {}, - /* Styles applied to the root element if `size="small"`. */ - sizeSmall: { - padding: 3, - fontSize: theme.typography.pxToRem(18), - }, +)({ /* Styles applied to the children container element. */ - label: { - width: '100%', - display: 'flex', - alignItems: 'inherit', - justifyContent: 'inherit', - }, + width: '100%', + display: 'flex', + alignItems: 'inherit', + justifyContent: 'inherit', }); /** * Refer to the [Icons](/components/icons/) section of the documentation * regarding the available icon options. */ -const IconButton = React.forwardRef(function IconButton(props, ref) { +const IconButton = React.forwardRef(function IconButton(inProps, ref) { + const props = useThemeProps({ props: inProps, name: 'MuiIconButton' }); const { edge = false, children, - classes, className, color = 'default', disabled = false, @@ -105,27 +147,31 @@ const IconButton = React.forwardRef(function IconButton(props, ref) { ...other } = props; + const styleProps = { + ...props, + edge, + color, + disabled, + disableFocusRipple, + size, + }; + + const classes = useUtilityClasses(styleProps); + return ( - - {children} - + + {children} + + ); }); @@ -199,6 +245,10 @@ IconButton.propTypes = { * @default 'medium' */ size: PropTypes.oneOf(['medium', 'small']), + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx: PropTypes.object, }; -export default withStyles(styles, { name: 'MuiIconButton' })(IconButton); +export default IconButton; diff --git a/packages/material-ui/src/IconButton/IconButton.test.js b/packages/material-ui/src/IconButton/IconButton.test.js index 6e99ca8ca7e9a2..5a744fcefc2b1b 100644 --- a/packages/material-ui/src/IconButton/IconButton.test.js +++ b/packages/material-ui/src/IconButton/IconButton.test.js @@ -1,26 +1,25 @@ import * as React from 'react'; import { expect } from 'chai'; import PropTypes from 'prop-types'; -import { getClasses, createMount, createClientRender, describeConformance } from 'test/utils'; +import { createMount, createClientRender, describeConformanceV5 } from 'test/utils'; import Icon from '../Icon'; import ButtonBase from '../ButtonBase'; import IconButton from './IconButton'; +import classes from './iconButtonClasses'; describe('', () => { - let classes; const mount = createMount(); const render = createClientRender({ strict: false }); - before(() => { - classes = getClasses(); - }); - - describeConformance(book, () => ({ + describeConformanceV5(book, () => ({ classes, inheritComponent: ButtonBase, mount, refInstanceof: window.HTMLButtonElement, - skip: ['componentProp'], + muiName: 'MuiIconButton', + testVariantProps: { edge: 'end', disabled: true }, + testDeepOverrides: { slotName: 'label', slotClassName: classes.label }, + skip: ['componentProp', 'componentsProp'], })); it('should render an inner label span (bloody safari)', () => { @@ -108,7 +107,7 @@ describe('', () => { it('should raise a warning about onClick in children because of Firefox', () => { expect(() => { PropTypes.checkPropTypes( - IconButton.Naked.propTypes, + IconButton.propTypes, { classes: {}, children: {}} /> }, 'prop', 'MockedName', diff --git a/packages/material-ui/src/IconButton/iconButtonClasses.d.ts b/packages/material-ui/src/IconButton/iconButtonClasses.d.ts new file mode 100644 index 00000000000000..6ef2e97a86af05 --- /dev/null +++ b/packages/material-ui/src/IconButton/iconButtonClasses.d.ts @@ -0,0 +1,18 @@ +export interface IconButtonClasses { + root: string; + disabled: string; + colorInherit: string; + colorPrimary: string; + colorSecondary: string; + edgeStart: string; + edgeEnd: string; + sizeSmall: string; + sizeMedium: string; + label: string; +} + +declare const iconButtonClasses: IconButtonClasses; + +export function getIconButtonUtilityClass(slot: string): string; + +export default iconButtonClasses; diff --git a/packages/material-ui/src/IconButton/iconButtonClasses.js b/packages/material-ui/src/IconButton/iconButtonClasses.js new file mode 100644 index 00000000000000..d60651f8984fc0 --- /dev/null +++ b/packages/material-ui/src/IconButton/iconButtonClasses.js @@ -0,0 +1,20 @@ +import { generateUtilityClass, generateUtilityClasses } from '@material-ui/unstyled'; + +export function getIconButtonUtilityClass(slot) { + return generateUtilityClass('MuiIconButton', slot); +} + +const iconButtonClasses = generateUtilityClasses('MuiIconButton', [ + 'root', + 'disabled', + 'colorInherit', + 'colorPrimary', + 'colorSecondary', + 'edgeStart', + 'edgeEnd', + 'sizeSmall', + 'sizeMedium', + 'label', +]); + +export default iconButtonClasses; diff --git a/packages/material-ui/src/IconButton/index.d.ts b/packages/material-ui/src/IconButton/index.d.ts index 655ec4c48891c5..aecf12553a0334 100644 --- a/packages/material-ui/src/IconButton/index.d.ts +++ b/packages/material-ui/src/IconButton/index.d.ts @@ -1,2 +1,5 @@ export { default } from './IconButton'; export * from './IconButton'; + +export { default as iconButtonClasses } from './iconButtonClasses'; +export * from './iconButtonClasses'; diff --git a/packages/material-ui/src/IconButton/index.js b/packages/material-ui/src/IconButton/index.js index 86c5e513af24dd..115058c9d6a9f6 100644 --- a/packages/material-ui/src/IconButton/index.js +++ b/packages/material-ui/src/IconButton/index.js @@ -1 +1,4 @@ export { default } from './IconButton'; + +export { default as iconButtonClasses } from './iconButtonClasses'; +export * from './iconButtonClasses';