diff --git a/packages/mui-material-next/src/Button/Button.test.js b/packages/mui-material-next/src/Button/Button.test.js
index fc3b6389b2a16d..5c6954c1fed9ef 100644
--- a/packages/mui-material-next/src/Button/Button.test.js
+++ b/packages/mui-material-next/src/Button/Button.test.js
@@ -1,54 +1,41 @@
import * as React from 'react';
import { expect } from 'chai';
-import { describeConformance, createRenderer } from 'test/utils';
-import Button, { buttonClasses as classes } from '@mui/material/Button';
-import ButtonBase from '@mui/material/ButtonBase';
+import { describeConformance, createRenderer, fireEvent } from 'test/utils';
+import Button, { buttonClasses as classes } from '@mui/material-next/Button';
+import { CssVarsProvider, extendTheme } from '@mui/material-next/styles';
describe('', () => {
const { render, renderToString } = createRenderer();
describeConformance(, () => ({
classes,
- inheritComponent: ButtonBase,
render,
+ inheritComponent: 'button',
refInstanceof: window.HTMLButtonElement,
muiName: 'MuiButton',
testDeepOverrides: { slotName: 'startIcon', slotClassName: classes.startIcon },
testVariantProps: { variant: 'contained', fullWidth: true },
testStateOverrides: { prop: 'size', value: 'small', styleKey: 'sizeSmall' },
+ ThemeProvider: CssVarsProvider,
+ createTheme: extendTheme,
skip: ['componentsProp'],
}));
- it('should render with the root, text, and textPrimary classes but no others', () => {
+ it('should render with the root, text and colorPrimary classes but no others', () => {
const { getByRole } = render();
const button = getByRole('button');
expect(button).to.have.class(classes.root);
expect(button).to.have.class(classes.text);
- expect(button).to.have.class(classes.textPrimary);
- expect(button).not.to.have.class(classes.textSecondary);
+ expect(button).to.have.class(classes.colorPrimary);
+ expect(button).not.to.have.class(classes.filled);
+ expect(button).not.to.have.class(classes.filledTonal);
expect(button).not.to.have.class(classes.outlined);
- expect(button).not.to.have.class(classes.outlinedPrimary);
- expect(button).not.to.have.class(classes.outlinedSecondary);
- expect(button).not.to.have.class(classes.contained);
- expect(button).not.to.have.class(classes.containedPrimary);
- expect(button).not.to.have.class(classes.containedSecondary);
- expect(button).not.to.have.class(classes.textSizeSmall);
- expect(button).not.to.have.class(classes.textSizeLarge);
- expect(button).not.to.have.class(classes.outlinedSizeSmall);
- expect(button).not.to.have.class(classes.outlinedSizeLarge);
- expect(button).not.to.have.class(classes.containedSizeSmall);
- expect(button).not.to.have.class(classes.containedSizeLarge);
- });
-
- it('can render a text primary button', () => {
- const { getByRole } = render();
- const button = getByRole('button');
-
- expect(button).to.have.class(classes.root);
- expect(button).not.to.have.class(classes.contained);
- expect(button).to.have.class(classes.textPrimary);
- expect(button).not.to.have.class(classes.textSecondary);
+ expect(button).not.to.have.class(classes.elevated);
+ expect(button).not.to.have.class(classes.colorSecondary);
+ expect(button).not.to.have.class(classes.colorTertiary);
+ expect(button).not.to.have.class(classes.sizeSmall);
+ expect(button).not.to.have.class(classes.sizeLarge);
});
it('should render a text secondary button', () => {
@@ -56,208 +43,74 @@ describe('', () => {
const button = getByRole('button');
expect(button).to.have.class(classes.root);
- expect(button).not.to.have.class(classes.contained);
- expect(button).not.to.have.class(classes.textPrimary);
- expect(button).to.have.class(classes.textSecondary);
- });
-
- it('should render an outlined button', () => {
- const { getByRole } = render();
- const button = getByRole('button');
-
- expect(button).to.have.class(classes.root);
- expect(button).to.have.class(classes.outlined);
- expect(button).not.to.have.class(classes.contained);
- expect(button).not.to.have.class(classes.text);
- });
-
- it('should render a primary outlined button', () => {
- const { getByRole } = render(
- ,
- );
- const button = getByRole('button');
-
- expect(button).to.have.class(classes.root);
- expect(button).to.have.class(classes.outlined);
- expect(button).to.have.class(classes.outlinedPrimary);
- expect(button).not.to.have.class(classes.text);
- expect(button).not.to.have.class(classes.textPrimary);
- expect(button).not.to.have.class(classes.contained);
- });
-
- it('should render a secondary outlined button', () => {
- const { getByRole } = render(
- ,
- );
- const button = getByRole('button');
-
- expect(button).to.have.class(classes.root);
- expect(button).to.have.class(classes.outlined);
- expect(button).to.have.class(classes.outlinedSecondary);
- expect(button).not.to.have.class(classes.text);
- expect(button).not.to.have.class(classes.textSecondary);
- expect(button).not.to.have.class(classes.contained);
- });
-
- it('should render an inherit outlined button', () => {
- const { getByRole } = render(
- ,
- );
- const button = getByRole('button');
-
- expect(button).to.have.class(classes.root);
- expect(button).to.have.class(classes.outlined);
- expect(button).to.have.class(classes.colorInherit);
- expect(button).not.to.have.class(classes.text);
- expect(button).not.to.have.class(classes.textSecondary);
- expect(button).not.to.have.class(classes.contained);
- });
-
- it('should render a contained button', () => {
- const { getByRole } = render();
- const button = getByRole('button');
-
- expect(button).to.have.class(classes.root);
- expect(button).not.to.have.class(classes.text);
- expect(button).not.to.have.class(classes.textPrimary);
- expect(button).not.to.have.class(classes.textSecondary);
- expect(button).to.have.class(classes.contained);
- });
-
- it('should render a contained primary button', () => {
- const { getByRole } = render(
- ,
- );
- const button = getByRole('button');
-
- expect(button).to.have.class(classes.root);
- expect(button).not.to.have.class(classes.text);
- expect(button).to.have.class(classes.contained);
- expect(button).to.have.class(classes.containedPrimary);
- expect(button).not.to.have.class(classes.containedSecondary);
+ expect(button).to.have.class(classes.text);
+ expect(button).to.have.class(classes.colorSecondary);
+ expect(button).not.to.have.class(classes.colorPrimary);
});
- it('should render a contained secondary button', () => {
- const { getByRole } = render(
- ,
- );
- const button = getByRole('button');
+ ['filled', 'filledTonal', 'outlined', 'elevated'].forEach((variant) => {
+ it(`should render an ${variant} button`, () => {
+ const { getByRole } = render();
+ const button = getByRole('button');
- expect(button).to.have.class(classes.root);
- expect(button).not.to.have.class(classes.text);
- expect(button).to.have.class(classes.contained);
- expect(button).not.to.have.class(classes.containedPrimary);
- expect(button).to.have.class(classes.containedSecondary);
- });
+ expect(button).to.have.class(classes.root);
+ expect(button).to.have.class(classes[variant]);
+ expect(button).to.have.class(classes.colorPrimary);
+ expect(button).not.to.have.class(classes.text);
+ });
- it('should render a small text button', () => {
+ // these two variants do not support different colors
+ if (variant !== 'elevated' && variant !== 'filledTonal') {
+ it(`should render an ${variant} secondary button`, () => {
+ const { getByRole } = render(
+ ,
+ );
+ const button = getByRole('button');
+
+ expect(button).to.have.class(classes.root);
+ expect(button).to.have.class(classes[variant]);
+ expect(button).to.have.class(classes.colorSecondary);
+ expect(button).not.to.have.class(classes.text);
+ expect(button).not.to.have.class(classes.colorPrimary);
+ });
+
+ it(`should render an ${variant} tertiary button`, () => {
+ const { getByRole } = render(
+ ,
+ );
+ const button = getByRole('button');
+
+ expect(button).to.have.class(classes.root);
+ expect(button).to.have.class(classes[variant]);
+ expect(button).to.have.class(classes.colorTertiary);
+ expect(button).not.to.have.class(classes.text);
+ expect(button).not.to.have.class(classes.colorPrimary);
+ });
+ }
+ });
+
+ it('should render a small button', () => {
const { getByRole } = render();
const button = getByRole('button');
expect(button).to.have.class(classes.root);
- expect(button).to.have.class(classes.text);
- expect(button).to.have.class(classes.textSizeSmall);
- expect(button).not.to.have.class(classes.textSizeLarge);
- expect(button).not.to.have.class(classes.outlinedSizeSmall);
- expect(button).not.to.have.class(classes.outlinedSizeLarge);
- expect(button).not.to.have.class(classes.containedSizeSmall);
- expect(button).not.to.have.class(classes.containedSizeLarge);
+ expect(button).to.have.class(classes.sizeSmall);
+ expect(button).not.to.have.class(classes.sizeMedium);
+ expect(button).not.to.have.class(classes.sizeLarge);
});
- it('should render a large text button', () => {
+ it('should render a large button', () => {
const { getByRole } = render();
const button = getByRole('button');
expect(button).to.have.class(classes.root);
- expect(button).to.have.class(classes.text);
- expect(button).not.to.have.class(classes.textSizeSmall);
- expect(button).to.have.class(classes.textSizeLarge);
- expect(button).not.to.have.class(classes.outlinedSizeSmall);
- expect(button).not.to.have.class(classes.outlinedSizeLarge);
- expect(button).not.to.have.class(classes.containedSizeSmall);
- expect(button).not.to.have.class(classes.containedSizeLarge);
- });
-
- it('should render a small outlined button', () => {
- const { getByRole } = render(
- ,
- );
- const button = getByRole('button');
-
- expect(button).to.have.class(classes.root);
- expect(button).to.have.class(classes.outlined);
- expect(button).not.to.have.class(classes.textSizeSmall);
- expect(button).not.to.have.class(classes.textSizeLarge);
- expect(button).to.have.class(classes.outlinedSizeSmall);
- expect(button).not.to.have.class(classes.outlinedSizeLarge);
- expect(button).not.to.have.class(classes.containedSizeSmall);
- expect(button).not.to.have.class(classes.containedSizeLarge);
- });
-
- it('should render a large outlined button', () => {
- const { getByRole } = render(
- ,
- );
- const button = getByRole('button');
-
- expect(button).to.have.class(classes.root);
- expect(button).to.have.class(classes.outlined);
- expect(button).not.to.have.class(classes.textSizeSmall);
- expect(button).not.to.have.class(classes.textSizeLarge);
- expect(button).not.to.have.class(classes.outlinedSizeSmall);
- expect(button).to.have.class(classes.outlinedSizeLarge);
- expect(button).not.to.have.class(classes.containedSizeSmall);
- expect(button).not.to.have.class(classes.containedSizeLarge);
- });
-
- it('should render a small contained button', () => {
- const { getByRole } = render(
- ,
- );
- const button = getByRole('button');
-
- expect(button).to.have.class(classes.root);
- expect(button).to.have.class(classes.contained);
- expect(button).not.to.have.class(classes.textSizeSmall);
- expect(button).not.to.have.class(classes.textSizeLarge);
- expect(button).not.to.have.class(classes.outlinedSizeSmall);
- expect(button).not.to.have.class(classes.outlinedSizeLarge);
- expect(button).to.have.class(classes.containedSizeSmall);
- expect(button).not.to.have.class(classes.containedSizeLarge);
- });
-
- it('should render a large contained button', () => {
- const { getByRole } = render(
- ,
- );
- const button = getByRole('button');
-
- expect(button).to.have.class(classes.root);
- expect(button).to.have.class(classes.contained);
- expect(button).not.to.have.class(classes.textSizeSmall);
- expect(button).not.to.have.class(classes.textSizeLarge);
- expect(button).not.to.have.class(classes.outlinedSizeSmall);
- expect(button).not.to.have.class(classes.outlinedSizeLarge);
- expect(button).not.to.have.class(classes.containedSizeSmall);
- expect(button).to.have.class(classes.containedSizeLarge);
+ expect(button).to.have.class(classes.sizeLarge);
+ expect(button).not.to.have.class(classes.sizeMedium);
+ expect(button).not.to.have.class(classes.sizeSmall);
});
it('should render a button with startIcon', () => {
@@ -317,4 +170,13 @@ describe('', () => {
expect(container.querySelector('button')).to.have.class(disabledClassName);
});
+
+ it('should render active class', () => {
+ const { container } = render();
+
+ const button = container.querySelector('button');
+ fireEvent.mouseDown(button);
+
+ expect(button).to.have.class(classes.active);
+ });
});
diff --git a/packages/mui-material-next/src/Button/Button.tsx b/packages/mui-material-next/src/Button/Button.tsx
index 938c708524f6c8..7354643003fdef 100644
--- a/packages/mui-material-next/src/Button/Button.tsx
+++ b/packages/mui-material-next/src/Button/Button.tsx
@@ -23,6 +23,7 @@ const useUtilityClasses = (styleProps: ButtonOwnerState) => {
classes,
color,
disabled,
+ active,
disableElevation,
focusVisible,
focusVisibleClassName,
@@ -36,6 +37,7 @@ const useUtilityClasses = (styleProps: ButtonOwnerState) => {
'root',
disabled && 'disabled',
focusVisible && 'focusVisible',
+ active && 'active',
variant,
`color${capitalize(color ?? '')}`,
`size${capitalize(size ?? '')}`,
@@ -348,7 +350,7 @@ export const ButtonRoot = styled('button', {
backgroundColor: hoveredContainerColor[ownerState.variant ?? 'text'],
boxShadow: hoveredContainerElevation[ownerState.variant ?? 'text'],
},
- '&:active': {
+ [`&.${buttonClasses.active}`]: {
'--md-comp-button-icon-color': 'var(--md-comp-button-pressed-icon-color)',
...((ownerState.disableRipple || ownerState.disableTouchRipple) && {
backgroundColor: pressedContainerColor[ownerState.variant ?? 'text'],
@@ -416,6 +418,7 @@ const Button = React.forwardRef(function Button<
centerRipple = false,
children,
className,
+ classes: classesProp,
color = 'primary',
component = 'button',
disabled = false,
@@ -460,7 +463,7 @@ const Button = React.forwardRef(function Button<
ComponentProp = LinkComponent;
}
- const { focusVisible, setFocusVisible, getRootProps } = useButton({
+ const { focusVisible, active, setFocusVisible, getRootProps } = useButton({
disabled,
focusableWhenDisabled,
href: props.href,
@@ -506,10 +509,12 @@ const Button = React.forwardRef(function Button<
const ownerState = {
...props,
+ classes: classesProp,
color,
component,
disabled,
disableElevation,
+ active,
focusVisible,
fullWidth,
size,
@@ -571,6 +576,10 @@ Button.propTypes /* remove-proptypes */ = {
* The content of the component.
*/
children: PropTypes.node,
+ /**
+ * Override or extend the styles applied to the component.
+ */
+ classes: PropTypes.object,
/**
* @ignore
*/
diff --git a/packages/mui-material-next/src/Button/Button.types.ts b/packages/mui-material-next/src/Button/Button.types.ts
index 4ba938aba3319b..6afbae2d4452fc 100644
--- a/packages/mui-material-next/src/Button/Button.types.ts
+++ b/packages/mui-material-next/src/Button/Button.types.ts
@@ -139,6 +139,10 @@ export interface ButtonOwnerState extends ButtonProps {
* If `true`, the button's focus is visible.
*/
focusVisible?: boolean;
+ /**
+ * If `true`, the button is active.
+ */
+ active?: boolean;
}
/**
diff --git a/packages/mui-material-next/src/Button/buttonClasses.ts b/packages/mui-material-next/src/Button/buttonClasses.ts
index 6c5ecf49c3b724..46aee0f25c386b 100644
--- a/packages/mui-material-next/src/Button/buttonClasses.ts
+++ b/packages/mui-material-next/src/Button/buttonClasses.ts
@@ -20,6 +20,8 @@ export interface ButtonClasses {
focusVisible: string;
/** State class applied to the root element if `disabled={true}`. */
disabled: string;
+ /** State class applied to the root element if the element is active. */
+ active: string;
/** Styles applied to the root element if `color="primary"`. */
colorPrimary: string;
/** Styles applied to the root element if `color="secondary"`. */
@@ -65,6 +67,7 @@ const buttonClasses: ButtonClasses = generateUtilityClasses('MuiButton', [
'disableElevation',
'focusVisible',
'disabled',
+ 'active',
'colorInherit',
'sizeSmall',
'sizeMedium',
diff --git a/test/utils/describeConformance.tsx b/test/utils/describeConformance.tsx
index 3b95ef49db4ab9..10ce83e046653d 100644
--- a/test/utils/describeConformance.tsx
+++ b/test/utils/describeConformance.tsx
@@ -2,7 +2,10 @@
import * as React from 'react';
import { expect } from 'chai';
import { ReactWrapper } from 'enzyme';
-import { ThemeProvider as MDThemeProvider, createTheme } from '@mui/material/styles';
+import {
+ ThemeProvider as MDThemeProvider,
+ createTheme as mdCreateTheme,
+} from '@mui/material/styles';
import { unstable_capitalize as capitalize } from '@mui/utils';
import ReactTestRenderer from 'react-test-renderer';
import createMount from './createMount';
@@ -44,6 +47,7 @@ export interface InputConformanceOptions {
) => (node: React.ReactNode) => ReactWrapper;
slots?: Record;
ThemeProvider?: React.ElementType;
+ createTheme?: (arg: any) => any;
}
export interface ConformanceOptions extends InputConformanceOptions {
@@ -552,7 +556,12 @@ function testThemeDefaultProps(element: React.ReactElement, getOptions: () => Co
describe('theme default components:', () => {
it("respect theme's defaultProps", () => {
const testProp = 'data-id';
- const { muiName, render, ThemeProvider = MDThemeProvider } = getOptions();
+ const {
+ muiName,
+ render,
+ ThemeProvider = MDThemeProvider,
+ createTheme = mdCreateTheme,
+ } = getOptions();
if (!muiName) {
throwMissingPropError('muiName');
@@ -592,7 +601,13 @@ function testThemeStyleOverrides(
if (/jsdom/.test(window.navigator.userAgent)) {
this.skip();
}
- const { muiName, testStateOverrides, render, ThemeProvider = MDThemeProvider } = getOptions();
+ const {
+ muiName,
+ testStateOverrides,
+ render,
+ ThemeProvider = MDThemeProvider,
+ createTheme = mdCreateTheme,
+ } = getOptions();
if (!testStateOverrides) {
return;
@@ -646,6 +661,7 @@ function testThemeStyleOverrides(
testRootOverrides = { slotName: 'root' },
render,
ThemeProvider = MDThemeProvider,
+ createTheme = mdCreateTheme,
} = getOptions();
const testStyle = {
@@ -748,6 +764,7 @@ function testThemeStyleOverrides(
testStateOverrides,
render,
ThemeProvider = MDThemeProvider,
+ createTheme = mdCreateTheme,
} = getOptions();
const classKeys = Object.keys(classes);
@@ -817,7 +834,13 @@ function testThemeVariants(element: React.ReactElement, getOptions: () => Confor
this.skip();
}
- const { muiName, testVariantProps, render, ThemeProvider = MDThemeProvider } = getOptions();
+ const {
+ muiName,
+ testVariantProps,
+ render,
+ ThemeProvider = MDThemeProvider,
+ createTheme = mdCreateTheme,
+ } = getOptions();
if (!testVariantProps) {
throw new Error('missing testVariantProps');
@@ -864,7 +887,13 @@ function testThemeVariants(element: React.ReactElement, getOptions: () => Confor
this.skip();
}
- const { muiName, testCustomVariant, render, ThemeProvider = MDThemeProvider } = getOptions();
+ const {
+ muiName,
+ testCustomVariant,
+ render,
+ ThemeProvider = MDThemeProvider,
+ createTheme = mdCreateTheme,
+ } = getOptions();
if (!testCustomVariant) {
return;
@@ -919,6 +948,29 @@ function describeConformance(
minimalElement: React.ReactElement,
getOptions: () => InputConformanceOptions,
) {
+ let originalMatchmedia: typeof window.matchMedia;
+ const storage: Record = {};
+ beforeEach(() => {
+ originalMatchmedia = window.matchMedia;
+ // Create mocks of localStorage getItem and setItem functions
+ Object.defineProperty(global, 'localStorage', {
+ value: {
+ getItem: (key: string) => storage[key],
+ setItem: (key: string, value: string) => {
+ storage[key] = value;
+ },
+ },
+ configurable: true,
+ });
+ window.matchMedia = () =>
+ ({
+ addListener: () => {},
+ removeListener: () => {},
+ } as unknown as MediaQueryList);
+ });
+ afterEach(() => {
+ window.matchMedia = originalMatchmedia;
+ });
const {
after: runAfterHook = () => {},
only = Object.keys(fullSuite),