diff --git a/docs/pages/api-docs/tab.json b/docs/pages/api-docs/tab.json index b2d7a97088679d..70668576cd2a58 100644 --- a/docs/pages/api-docs/tab.json +++ b/docs/pages/api-docs/tab.json @@ -7,6 +7,7 @@ "disableRipple": { "type": { "name": "bool" } }, "icon": { "type": { "name": "union", "description": "element
| string" } }, "label": { "type": { "name": "node" } }, + "sx": { "type": { "name": "object" } }, "value": { "type": { "name": "any" } }, "wrapped": { "type": { "name": "bool" } } }, @@ -32,6 +33,6 @@ "filename": "/packages/material-ui/src/Tab/Tab.js", "inheritance": { "component": "ButtonBase", "pathname": "/api/button-base/" }, "demos": "", - "styledComponent": false, + "styledComponent": true, "cssComponent": false } diff --git a/docs/translations/api-docs/tab/tab.json b/docs/translations/api-docs/tab/tab.json index 7f420f0f1759d7..53eb736ef65917 100644 --- a/docs/translations/api-docs/tab/tab.json +++ b/docs/translations/api-docs/tab/tab.json @@ -8,6 +8,7 @@ "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.", "icon": "The icon to display.", "label": "The label element.", + "sx": "The system prop that allows defining system overrides as well as additional CSS styles. See the `sx` page for more details.", "value": "You can provide your own value. Otherwise, we fallback to the child position index.", "wrapped": "Tab labels appear in a single row. They can use a second line if needed." }, diff --git a/packages/material-ui/src/Tab/Tab.d.ts b/packages/material-ui/src/Tab/Tab.d.ts index a8300f1de085c3..e540237e6b1f73 100644 --- a/packages/material-ui/src/Tab/Tab.d.ts +++ b/packages/material-ui/src/Tab/Tab.d.ts @@ -1,4 +1,6 @@ +import { SxProps } from '@material-ui/system'; import * as React from 'react'; +import { Theme } from '..'; import { ExtendButtonBase, ExtendButtonBaseTypeMap } from '../ButtonBase'; import { OverrideProps } from '../OverridableComponent'; @@ -52,6 +54,10 @@ export type TabTypeMap

= ExtendButt * The label element. */ label?: React.ReactNode; + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx?: SxProps; /** * You can provide your own value. Otherwise, we fallback to the child position index. */ diff --git a/packages/material-ui/src/Tab/Tab.js b/packages/material-ui/src/Tab/Tab.js index f06956517e6b8a..7abf5edcccacef 100644 --- a/packages/material-ui/src/Tab/Tab.js +++ b/packages/material-ui/src/Tab/Tab.js @@ -1,96 +1,142 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; -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 experimentalStyled from '../styles/experimentalStyled'; import unsupportedProp from '../utils/unsupportedProp'; +import tabClasses, { getTabUtilityClass } from './tabClasses'; -export const styles = (theme) => ({ +const overridesResolver = (props, styles) => { + const { styleProps } = props; + + return deepmerge(styles.root || {}, { + ...(styleProps.label && styleProps.icon && styles.labelIcon), + ...styles[`textColor${capitalize(styleProps.textColor)}`], + ...(styleProps.fullWidth && styles.fullWidth), + ...(styleProps.wrapped && styles.wrapped), + [`& .${tabClasses.wrapper}`]: styles.wrapper, + }); +}; + +const useUtilityClasses = (styleProps) => { + const { classes, textColor, fullWidth, wrapped, icon, label, selected, disabled } = styleProps; + + const slots = { + root: [ + 'root', + icon && label && 'labelIcon', + `textColor${capitalize(textColor)}`, + fullWidth && 'fullWidth', + wrapped && 'wrapped', + selected && 'selected', + disabled && 'disabled', + ], + wrapper: ['wrapper'], + }; + + return composeClasses(slots, getTabUtilityClass, classes); +}; + +const TabRoot = experimentalStyled( + ButtonBase, + {}, + { + name: 'MuiTab', + slot: 'Root', + overridesResolver, + }, +)(({ theme, styleProps }) => ({ /* Styles applied to the root element. */ - root: { - ...theme.typography.button, - maxWidth: 264, - minWidth: 72, - position: 'relative', - minHeight: 48, - flexShrink: 0, - padding: '6px 12px', - overflow: 'hidden', - whiteSpace: 'normal', - textAlign: 'center', - [theme.breakpoints.up('sm')]: { - minWidth: 160, - }, + ...theme.typography.button, + maxWidth: 264, + minWidth: 72, + position: 'relative', + minHeight: 48, + flexShrink: 0, + padding: '6px 12px', + overflow: 'hidden', + whiteSpace: 'normal', + textAlign: 'center', + [theme.breakpoints.up('sm')]: { + minWidth: 160, }, /* Styles applied to the root element if both `icon` and `label` are provided. */ - labelIcon: { - minHeight: 72, - paddingTop: 9, - '& $wrapper > *:first-child': { - marginBottom: 6, - }, - }, + ...(styleProps.icon && + styleProps.label && { + minHeight: 72, + paddingTop: 9, + [`& .${tabClasses.wrapper} > *:first-child`]: { + marginBottom: 6, + }, + }), /* Styles applied to the root element if the parent [`Tabs`](/api/tabs/) has `textColor="inherit"`. */ - textColorInherit: { + ...(styleProps.textColor === 'inherit' && { color: 'inherit', opacity: 0.7, - '&$selected': { + '&.Mui-selected': { opacity: 1, }, - '&$disabled': { + '&.Mui-disabled': { opacity: theme.palette.action.disabledOpacity, }, - }, + }), /* Styles applied to the root element if the parent [`Tabs`](/api/tabs/) has `textColor="primary"`. */ - textColorPrimary: { + ...(styleProps.textColor === 'primary' && { color: theme.palette.text.secondary, - '&$selected': { + '&.Mui-selected': { color: theme.palette.primary.main, }, - '&$disabled': { + '&.Mui-disabled': { color: theme.palette.text.disabled, }, - }, + }), /* Styles applied to the root element if the parent [`Tabs`](/api/tabs/) has `textColor="secondary"`. */ - textColorSecondary: { + ...(styleProps.textColor === 'secondary' && { color: theme.palette.text.secondary, - '&$selected': { + '&.Mui-selected': { color: theme.palette.secondary.main, }, - '&$disabled': { + '&.Mui-disabled': { color: theme.palette.text.disabled, }, - }, - /* Pseudo-class applied to the root element if `selected={true}` (controlled by the Tabs component). */ - selected: {}, - /* Pseudo-class applied to the root element if `disabled={true}` (controlled by the Tabs component). */ - disabled: {}, - /* Styles applied to the root element if `fullWidth={true}` (controlled by the Tabs component). */ - fullWidth: { + }), + /* Styles applied to the root element if `fullWidth={true}` */ + ...(styleProps.fullWidth && { flexShrink: 1, flexGrow: 1, flexBasis: 0, maxWidth: 'none', - }, + }), /* Styles applied to the root element if `wrapped={true}`. */ - wrapped: { + ...(styleProps.wrapped && { fontSize: theme.typography.pxToRem(12), lineHeight: 1.5, + }), +})); + +const TabWrapper = experimentalStyled( + 'span', + {}, + { + name: 'MuiTab', + slot: 'Wrapper', }, +)({ /* Styles applied to the `icon` and `label`'s wrapper element. */ - wrapper: { - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - width: '100%', - flexDirection: 'column', - }, + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + width: '100%', + flexDirection: 'column', }); -const Tab = React.forwardRef(function Tab(props, ref) { +const Tab = React.forwardRef(function Tab(inProps, ref) { + const props = useThemeProps({ props: inProps, name: 'MuiTab' }); const { - classes, className, disabled = false, disableFocusRipple = false, @@ -114,6 +160,20 @@ const Tab = React.forwardRef(function Tab(props, ref) { ...other } = props; + const styleProps = { + ...props, + disabled, + disableFocusRipple, + selected, + icon: !!icon, + label: !!label, + fullWidth, + textColor, + wrapped, + }; + + const classes = useUtilityClasses(styleProps); + const handleClick = (event) => { if (!selected && onChange) { onChange(event, value); @@ -135,35 +195,25 @@ const Tab = React.forwardRef(function Tab(props, ref) { }; return ( - - + {icon} {label} - + {indicator} - + ); }); @@ -223,6 +273,10 @@ Tab.propTypes = { * @ignore */ onFocus: PropTypes.func, + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx: PropTypes.object, /** * You can provide your own value. Otherwise, we fallback to the child position index. */ @@ -235,4 +289,4 @@ Tab.propTypes = { wrapped: PropTypes.bool, }; -export default withStyles(styles, { name: 'MuiTab' })(Tab); +export default Tab; diff --git a/packages/material-ui/src/Tab/Tab.test.js b/packages/material-ui/src/Tab/Tab.test.js index f91db82edd35af..53d8eabd081ffe 100644 --- a/packages/material-ui/src/Tab/Tab.test.js +++ b/packages/material-ui/src/Tab/Tab.test.js @@ -1,31 +1,24 @@ import * as React from 'react'; import { expect } from 'chai'; import { spy } from 'sinon'; -import { - getClasses, - createMount, - describeConformance, - act, - createClientRender, - fireEvent, -} from 'test/utils'; +import { createMount, describeConformanceV5, act, createClientRender, fireEvent } from 'test/utils'; import Tab from './Tab'; import ButtonBase from '../ButtonBase'; +import classes from './tabClasses'; describe('', () => { const mount = createMount(); const render = createClientRender(); - let classes; - before(() => { - classes = getClasses(); - }); - - describeConformance(, () => ({ + describeConformanceV5(, () => ({ classes, inheritComponent: ButtonBase, mount, + muiName: 'MuiTab', + testDeepOverrides: { slotName: 'wrapper', slotClassName: classes.wrapper }, + testVariantProps: { variant: 'foo' }, refInstanceof: window.HTMLButtonElement, + skip: ['componentProp', 'componentsProp'], })); it('should have a ripple by default', () => { diff --git a/packages/material-ui/src/Tab/index.d.ts b/packages/material-ui/src/Tab/index.d.ts index 9d664d03f2e2d4..dae620b149a0af 100644 --- a/packages/material-ui/src/Tab/index.d.ts +++ b/packages/material-ui/src/Tab/index.d.ts @@ -1,2 +1,5 @@ export { default } from './Tab'; export * from './Tab'; + +export { default as tabClasses } from './tabClasses'; +export * from './tabClasses'; diff --git a/packages/material-ui/src/Tab/index.js b/packages/material-ui/src/Tab/index.js index dcfd2afd792227..7c828ba9dd2757 100644 --- a/packages/material-ui/src/Tab/index.js +++ b/packages/material-ui/src/Tab/index.js @@ -1 +1,4 @@ export { default } from './Tab'; + +export { default as tabClasses } from './tabClasses'; +export * from './tabClasses'; diff --git a/packages/material-ui/src/Tab/tabClasses.d.ts b/packages/material-ui/src/Tab/tabClasses.d.ts new file mode 100644 index 00000000000000..9389b19a2fdb6f --- /dev/null +++ b/packages/material-ui/src/Tab/tabClasses.d.ts @@ -0,0 +1,18 @@ +export interface TabClasses { + root: string; + labelIcon: string; + textColorInherit: string; + textColorPrimary: string; + textColorSecondary: string; + selected: string; + disabled: string; + fullWidth: string; + wrapped: string; + wrapper: string; +} + +declare const tabClasses: TabClasses; + +export function getTabUtilityClass(slot: string): string; + +export default tabClasses; diff --git a/packages/material-ui/src/Tab/tabClasses.js b/packages/material-ui/src/Tab/tabClasses.js new file mode 100644 index 00000000000000..598a9f783ea5ee --- /dev/null +++ b/packages/material-ui/src/Tab/tabClasses.js @@ -0,0 +1,20 @@ +import { generateUtilityClass, generateUtilityClasses } from '@material-ui/unstyled'; + +export function getTabUtilityClass(slot) { + return generateUtilityClass('MuiTab', slot); +} + +const tabClasses = generateUtilityClasses('MuiTab', [ + 'root', + 'labelIcon', + 'textColorInherit', + 'textColorPrimary', + 'textColorSecondary', + 'selected', + 'disabled', + 'fullWidth', + 'wrapped', + 'wrapper', +]); + +export default tabClasses;