Skip to content

Commit

Permalink
[ButtonGroup] Determine first, last and middle buttons to support dif…
Browse files Browse the repository at this point in the history
…ferent elements with correct styling (mui#38520)
  • Loading branch information
ZeeshanTamboli authored and xcode-it committed Sep 11, 2023
1 parent 6fc19c2 commit 432d039
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 91 deletions.
5 changes: 4 additions & 1 deletion docs/pages/material-ui/api/button-group.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"text",
"disableElevation",
"disabled",
"firstButton",
"fullWidth",
"vertical",
"grouped",
Expand All @@ -73,7 +74,9 @@
"groupedContainedHorizontal",
"groupedContainedVertical",
"groupedContainedPrimary",
"groupedContainedSecondary"
"groupedContainedSecondary",
"lastButton",
"middleButton"
],
"globalClasses": { "disabled": "Mui-disabled" },
"name": "MuiButtonGroup"
Expand Down
12 changes: 12 additions & 0 deletions docs/translations/api-docs/button-group/button-group.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@
"nodeName": "the child elements",
"conditions": "<code>disabled={true}</code>"
},
"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",
Expand Down Expand Up @@ -151,6 +155,14 @@
"description": "Styles applied to {{nodeName}} if {{conditions}}.",
"nodeName": "the children",
"conditions": "<code>variant=\"contained\"</code> and <code>color=\"secondary\"</code>"
},
"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"
}
}
}
6 changes: 5 additions & 1 deletion packages/mui-material/src/Button/Button.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -345,10 +347,12 @@ const Button = React.forwardRef(function Button(inProps, ref) {
</ButtonEndIcon>
);

const positionClassName = buttonGroupButtonContextPositionClassName || '';

return (
<ButtonRoot
ownerState={ownerState}
className={clsx(contextProps.className, classes.root, className)}
className={clsx(contextProps.className, classes.root, className, positionClassName)}
component={component}
disabled={disabled}
focusRipple={!disableFocusRipple}
Expand Down
219 changes: 130 additions & 89 deletions packages/mui-material/src/ButtonGroup/ButtonGroup.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import styled from '../styles/styled';
import useThemeProps from '../styles/useThemeProps';
import buttonGroupClasses, { getButtonGroupUtilityClass } from './buttonGroupClasses';
import ButtonGroupContext from './ButtonGroupContext';
import ButtonGroupButtonContext from './ButtonGroupButtonContext';

const overridesResolver = (props, styles) => {
const { ownerState } = props;
Expand All @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -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,
}),
},
}));

Expand Down Expand Up @@ -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 (
<ButtonGroupRoot
as={component}
Expand All @@ -252,7 +281,19 @@ const ButtonGroup = React.forwardRef(function ButtonGroup(inProps, ref) {
ownerState={ownerState}
{...other}
>
<ButtonGroupContext.Provider value={context}>{children}</ButtonGroupContext.Provider>
<ButtonGroupContext.Provider value={context}>
{React.Children.map(children, (child, index) => {
if (!React.isValidElement(child)) {
return child;
}

return (
<ButtonGroupButtonContext.Provider value={getButtonPositionClassName(index, children)}>
{child}
</ButtonGroupButtonContext.Provider>
);
})}
</ButtonGroupContext.Provider>
</ButtonGroupRoot>
);
});
Expand Down
42 changes: 42 additions & 0 deletions packages/mui-material/src/ButtonGroup/ButtonGroup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,4 +209,46 @@ describe('<ButtonGroup />', () => {
expect(screen.getByRole('button')).to.have.class(buttonClasses.outlinedSecondary);
});
});

describe('position classes', () => {
it('correctly applies position classes to buttons', () => {
render(
<ButtonGroup>
<Button>Button 1</Button>
<Button>Button 2</Button>
<Button>Button 3</Button>
</ButtonGroup>,
);

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(
<ButtonGroup>
<Button>Single Button</Button>
</ButtonGroup>,
);

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);
});
});
});
Loading

0 comments on commit 432d039

Please sign in to comment.