diff --git a/docs/pages/api-docs/fab.json b/docs/pages/api-docs/fab.json index 67e5ff3272d189..c5928f084db175 100644 --- a/docs/pages/api-docs/fab.json +++ b/docs/pages/api-docs/fab.json @@ -21,6 +21,7 @@ }, "default": "'large'" }, + "sx": { "type": { "name": "object" } }, "variant": { "type": { "name": "union", @@ -52,6 +53,6 @@ "filename": "/packages/material-ui/src/Fab/Fab.js", "inheritance": { "component": "ButtonBase", "pathname": "/api/button-base/" }, "demos": "", - "styledComponent": false, + "styledComponent": true, "cssComponent": false } diff --git a/docs/translations/api-docs/fab/fab.json b/docs/translations/api-docs/fab/fab.json index 7216e76d8fa3fb..408c2621ecd582 100644 --- a/docs/translations/api-docs/fab/fab.json +++ b/docs/translations/api-docs/fab/fab.json @@ -10,6 +10,7 @@ "disableRipple": "If true, the ripple effect is disabled.", "href": "The URL to link to when the button is clicked. If defined, an a element will be used as the root node.", "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.", "variant": "The variant to use." }, "classDescriptions": { diff --git a/framer/scripts/framerConfig.js b/framer/scripts/framerConfig.js index d4ce7dc1b9748a..473f8f45ff1d71 100644 --- a/framer/scripts/framerConfig.js +++ b/framer/scripts/framerConfig.js @@ -165,6 +165,7 @@ export const componentSettings = { 'disableFocusRipple', // FIXME: `Union` 'variant', + 'sx', ], propValues: { icon: "'add'", diff --git a/packages/material-ui/src/Fab/Fab.d.ts b/packages/material-ui/src/Fab/Fab.d.ts index f60f5b979d05f8..8ee98a096b1f69 100644 --- a/packages/material-ui/src/Fab/Fab.d.ts +++ b/packages/material-ui/src/Fab/Fab.d.ts @@ -1,5 +1,6 @@ import { OverridableStringUnion } from '@material-ui/types'; -import { PropTypes } from '..'; +import { SxProps } from '@material-ui/system'; +import { PropTypes, Theme } from '..'; import { ExtendButtonBase, ExtendButtonBaseTypeMap } from '../ButtonBase'; import { OverrideProps } from '../OverridableComponent'; @@ -74,6 +75,10 @@ export type FabTypeMap

= ExtendB * @default 'circular' */ variant?: OverridableStringUnion; + /** + * 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/Fab/Fab.js b/packages/material-ui/src/Fab/Fab.js index 14e82ea90080f7..cc34a706bbd71a 100644 --- a/packages/material-ui/src/Fab/Fab.js +++ b/packages/material-ui/src/Fab/Fab.js @@ -1,14 +1,56 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; -import { useThemeVariants } from '@material-ui/styles'; -import withStyles from '../styles/withStyles'; +import { deepmerge } from '@material-ui/utils'; +import { unstable_composeClasses as composeClasses } from '@material-ui/unstyled'; import ButtonBase from '../ButtonBase'; import capitalize from '../utils/capitalize'; +import useThemeProps from '../styles/useThemeProps'; +import fabClasses, { getFabUtilityClass } from './fabClasses'; +import experimentalStyled from '../styles/experimentalStyled'; -export const styles = (theme) => ({ - /* Styles applied to the root element. */ - root: { +const overridesResolver = (props, styles) => { + const { styleProps } = props; + + return deepmerge(styles.root || {}, { + ...styles[styleProps.variant], + ...styles[`size${capitalize(styleProps.size)}`], + ...(styleProps.color === 'inherit' && styles.colorInherit), + ...(styleProps.color === 'primary' && styles.primary), + ...(styleProps.color === 'secondary' && styles.secondary), + [`& .${fabClasses.label}`]: styles.label, + }); +}; + +const useUtilityClasses = (styleProps) => { + const { color, variant, classes, size } = styleProps; + + const slots = { + root: [ + 'root', + variant, + `size${capitalize(size)}`, + color === 'inherit' && 'colorInherit', + color === 'primary' && 'primary', + color === 'secondary' && 'secondary', + ], + label: ['label'], + }; + + return composeClasses(slots, getFabUtilityClass, classes); +}; + +const FabRoot = experimentalStyled( + ButtonBase, + {}, + { + name: 'MuiFab', + slot: 'Root', + overridesResolver, + }, +)( + ({ theme, styleProps }) => ({ + /* Styles applied to the root element. */ ...theme.typography.button, minHeight: 36, transition: theme.transitions.create(['background-color', 'box-shadow', 'border-color'], { @@ -33,95 +75,101 @@ export const styles = (theme) => ({ }, textDecoration: 'none', }, - '&$focusVisible': { + '&.Mui-focusVisible': { boxShadow: theme.shadows[6], }, - '&$disabled': { + '&.Mui-disabled': { color: theme.palette.action.disabled, boxShadow: theme.shadows[0], backgroundColor: theme.palette.action.disabledBackground, }, - }, - /* Styles applied to the span element that wraps the children. */ - label: { - width: '100%', // assure the correct width for iOS Safari - display: 'inherit', - alignItems: 'inherit', - justifyContent: 'inherit', - }, - /* Styles applied to the root element if `color="primary"`. */ - primary: { - color: theme.palette.primary.contrastText, - backgroundColor: theme.palette.primary.main, - '&:hover': { - backgroundColor: theme.palette.primary.dark, - // Reset on touch devices, it doesn't add specificity - '@media (hover: none)': { - backgroundColor: theme.palette.primary.main, + /* Styles applied to the root element if `size="small"``. */ + ...(styleProps.size === 'small' && { + width: 40, + height: 40, + }), + /* Styles applied to the root element if `size="medium"``. */ + ...(styleProps.size === 'medium' && { + width: 48, + height: 48, + }), + /* Styles applied to the root element if `variant="extended"`. */ + ...(styleProps.variant === 'extended' && { + borderRadius: 48 / 2, + padding: '0 16px', + width: 'auto', + minHeight: 'auto', + minWidth: 48, + height: 48, + }), + ...(styleProps.variant === 'extended' && + styleProps.size === 'small' && { + width: 'auto', + padding: '0 8px', + borderRadius: 34 / 2, + minWidth: 34, + height: 34, + }), + ...(styleProps.variant === 'extended' && + styleProps.size === 'medium' && { + width: 'auto', + padding: '0 16px', + borderRadius: 40 / 2, + minWidth: 40, + height: 40, + }), + /* Styles applied to the root element if `color="inherit"`. */ + ...(styleProps.color === 'inherit' && { + color: 'inherit', + }), + }), + ({ theme, styleProps }) => ({ + /* Styles applied to the root element if `color="primary"`. */ + ...(styleProps.color === 'primary' && { + color: theme.palette.primary.contrastText, + backgroundColor: theme.palette.primary.main, + '&:hover': { + backgroundColor: theme.palette.primary.dark, + // Reset on touch devices, it doesn't add specificity + '@media (hover: none)': { + backgroundColor: theme.palette.primary.main, + }, }, - }, - }, - /* Styles applied to the root element if `color="secondary"`. */ - secondary: { - color: theme.palette.secondary.contrastText, - backgroundColor: theme.palette.secondary.main, - '&:hover': { - backgroundColor: theme.palette.secondary.dark, - // Reset on touch devices, it doesn't add specificity - '@media (hover: none)': { - backgroundColor: theme.palette.secondary.main, + }), + /* Styles applied to the root element if `color="secondary"`. */ + ...(styleProps.color === 'secondary' && { + color: theme.palette.secondary.contrastText, + backgroundColor: theme.palette.secondary.main, + '&:hover': { + backgroundColor: theme.palette.secondary.dark, + // Reset on touch devices, it doesn't add specificity + '@media (hover: none)': { + backgroundColor: theme.palette.secondary.main, + }, }, - }, - }, - /* Styles applied to the root element if `variant="extended"`. */ - extended: { - borderRadius: 48 / 2, - padding: '0 16px', - width: 'auto', - minHeight: 'auto', - minWidth: 48, - height: 48, - '&$sizeSmall': { - width: 'auto', - padding: '0 8px', - borderRadius: 34 / 2, - minWidth: 34, - height: 34, - }, - '&$sizeMedium': { - width: 'auto', - padding: '0 16px', - borderRadius: 40 / 2, - minWidth: 40, - height: 40, - }, - }, - /* Styles applied to the root element if `variant="circular"`. */ - circular: {}, - /* Pseudo-class applied to the ButtonBase root element if the button is keyboard focused. */ - focusVisible: {}, - /* Pseudo-class applied to the root element if `disabled={true}`. */ - disabled: {}, - /* Styles applied to the root element if `color="inherit"`. */ - colorInherit: { - color: 'inherit', - }, - /* Styles applied to the root element if `size="small"``. */ - sizeSmall: { - width: 40, - height: 40, - }, - /* Styles applied to the root element if `size="medium"``. */ - sizeMedium: { - width: 48, - height: 48, + }), + }), +); + +const FabLabel = experimentalStyled( + 'span', + {}, + { + name: 'MuiFab', + slot: 'Label', }, +)({ + /* Styles applied to the span element that wraps the children. */ + width: '100%', // assure the correct width for iOS Safari + display: 'inherit', + alignItems: 'inherit', + justifyContent: 'inherit', }); -const Fab = React.forwardRef(function Fab(props, ref) { +const Fab = React.forwardRef(function Fab(inProps, ref) { + const props = useThemeProps({ props: inProps, name: 'MuiFab' }); const { children, - classes, className, color = 'default', component = 'button', @@ -133,43 +181,33 @@ const Fab = React.forwardRef(function Fab(props, ref) { ...other } = props; - const themeVariantsClasses = useThemeVariants( - { - ...props, - color, - component, - disabled, - disableFocusRipple, - size, - variant, - }, - 'MuiFab', - ); + const styleProps = { + ...props, + color, + component, + disabled, + disableFocusRipple, + size, + variant, + }; + + const classes = useUtilityClasses(styleProps); return ( - - {children} - + + {children} + + ); }); @@ -229,6 +267,10 @@ Fab.propTypes = { * @default 'large' */ size: PropTypes.oneOf(['large', 'medium', 'small']), + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx: PropTypes.object, /** * The variant to use. * @default 'circular' @@ -239,4 +281,4 @@ Fab.propTypes = { ]), }; -export default withStyles(styles, { name: 'MuiFab' })(Fab); +export default Fab; diff --git a/packages/material-ui/src/Fab/Fab.test.js b/packages/material-ui/src/Fab/Fab.test.js index ed0a36bdd20bce..fa5e8a8969bada 100644 --- a/packages/material-ui/src/Fab/Fab.test.js +++ b/packages/material-ui/src/Fab/Fab.test.js @@ -1,8 +1,7 @@ import * as React from 'react'; import { expect } from 'chai'; import { - getClasses, - describeConformance, + describeConformanceV5, createClientRender, createMount, createServerRender, @@ -12,22 +11,22 @@ import { import Fab from './Fab'; import ButtonBase, { touchRippleClasses } from '../ButtonBase'; import Icon from '../Icon'; +import classes from './fabClasses'; describe('', () => { const mount = createMount(); const render = createClientRender({ strict: false }); - let classes; - before(() => { - classes = getClasses(Fab); - }); - - describeConformance(Conformance?, () => ({ + describeConformanceV5(Conformance?, () => ({ classes, inheritComponent: ButtonBase, mount, + muiName: 'MuiFab', + testVariantProps: { variant: 'extended' }, + testDeepOverrides: { slotName: 'label', slotClassName: classes.label }, + testStateOverrides: { prop: 'size', value: 'small', styleKey: 'sizeSmall' }, refInstanceof: window.HTMLButtonElement, - skip: ['componentProp'], + skip: ['componentsProp'], })); it('should render with the root class but no others', () => { diff --git a/packages/material-ui/src/Fab/fabClasses.d.ts b/packages/material-ui/src/Fab/fabClasses.d.ts new file mode 100644 index 00000000000000..07c7c2478f200e --- /dev/null +++ b/packages/material-ui/src/Fab/fabClasses.d.ts @@ -0,0 +1,20 @@ +export interface FabClasses { + root: string; + label: string; + primary: string; + secondary: string; + extended: string; + circular: string; + focusVisible: string; + disabled: string; + colorInherit: string; + sizeSmall: string; + sizeMedium: string; + sizeLarge: string; +} + +declare const fabClasses: FabClasses; + +export function getFabUtilityClass(slot: string): string; + +export default fabClasses; diff --git a/packages/material-ui/src/Fab/fabClasses.js b/packages/material-ui/src/Fab/fabClasses.js new file mode 100644 index 00000000000000..4e52490393d9e7 --- /dev/null +++ b/packages/material-ui/src/Fab/fabClasses.js @@ -0,0 +1,22 @@ +import { generateUtilityClass, generateUtilityClasses } from '@material-ui/unstyled'; + +export function getFabUtilityClass(slot) { + return generateUtilityClass('MuiFab', slot); +} + +const fabClasses = generateUtilityClasses('MuiFab', [ + 'root', + 'label', + 'primary', + 'secondary', + 'extended', + 'circular', + 'focusVisible', + 'disabled', + 'colorInherit', + 'sizeSmall', + 'sizeMedium', + 'sizeLarge', +]); + +export default fabClasses; diff --git a/packages/material-ui/src/Fab/index.d.ts b/packages/material-ui/src/Fab/index.d.ts index 898b9dbebe7c83..b1751bf8ec2032 100644 --- a/packages/material-ui/src/Fab/index.d.ts +++ b/packages/material-ui/src/Fab/index.d.ts @@ -1,2 +1,5 @@ export { default } from './Fab'; export * from './Fab'; + +export { default as fabClasses } from './fabClasses'; +export * from './fabClasses'; diff --git a/packages/material-ui/src/Fab/index.js b/packages/material-ui/src/Fab/index.js index e27f6caceabbcb..e5fd203ad858c0 100644 --- a/packages/material-ui/src/Fab/index.js +++ b/packages/material-ui/src/Fab/index.js @@ -1 +1,4 @@ export { default } from './Fab'; + +export { default as fabClasses } from './fabClasses'; +export * from './fabClasses'; diff --git a/packages/material-ui/src/SpeedDialAction/SpeedDialAction.test.js b/packages/material-ui/src/SpeedDialAction/SpeedDialAction.test.js index e2e760b3f31154..48f3c5bf253b82 100644 --- a/packages/material-ui/src/SpeedDialAction/SpeedDialAction.test.js +++ b/packages/material-ui/src/SpeedDialAction/SpeedDialAction.test.js @@ -11,7 +11,7 @@ import { import { useFakeTimers } from 'sinon'; import Icon from '@material-ui/core/Icon'; import Tooltip from '@material-ui/core/Tooltip'; -import Fab from '@material-ui/core/Fab'; +import { fabClasses } from '@material-ui/core/Fab'; import SpeedDialAction from './SpeedDialAction'; describe('', () => { @@ -27,11 +27,9 @@ describe('', () => { const mount = createMount({ strict: true }); const render = createClientRender(); let classes; - let fabClasses; before(() => { classes = getClasses(add} tooltipTitle="placeholder" />); - fabClasses = getClasses(Fab); }); describeConformance(