diff --git a/docs/pages/material-ui/api/button-group.json b/docs/pages/material-ui/api/button-group.json
index ef1eb072c05e1b..a89d57e49e857b 100644
--- a/docs/pages/material-ui/api/button-group.json
+++ b/docs/pages/material-ui/api/button-group.json
@@ -54,6 +54,7 @@
"text",
"disableElevation",
"disabled",
+ "firstButton",
"fullWidth",
"vertical",
"grouped",
@@ -73,7 +74,9 @@
"groupedContainedHorizontal",
"groupedContainedVertical",
"groupedContainedPrimary",
- "groupedContainedSecondary"
+ "groupedContainedSecondary",
+ "lastButton",
+ "middleButton"
],
"globalClasses": { "disabled": "Mui-disabled" },
"name": "MuiButtonGroup"
diff --git a/docs/translations/api-docs/button-group/button-group.json b/docs/translations/api-docs/button-group/button-group.json
index 4f813acc0ba716..d2479a7bb3dee2 100644
--- a/docs/translations/api-docs/button-group/button-group.json
+++ b/docs/translations/api-docs/button-group/button-group.json
@@ -56,6 +56,10 @@
"nodeName": "the child elements",
"conditions": "disabled={true}
"
},
+ "firstButton": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "the first button in the button group"
+ },
"fullWidth": {
"description": "Styles applied to {{nodeName}} if {{conditions}}.",
"nodeName": "the root element",
@@ -151,6 +155,14 @@
"description": "Styles applied to {{nodeName}} if {{conditions}}.",
"nodeName": "the children",
"conditions": "variant=\"contained\"
and color=\"secondary\"
"
+ },
+ "lastButton": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "the last button in the button group"
+ },
+ "middleButton": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "buttons in the middle of the button group"
}
}
}
diff --git a/packages/mui-material/src/Button/Button.js b/packages/mui-material/src/Button/Button.js
index dd26bdd925f4f2..ad9fb46f49afc8 100644
--- a/packages/mui-material/src/Button/Button.js
+++ b/packages/mui-material/src/Button/Button.js
@@ -11,6 +11,7 @@ import ButtonBase from '../ButtonBase';
import capitalize from '../utils/capitalize';
import buttonClasses, { getButtonUtilityClass } from './buttonClasses';
import ButtonGroupContext from '../ButtonGroup/ButtonGroupContext';
+import ButtonGroupButtonContext from '../ButtonGroup/ButtonGroupButtonContext';
const useUtilityClasses = (ownerState) => {
const { color, disableElevation, fullWidth, size, variant, classes } = ownerState;
@@ -298,6 +299,7 @@ const ButtonEndIcon = styled('span', {
const Button = React.forwardRef(function Button(inProps, ref) {
// props priority: `inProps` > `contextProps` > `themeDefaultProps`
const contextProps = React.useContext(ButtonGroupContext);
+ const buttonGroupButtonContextPositionClassName = React.useContext(ButtonGroupButtonContext);
const resolvedProps = resolveProps(contextProps, inProps);
const props = useThemeProps({ props: resolvedProps, name: 'MuiButton' });
const {
@@ -345,10 +347,12 @@ const Button = React.forwardRef(function Button(inProps, ref) {
);
+ const positionClassName = buttonGroupButtonContextPositionClassName || '';
+
return (
{
const { ownerState } = props;
@@ -27,6 +28,15 @@ const overridesResolver = (props, styles) => {
[`& .${buttonGroupClasses.grouped}`]:
styles[`grouped${capitalize(ownerState.variant)}${capitalize(ownerState.color)}`],
},
+ {
+ [`& .${buttonGroupClasses.firstButton}`]: styles.firstButton,
+ },
+ {
+ [`& .${buttonGroupClasses.lastButton}`]: styles.lastButton,
+ },
+ {
+ [`& .${buttonGroupClasses.middleButton}`]: styles.middleButton,
+ },
styles.root,
styles[ownerState.variant],
ownerState.disableElevation === true && styles.disableElevation,
@@ -55,6 +65,9 @@ const useUtilityClasses = (ownerState) => {
`grouped${capitalize(variant)}${capitalize(color)}`,
disabled && 'disabled',
],
+ firstButton: ['firstButton'],
+ lastButton: ['lastButton'],
+ middleButton: ['middleButton'],
};
return composeClasses(slots, getButtonGroupUtilityClass, classes);
@@ -81,106 +94,106 @@ const ButtonGroupRoot = styled('div', {
}),
[`& .${buttonGroupClasses.grouped}`]: {
minWidth: 40,
- '&:not(:first-of-type)': {
- ...(ownerState.orientation === 'horizontal' && {
- borderTopLeftRadius: 0,
- borderBottomLeftRadius: 0,
- }),
- ...(ownerState.orientation === 'vertical' && {
- borderTopRightRadius: 0,
- borderTopLeftRadius: 0,
+ '&:hover': {
+ ...(ownerState.variant === 'contained' && {
+ boxShadow: 'none',
}),
- ...(ownerState.variant === 'outlined' &&
- ownerState.orientation === 'horizontal' && {
- marginLeft: -1,
- }),
- ...(ownerState.variant === 'outlined' &&
- ownerState.orientation === 'vertical' && {
- marginTop: -1,
- }),
},
- '&:not(:last-of-type)': {
- ...(ownerState.orientation === 'horizontal' && {
- borderTopRightRadius: 0,
- borderBottomRightRadius: 0,
+ ...(ownerState.variant === 'contained' && {
+ boxShadow: 'none',
+ }),
+ },
+ [`& .${buttonGroupClasses.firstButton},& .${buttonGroupClasses.middleButton}`]: {
+ ...(ownerState.orientation === 'horizontal' && {
+ borderTopRightRadius: 0,
+ borderBottomRightRadius: 0,
+ }),
+ ...(ownerState.orientation === 'vertical' && {
+ borderBottomRightRadius: 0,
+ borderBottomLeftRadius: 0,
+ }),
+ ...(ownerState.variant === 'text' &&
+ ownerState.orientation === 'horizontal' && {
+ borderRight: theme.vars
+ ? `1px solid rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.23)`
+ : `1px solid ${
+ theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.23)' : 'rgba(255, 255, 255, 0.23)'
+ }`,
+ [`&.${buttonGroupClasses.disabled}`]: {
+ borderRight: `1px solid ${(theme.vars || theme).palette.action.disabled}`,
+ },
}),
- ...(ownerState.orientation === 'vertical' && {
- borderBottomRightRadius: 0,
- borderBottomLeftRadius: 0,
+ ...(ownerState.variant === 'text' &&
+ ownerState.orientation === 'vertical' && {
+ borderBottom: theme.vars
+ ? `1px solid rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.23)`
+ : `1px solid ${
+ theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.23)' : 'rgba(255, 255, 255, 0.23)'
+ }`,
+ [`&.${buttonGroupClasses.disabled}`]: {
+ borderBottom: `1px solid ${(theme.vars || theme).palette.action.disabled}`,
+ },
}),
- ...(ownerState.variant === 'text' &&
- ownerState.orientation === 'horizontal' && {
- borderRight: theme.vars
- ? `1px solid rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.23)`
- : `1px solid ${
- theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.23)' : 'rgba(255, 255, 255, 0.23)'
- }`,
- [`&.${buttonGroupClasses.disabled}`]: {
- borderRight: `1px solid ${(theme.vars || theme).palette.action.disabled}`,
- },
- }),
- ...(ownerState.variant === 'text' &&
- ownerState.orientation === 'vertical' && {
- borderBottom: theme.vars
- ? `1px solid rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.23)`
- : `1px solid ${
- theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.23)' : 'rgba(255, 255, 255, 0.23)'
- }`,
- [`&.${buttonGroupClasses.disabled}`]: {
- borderBottom: `1px solid ${(theme.vars || theme).palette.action.disabled}`,
- },
- }),
- ...(ownerState.variant === 'text' &&
- ownerState.color !== 'inherit' && {
- borderColor: theme.vars
- ? `rgba(${theme.vars.palette[ownerState.color].mainChannel} / 0.5)`
- : alpha(theme.palette[ownerState.color].main, 0.5),
- }),
+ ...(ownerState.variant === 'text' &&
+ ownerState.color !== 'inherit' && {
+ borderColor: theme.vars
+ ? `rgba(${theme.vars.palette[ownerState.color].mainChannel} / 0.5)`
+ : alpha(theme.palette[ownerState.color].main, 0.5),
+ }),
+ ...(ownerState.variant === 'outlined' &&
+ ownerState.orientation === 'horizontal' && {
+ borderRightColor: 'transparent',
+ }),
+ ...(ownerState.variant === 'outlined' &&
+ ownerState.orientation === 'vertical' && {
+ borderBottomColor: 'transparent',
+ }),
+ ...(ownerState.variant === 'contained' &&
+ ownerState.orientation === 'horizontal' && {
+ borderRight: `1px solid ${(theme.vars || theme).palette.grey[400]}`,
+ [`&.${buttonGroupClasses.disabled}`]: {
+ borderRight: `1px solid ${(theme.vars || theme).palette.action.disabled}`,
+ },
+ }),
+ ...(ownerState.variant === 'contained' &&
+ ownerState.orientation === 'vertical' && {
+ borderBottom: `1px solid ${(theme.vars || theme).palette.grey[400]}`,
+ [`&.${buttonGroupClasses.disabled}`]: {
+ borderBottom: `1px solid ${(theme.vars || theme).palette.action.disabled}`,
+ },
+ }),
+ ...(ownerState.variant === 'contained' &&
+ ownerState.color !== 'inherit' && {
+ borderColor: (theme.vars || theme).palette[ownerState.color].dark,
+ }),
+ '&:hover': {
...(ownerState.variant === 'outlined' &&
ownerState.orientation === 'horizontal' && {
- borderRightColor: 'transparent',
+ borderRightColor: 'currentColor',
}),
...(ownerState.variant === 'outlined' &&
ownerState.orientation === 'vertical' && {
- borderBottomColor: 'transparent',
+ borderBottomColor: 'currentColor',
}),
- ...(ownerState.variant === 'contained' &&
- ownerState.orientation === 'horizontal' && {
- borderRight: `1px solid ${(theme.vars || theme).palette.grey[400]}`,
- [`&.${buttonGroupClasses.disabled}`]: {
- borderRight: `1px solid ${(theme.vars || theme).palette.action.disabled}`,
- },
- }),
- ...(ownerState.variant === 'contained' &&
- ownerState.orientation === 'vertical' && {
- borderBottom: `1px solid ${(theme.vars || theme).palette.grey[400]}`,
- [`&.${buttonGroupClasses.disabled}`]: {
- borderBottom: `1px solid ${(theme.vars || theme).palette.action.disabled}`,
- },
- }),
- ...(ownerState.variant === 'contained' &&
- ownerState.color !== 'inherit' && {
- borderColor: (theme.vars || theme).palette[ownerState.color].dark,
- }),
- '&:hover': {
- ...(ownerState.variant === 'outlined' &&
- ownerState.orientation === 'horizontal' && {
- borderRightColor: 'currentColor',
- }),
- ...(ownerState.variant === 'outlined' &&
- ownerState.orientation === 'vertical' && {
- borderBottomColor: 'currentColor',
- }),
- },
},
- '&:hover': {
- ...(ownerState.variant === 'contained' && {
- boxShadow: 'none',
- }),
- },
- ...(ownerState.variant === 'contained' && {
- boxShadow: 'none',
+ },
+ [`& .${buttonGroupClasses.lastButton},& .${buttonGroupClasses.middleButton}`]: {
+ ...(ownerState.orientation === 'horizontal' && {
+ borderTopLeftRadius: 0,
+ borderBottomLeftRadius: 0,
+ }),
+ ...(ownerState.orientation === 'vertical' && {
+ borderTopRightRadius: 0,
+ borderTopLeftRadius: 0,
}),
+ ...(ownerState.variant === 'outlined' &&
+ ownerState.orientation === 'horizontal' && {
+ marginLeft: -1,
+ }),
+ ...(ownerState.variant === 'outlined' &&
+ ownerState.orientation === 'vertical' && {
+ marginTop: -1,
+ }),
},
}));
@@ -243,6 +256,22 @@ const ButtonGroup = React.forwardRef(function ButtonGroup(inProps, ref) {
],
);
+ const getButtonPositionClassName = (index, childrenParam) => {
+ const isFirstButton = index === 0;
+ const isLastButton = index === React.Children.count(childrenParam) - 1;
+
+ if (isFirstButton && isLastButton) {
+ return '';
+ }
+ if (isFirstButton) {
+ return classes.firstButton;
+ }
+ if (isLastButton) {
+ return classes.lastButton;
+ }
+ return classes.middleButton;
+ };
+
return (
- {children}
+
+ {React.Children.map(children, (child, index) => {
+ if (!React.isValidElement(child)) {
+ return child;
+ }
+
+ return (
+
+ {child}
+
+ );
+ })}
+
);
});
diff --git a/packages/mui-material/src/ButtonGroup/ButtonGroup.test.js b/packages/mui-material/src/ButtonGroup/ButtonGroup.test.js
index 283625bd5ef426..e79439f7b13cba 100644
--- a/packages/mui-material/src/ButtonGroup/ButtonGroup.test.js
+++ b/packages/mui-material/src/ButtonGroup/ButtonGroup.test.js
@@ -209,4 +209,46 @@ describe('', () => {
expect(screen.getByRole('button')).to.have.class(buttonClasses.outlinedSecondary);
});
});
+
+ describe('position classes', () => {
+ it('correctly applies position classes to buttons', () => {
+ render(
+
+
+
+
+ ,
+ );
+
+ const firstButton = screen.getAllByRole('button')[0];
+ const middleButton = screen.getAllByRole('button')[1];
+ const lastButton = screen.getAllByRole('button')[2];
+
+ expect(firstButton).to.have.class(classes.firstButton);
+ expect(firstButton).not.to.have.class(classes.middleButton);
+ expect(firstButton).not.to.have.class(classes.lastButton);
+
+ expect(middleButton).to.have.class(classes.middleButton);
+ expect(middleButton).not.to.have.class(classes.firstButton);
+ expect(middleButton).not.to.have.class(classes.lastButton);
+
+ expect(lastButton).to.have.class(classes.lastButton);
+ expect(lastButton).not.to.have.class(classes.middleButton);
+ expect(lastButton).not.to.have.class(classes.firstButton);
+ });
+
+ it('does not apply any position classes to a single button', () => {
+ render(
+
+
+ ,
+ );
+
+ const button = screen.getByRole('button');
+
+ expect(button).not.to.have.class(classes.firstButton);
+ expect(button).not.to.have.class(classes.middleButton);
+ expect(button).not.to.have.class(classes.lastButton);
+ });
+ });
});
diff --git a/packages/mui-material/src/ButtonGroup/ButtonGroupButtonContext.ts b/packages/mui-material/src/ButtonGroup/ButtonGroupButtonContext.ts
new file mode 100644
index 00000000000000..8a93fe171954ef
--- /dev/null
+++ b/packages/mui-material/src/ButtonGroup/ButtonGroupButtonContext.ts
@@ -0,0 +1,16 @@
+import * as React from 'react';
+
+type ButtonPositionClassName = string;
+
+/**
+ * @ignore - internal component.
+ */
+const ButtonGroupButtonContext = React.createContext(
+ undefined,
+);
+
+if (process.env.NODE_ENV !== 'production') {
+ ButtonGroupButtonContext.displayName = 'ButtonGroupButtonContext';
+}
+
+export default ButtonGroupButtonContext;
diff --git a/packages/mui-material/src/ButtonGroup/buttonGroupClasses.ts b/packages/mui-material/src/ButtonGroup/buttonGroupClasses.ts
index 55dec11fdfe720..433083e3ae20f9 100644
--- a/packages/mui-material/src/ButtonGroup/buttonGroupClasses.ts
+++ b/packages/mui-material/src/ButtonGroup/buttonGroupClasses.ts
@@ -14,6 +14,8 @@ export interface ButtonGroupClasses {
disableElevation: string;
/** State class applied to the child elements if `disabled={true}`. */
disabled: string;
+ /** Styles applied to the first button in the button group. */
+ firstButton: string;
/** Styles applied to the root element if `fullWidth={true}`. */
fullWidth: string;
/** Styles applied to the root element if `orientation="vertical"`. */
@@ -54,6 +56,10 @@ export interface ButtonGroupClasses {
groupedContainedPrimary: string;
/** Styles applied to the children if `variant="contained"` and `color="secondary"`. */
groupedContainedSecondary: string;
+ /** Styles applied to the last button in the button group. */
+ lastButton: string;
+ /** Styles applied to buttons in the middle of the button group. */
+ middleButton: string;
}
export type ButtonGroupClassKey = keyof ButtonGroupClasses;
@@ -69,6 +75,7 @@ const buttonGroupClasses: ButtonGroupClasses = generateUtilityClasses('MuiButton
'text',
'disableElevation',
'disabled',
+ 'firstButton',
'fullWidth',
'vertical',
'grouped',
@@ -89,6 +96,8 @@ const buttonGroupClasses: ButtonGroupClasses = generateUtilityClasses('MuiButton
'groupedContainedVertical',
'groupedContainedPrimary',
'groupedContainedSecondary',
+ 'lastButton',
+ 'middleButton',
]);
export default buttonGroupClasses;
diff --git a/test/regressions/fixtures/ButtonGroup/DifferentChildren.js b/test/regressions/fixtures/ButtonGroup/DifferentChildren.js
new file mode 100644
index 00000000000000..44f107dcc0badd
--- /dev/null
+++ b/test/regressions/fixtures/ButtonGroup/DifferentChildren.js
@@ -0,0 +1,40 @@
+import * as React from 'react';
+import Button from '@mui/material/Button';
+import ButtonGroup from '@mui/material/ButtonGroup';
+import Stack from '@mui/material/Stack';
+import Tooltip from '@mui/material/Tooltip';
+
+export default function DifferentChildren() {
+ return (
+
+ {/* It has one button with href which is rendered as anchor tag */}
+
+
+
+
+
+
+ {/* With tooltip */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Single button */}
+
+
+
+
+ );
+}