diff --git a/docs/data/material/components/toggle-button/CustomizedDividers.js b/docs/data/material/components/toggle-button/CustomizedDividers.js
index e4e7a4cc074706..1c7063d44caaaa 100644
--- a/docs/data/material/components/toggle-button/CustomizedDividers.js
+++ b/docs/data/material/components/toggle-button/CustomizedDividers.js
@@ -12,22 +12,24 @@ import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import Divider from '@mui/material/Divider';
import Paper from '@mui/material/Paper';
import ToggleButton from '@mui/material/ToggleButton';
-import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
+import ToggleButtonGroup, {
+ toggleButtonGroupClasses,
+} from '@mui/material/ToggleButtonGroup';
const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }) => ({
- '& .MuiToggleButtonGroup-grouped': {
+ [`& .${toggleButtonGroupClasses.grouped}`]: {
margin: theme.spacing(0.5),
border: 0,
- '&.Mui-disabled': {
+ borderRadius: theme.shape.borderRadius,
+ [`&.${toggleButtonGroupClasses.disabled}`]: {
border: 0,
},
- '&:not(:first-of-type)': {
- borderRadius: theme.shape.borderRadius,
- },
- '&:first-of-type': {
- borderRadius: theme.shape.borderRadius,
- },
},
+ [`& .${toggleButtonGroupClasses.middleButton},& .${toggleButtonGroupClasses.lastButton}`]:
+ {
+ marginLeft: -1,
+ borderLeft: '1px solid transparent',
+ },
}));
export default function CustomizedDividers() {
diff --git a/docs/data/material/components/toggle-button/CustomizedDividers.tsx b/docs/data/material/components/toggle-button/CustomizedDividers.tsx
index 92dd37287a2798..63e67aaedbe755 100644
--- a/docs/data/material/components/toggle-button/CustomizedDividers.tsx
+++ b/docs/data/material/components/toggle-button/CustomizedDividers.tsx
@@ -12,22 +12,24 @@ import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import Divider from '@mui/material/Divider';
import Paper from '@mui/material/Paper';
import ToggleButton from '@mui/material/ToggleButton';
-import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
+import ToggleButtonGroup, {
+ toggleButtonGroupClasses,
+} from '@mui/material/ToggleButtonGroup';
const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }) => ({
- '& .MuiToggleButtonGroup-grouped': {
+ [`& .${toggleButtonGroupClasses.grouped}`]: {
margin: theme.spacing(0.5),
border: 0,
- '&.Mui-disabled': {
+ borderRadius: theme.shape.borderRadius,
+ [`&.${toggleButtonGroupClasses.disabled}`]: {
border: 0,
},
- '&:not(:first-of-type)': {
- borderRadius: theme.shape.borderRadius,
- },
- '&:first-of-type': {
- borderRadius: theme.shape.borderRadius,
- },
},
+ [`& .${toggleButtonGroupClasses.middleButton},& .${toggleButtonGroupClasses.lastButton}`]:
+ {
+ marginLeft: -1,
+ borderLeft: '1px solid transparent',
+ },
}));
export default function CustomizedDividers() {
diff --git a/docs/pages/material-ui/api/toggle-button-group.json b/docs/pages/material-ui/api/toggle-button-group.json
index e0d7c66bc56cb0..e3d1c10eefb45c 100644
--- a/docs/pages/material-ui/api/toggle-button-group.json
+++ b/docs/pages/material-ui/api/toggle-button-group.json
@@ -51,6 +51,12 @@
"description": "State class applied to the root element if `disabled={true}`.",
"isGlobal": true
},
+ {
+ "key": "firstButton",
+ "className": "MuiToggleButtonGroup-firstButton",
+ "description": "Styles applied to the first button in the toggle button group.",
+ "isGlobal": false
+ },
{
"key": "fullWidth",
"className": "MuiToggleButtonGroup-fullWidth",
@@ -75,6 +81,18 @@
"description": "Styles applied to the children if `orientation=\"vertical\"`.",
"isGlobal": false
},
+ {
+ "key": "lastButton",
+ "className": "MuiToggleButtonGroup-lastButton",
+ "description": "Styles applied to the last button in the toggle button group.",
+ "isGlobal": false
+ },
+ {
+ "key": "middleButton",
+ "className": "MuiToggleButtonGroup-middleButton",
+ "description": "Styles applied to buttons in the middle of the toggle button group.",
+ "isGlobal": false
+ },
{
"key": "root",
"className": "MuiToggleButtonGroup-root",
diff --git a/docs/translations/api-docs/toggle-button-group/toggle-button-group.json b/docs/translations/api-docs/toggle-button-group/toggle-button-group.json
index 8768d86fb8b4d3..21eccbcd92b660 100644
--- a/docs/translations/api-docs/toggle-button-group/toggle-button-group.json
+++ b/docs/translations/api-docs/toggle-button-group/toggle-button-group.json
@@ -37,6 +37,10 @@
"nodeName": "the root element",
"conditions": "disabled={true}
"
},
+ "firstButton": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "the first button in the toggle button group"
+ },
"fullWidth": {
"description": "Styles applied to {{nodeName}} if {{conditions}}.",
"nodeName": "the root element",
@@ -53,6 +57,14 @@
"nodeName": "the children",
"conditions": "orientation=\"vertical\"
"
},
+ "lastButton": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "the last button in the toggle button group"
+ },
+ "middleButton": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "buttons in the middle of the toggle button group"
+ },
"root": { "description": "Styles applied to the root element." },
"vertical": {
"description": "Styles applied to {{nodeName}} if {{conditions}}.",
diff --git a/packages/mui-material/src/ButtonGroup/ButtonGroupContext.ts b/packages/mui-material/src/ButtonGroup/ButtonGroupContext.ts
index 63352996156cc5..d336154db99963 100644
--- a/packages/mui-material/src/ButtonGroup/ButtonGroupContext.ts
+++ b/packages/mui-material/src/ButtonGroup/ButtonGroupContext.ts
@@ -1,7 +1,7 @@
import * as React from 'react';
import type { ButtonGroupProps } from './ButtonGroup';
-interface IButtonGroupContext {
+interface ButtonGroupContextType {
className?: string;
color?: ButtonGroupProps['color'];
disabled?: boolean;
@@ -16,7 +16,7 @@ interface IButtonGroupContext {
/**
* @ignore - internal component.
*/
-const ButtonGroupContext = React.createContext({});
+const ButtonGroupContext = React.createContext({});
if (process.env.NODE_ENV !== 'production') {
ButtonGroupContext.displayName = 'ButtonGroupContext';
diff --git a/packages/mui-material/src/ToggleButton/ToggleButton.js b/packages/mui-material/src/ToggleButton/ToggleButton.js
index d31b2b23b15e2b..eb7c65a40aa97f 100644
--- a/packages/mui-material/src/ToggleButton/ToggleButton.js
+++ b/packages/mui-material/src/ToggleButton/ToggleButton.js
@@ -3,6 +3,7 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
+import { internal_resolveProps as resolveProps } from '@mui/utils';
import { unstable_composeClasses as composeClasses } from '@mui/base/composeClasses';
import { alpha } from '../styles';
import ButtonBase from '../ButtonBase';
@@ -10,6 +11,9 @@ import capitalize from '../utils/capitalize';
import useThemeProps from '../styles/useThemeProps';
import styled from '../styles/styled';
import toggleButtonClasses, { getToggleButtonUtilityClass } from './toggleButtonClasses';
+import ToggleButtonGroupContext from '../ToggleButtonGroup/ToggleButtonGroupContext';
+import ToggleButtonGroupButtonContext from '../ToggleButtonGroup/ToggleButtonGroupButtonContext';
+import isValueSelected from '../ToggleButtonGroup/isValueSelected';
const useUtilityClasses = (ownerState) => {
const { classes, fullWidth, selected, disabled, size, color } = ownerState;
@@ -108,7 +112,16 @@ const ToggleButtonRoot = styled(ButtonBase, {
});
const ToggleButton = React.forwardRef(function ToggleButton(inProps, ref) {
- const props = useThemeProps({ props: inProps, name: 'MuiToggleButton' });
+ // props priority: `inProps` > `contextProps` > `themeDefaultProps`
+ const { value: contextValue, ...contextProps } = React.useContext(ToggleButtonGroupContext);
+ const toggleButtonGroupButtonContextPositionClassName = React.useContext(
+ ToggleButtonGroupButtonContext,
+ );
+ const resolvedProps = resolveProps(
+ { ...contextProps, selected: isValueSelected(inProps.value, contextValue) },
+ inProps,
+ );
+ const props = useThemeProps({ props: resolvedProps, name: 'MuiToggleButton' });
const {
children,
className,
@@ -148,9 +161,11 @@ const ToggleButton = React.forwardRef(function ToggleButton(inProps, ref) {
}
};
+ const positionClassName = toggleButtonGroupButtonContextPositionClassName || '';
+
return (
{
const { classes, orientation, fullWidth, disabled } = ownerState;
@@ -18,6 +21,9 @@ const useUtilityClasses = (ownerState) => {
const slots = {
root: ['root', orientation === 'vertical' && 'vertical', fullWidth && 'fullWidth'],
grouped: ['grouped', `grouped${capitalize(orientation)}`, disabled && 'disabled'],
+ firstButton: ['firstButton'],
+ lastButton: ['lastButton'],
+ middleButton: ['middleButton'],
};
return composeClasses(slots, getToggleButtonGroupUtilityClass, classes);
@@ -35,6 +41,15 @@ const ToggleButtonGroupRoot = styled('div', {
[`& .${toggleButtonGroupClasses.grouped}`]:
styles[`grouped${capitalize(ownerState.orientation)}`],
},
+ {
+ [`& .${toggleButtonGroupClasses.firstButton}`]: styles.firstButton,
+ },
+ {
+ [`& .${toggleButtonGroupClasses.lastButton}`]: styles.lastButton,
+ },
+ {
+ [`& .${toggleButtonGroupClasses.middleButton}`]: styles.middleButton,
+ },
styles.root,
ownerState.orientation === 'vertical' && styles.vertical,
ownerState.fullWidth && styles.fullWidth,
@@ -52,16 +67,6 @@ const ToggleButtonGroupRoot = styled('div', {
[`& .${toggleButtonGroupClasses.grouped}`]: {
...(ownerState.orientation === 'horizontal'
? {
- '&:not(:first-of-type)': {
- marginLeft: -1,
- borderLeft: '1px solid transparent',
- borderTopLeftRadius: 0,
- borderBottomLeftRadius: 0,
- },
- '&:not(:last-of-type)': {
- borderTopRightRadius: 0,
- borderBottomRightRadius: 0,
- },
[`&.${toggleButtonGroupClasses.selected} + .${toggleButtonGroupClasses.grouped}.${toggleButtonGroupClasses.selected}`]:
{
borderLeft: 0,
@@ -69,16 +74,6 @@ const ToggleButtonGroupRoot = styled('div', {
},
}
: {
- '&:not(:first-of-type)': {
- marginTop: -1,
- borderTop: '1px solid transparent',
- borderTopLeftRadius: 0,
- borderTopRightRadius: 0,
- },
- '&:not(:last-of-type)': {
- borderBottomLeftRadius: 0,
- borderBottomRightRadius: 0,
- },
[`&.${toggleButtonGroupClasses.selected} + .${toggleButtonGroupClasses.grouped}.${toggleButtonGroupClasses.selected}`]:
{
borderTop: 0,
@@ -86,6 +81,46 @@ const ToggleButtonGroupRoot = styled('div', {
},
}),
},
+ ...(ownerState.orientation === 'horizontal'
+ ? {
+ [`& .${toggleButtonGroupClasses.firstButton},& .${toggleButtonGroupClasses.middleButton}`]:
+ {
+ borderTopRightRadius: 0,
+ borderBottomRightRadius: 0,
+ },
+ [`& .${toggleButtonGroupClasses.lastButton},& .${toggleButtonGroupClasses.middleButton}`]: {
+ marginLeft: -1,
+ borderLeft: '1px solid transparent',
+ borderTopLeftRadius: 0,
+ borderBottomLeftRadius: 0,
+ },
+ }
+ : {
+ [`& .${toggleButtonGroupClasses.firstButton},& .${toggleButtonGroupClasses.middleButton}`]:
+ {
+ borderBottomLeftRadius: 0,
+ borderBottomRightRadius: 0,
+ },
+ [`& .${toggleButtonGroupClasses.lastButton},& .${toggleButtonGroupClasses.middleButton}`]: {
+ marginTop: -1,
+ borderTop: '1px solid transparent',
+ borderTopLeftRadius: 0,
+ borderTopRightRadius: 0,
+ },
+ }),
+ ...(ownerState.orientation === 'horizontal'
+ ? {
+ [`& .${toggleButtonGroupClasses.lastButton}.${toggleButtonClasses.disabled},& .${toggleButtonGroupClasses.middleButton}.${toggleButtonClasses.disabled}`]:
+ {
+ borderLeft: '1px solid transparent',
+ },
+ }
+ : {
+ [`& .${toggleButtonGroupClasses.lastButton}.${toggleButtonClasses.disabled},& .${toggleButtonGroupClasses.middleButton}.${toggleButtonClasses.disabled}`]:
+ {
+ borderTop: '1px solid transparent',
+ },
+ }),
}));
const ToggleButtonGroup = React.forwardRef(function ToggleButtonGroup(inProps, ref) {
@@ -106,30 +141,78 @@ const ToggleButtonGroup = React.forwardRef(function ToggleButtonGroup(inProps, r
const ownerState = { ...props, disabled, fullWidth, orientation, size };
const classes = useUtilityClasses(ownerState);
- const handleChange = (event, buttonValue) => {
- if (!onChange) {
- return;
- }
+ const handleChange = React.useCallback(
+ (event, buttonValue) => {
+ if (!onChange) {
+ return;
+ }
- const index = value && value.indexOf(buttonValue);
- let newValue;
+ const index = value && value.indexOf(buttonValue);
+ let newValue;
- if (value && index >= 0) {
- newValue = value.slice();
- newValue.splice(index, 1);
- } else {
- newValue = value ? value.concat(buttonValue) : [buttonValue];
- }
+ if (value && index >= 0) {
+ newValue = value.slice();
+ newValue.splice(index, 1);
+ } else {
+ newValue = value ? value.concat(buttonValue) : [buttonValue];
+ }
- onChange(event, newValue);
- };
+ onChange(event, newValue);
+ },
+ [onChange, value],
+ );
- const handleExclusiveChange = (event, buttonValue) => {
- if (!onChange) {
- return;
- }
+ const handleExclusiveChange = React.useCallback(
+ (event, buttonValue) => {
+ if (!onChange) {
+ return;
+ }
+
+ onChange(event, value === buttonValue ? null : buttonValue);
+ },
+ [onChange, value],
+ );
+
+ const context = React.useMemo(
+ () => ({
+ className: classes.grouped,
+ onChange: exclusive ? handleExclusiveChange : handleChange,
+ value,
+ size,
+ fullWidth,
+ color,
+ disabled,
+ }),
+ [
+ classes.grouped,
+ exclusive,
+ handleExclusiveChange,
+ handleChange,
+ value,
+ size,
+ fullWidth,
+ color,
+ disabled,
+ ],
+ );
- onChange(event, value === buttonValue ? null : buttonValue);
+ const validChildren = getValidReactChildren(children);
+ const childrenCount = validChildren.length;
+
+ const getButtonPositionClassName = (index) => {
+ const isFirstButton = index === 0;
+ const isLastButton = index === childrenCount - 1;
+
+ if (isFirstButton && isLastButton) {
+ return '';
+ }
+ if (isFirstButton) {
+ return classes.firstButton;
+ }
+ if (isLastButton) {
+ return classes.lastButton;
+ }
+ return classes.middleButton;
};
return (
@@ -140,35 +223,29 @@ const ToggleButtonGroup = React.forwardRef(function ToggleButtonGroup(inProps, r
ownerState={ownerState}
{...other}
>
- {React.Children.map(children, (child) => {
- if (!React.isValidElement(child)) {
- return null;
- }
-
- if (process.env.NODE_ENV !== 'production') {
- if (isFragment(child)) {
- console.error(
- [
- "MUI: The ToggleButtonGroup component doesn't accept a Fragment as a child.",
- 'Consider providing an array instead.',
- ].join('\n'),
- );
+
+ {validChildren.map((child, index) => {
+ if (process.env.NODE_ENV !== 'production') {
+ if (isFragment(child)) {
+ console.error(
+ [
+ "MUI: The ToggleButtonGroup component doesn't accept a Fragment as a child.",
+ 'Consider providing an array instead.',
+ ].join('\n'),
+ );
+ }
}
- }
- return React.cloneElement(child, {
- className: clsx(classes.grouped, child.props.className),
- onChange: exclusive ? handleExclusiveChange : handleChange,
- selected:
- child.props.selected === undefined
- ? isValueSelected(child.props.value, value)
- : child.props.selected,
- size: child.props.size || size,
- fullWidth,
- color: child.props.color || color,
- disabled: child.props.disabled || disabled,
- });
- })}
+ return (
+
+ {child}
+
+ );
+ })}
+
);
});
diff --git a/packages/mui-material/src/ToggleButtonGroup/ToggleButtonGroup.test.js b/packages/mui-material/src/ToggleButtonGroup/ToggleButtonGroup.test.js
index 5bfca3c6004acb..ff8d7c19461d55 100644
--- a/packages/mui-material/src/ToggleButtonGroup/ToggleButtonGroup.test.js
+++ b/packages/mui-material/src/ToggleButtonGroup/ToggleButtonGroup.test.js
@@ -6,6 +6,7 @@ import ToggleButtonGroup, {
toggleButtonGroupClasses as classes,
} from '@mui/material/ToggleButtonGroup';
import ToggleButton, { toggleButtonClasses } from '@mui/material/ToggleButton';
+import Tooltip from '@mui/material/Tooltip';
describe('', () => {
const { render } = createRenderer();
@@ -223,4 +224,60 @@ describe('', () => {
expect(buttonGroup).to.have.class(classes.fullWidth);
expect(button).to.have.class(toggleButtonClasses.fullWidth);
});
+
+ describe('position classes', () => {
+ it('correctly applies position classes to buttons', () => {
+ render(
+
+
+ One
+
+
+
+
+ Two
+
+
+
+
+
+
+ Three
+
+
+
+ ,
+ );
+
+ 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(
+
+ One
+ ,
+ );
+
+ 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/ToggleButtonGroup/ToggleButtonGroupButtonContext.ts b/packages/mui-material/src/ToggleButtonGroup/ToggleButtonGroupButtonContext.ts
new file mode 100644
index 00000000000000..df044c2b87ca7b
--- /dev/null
+++ b/packages/mui-material/src/ToggleButtonGroup/ToggleButtonGroupButtonContext.ts
@@ -0,0 +1,16 @@
+import * as React from 'react';
+
+type ToggleButtonPositionClassName = string;
+
+/**
+ * @ignore - internal component.
+ */
+const ToggleButtonGroupButtonContext = React.createContext<
+ ToggleButtonPositionClassName | undefined
+>(undefined);
+
+if (process.env.NODE_ENV !== 'production') {
+ ToggleButtonGroupButtonContext.displayName = 'ToggleButtonGroupButtonContext';
+}
+
+export default ToggleButtonGroupButtonContext;
diff --git a/packages/mui-material/src/ToggleButtonGroup/ToggleButtonGroupContext.ts b/packages/mui-material/src/ToggleButtonGroup/ToggleButtonGroupContext.ts
new file mode 100644
index 00000000000000..88c3153db2be8c
--- /dev/null
+++ b/packages/mui-material/src/ToggleButtonGroup/ToggleButtonGroupContext.ts
@@ -0,0 +1,23 @@
+import * as React from 'react';
+import type { ToggleButtonGroupProps } from './ToggleButtonGroup';
+
+interface ToggleButtonGroupContextType {
+ className?: string;
+ onChange?: ToggleButtonGroupProps['onChange'];
+ value?: ToggleButtonGroupProps['value'];
+ size?: ToggleButtonGroupProps['size'];
+ fullWidth?: ToggleButtonGroupProps['fullWidth'];
+ color?: ToggleButtonGroupProps['color'];
+ disabled?: ToggleButtonGroupProps['disabled'];
+}
+
+/**
+ * @ignore - internal component.
+ */
+const ToggleButtonGroupContext = React.createContext({});
+
+if (process.env.NODE_ENV !== 'production') {
+ ToggleButtonGroupContext.displayName = 'ToggleButtonGroupContext';
+}
+
+export default ToggleButtonGroupContext;
diff --git a/packages/mui-material/src/ToggleButtonGroup/toggleButtonGroupClasses.ts b/packages/mui-material/src/ToggleButtonGroup/toggleButtonGroupClasses.ts
index bf1cd0b471b50e..75cf854a4d59fc 100644
--- a/packages/mui-material/src/ToggleButtonGroup/toggleButtonGroupClasses.ts
+++ b/packages/mui-material/src/ToggleButtonGroup/toggleButtonGroupClasses.ts
@@ -16,6 +16,12 @@ export interface ToggleButtonGroupClasses {
groupedVertical: string;
/** Styles applied to the root element if `fullWidth={true}`. */
fullWidth: string;
+ /** Styles applied to the first button in the toggle button group. */
+ firstButton: string;
+ /** Styles applied to the last button in the toggle button group. */
+ lastButton: string;
+ /** Styles applied to buttons in the middle of the toggle button group. */
+ middleButton: string;
}
export type ToggleButtonGroupClassKey = keyof ToggleButtonGroupClasses;
@@ -35,6 +41,9 @@ const toggleButtonGroupClasses: ToggleButtonGroupClasses = generateUtilityClasse
'groupedHorizontal',
'groupedVertical',
'fullWidth',
+ 'firstButton',
+ 'lastButton',
+ 'middleButton',
],
);
diff --git a/test/regressions/fixtures/ToggleButtonGroup/DifferentChildren.js b/test/regressions/fixtures/ToggleButtonGroup/DifferentChildren.js
new file mode 100644
index 00000000000000..8d4e2813ed4b6d
--- /dev/null
+++ b/test/regressions/fixtures/ToggleButtonGroup/DifferentChildren.js
@@ -0,0 +1,46 @@
+import * as React from 'react';
+import ToggleButton from '@mui/material/ToggleButton';
+import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
+import Stack from '@mui/material/Stack';
+import Tooltip from '@mui/material/Tooltip';
+
+export default function DifferentChildren() {
+ const falsyCondition = 1 === 2;
+
+ return (
+
+ {/* With tooltip */}
+
+
+ One
+
+
+
+
+ Two
+
+
+
+
+
+
+ Three
+
+
+
+
+
+ {/* Single button */}
+
+ One
+
+
+ {/* Conditional elements */}
+
+ One
+ Two
+ {falsyCondition ? Three : undefined}
+
+
+ );
+}