Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ButtonGroup] Determine first, last and middle buttons to support different elements with correct styling #38520

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
393f3a0
add regression test
ZeeshanTamboli Aug 17, 2023
9dcb327
[ButtonGroup] Determine first and last buttons
ZeeshanTamboli Aug 17, 2023
01103cc
add middle button class
ZeeshanTamboli Aug 17, 2023
b9f6c7b
Merge branch 'master' into determine-first-last-buttons-in-button-group
ZeeshanTamboli Aug 17, 2023
62af815
update docs
ZeeshanTamboli Aug 17, 2023
f4bb2b9
fix style description
ZeeshanTamboli Aug 17, 2023
a7f1084
use data- attributes instead of class names
ZeeshanTamboli Aug 18, 2023
34f37ff
handle case for only a single button and if not within a button group…
ZeeshanTamboli Aug 19, 2023
8a1c416
reorganize Button group styles
ZeeshanTamboli Aug 28, 2023
367ce5a
Merge branch 'master' into determine-first-last-buttons-in-button-group
ZeeshanTamboli Aug 28, 2023
5c7de19
rename interface
ZeeshanTamboli Aug 29, 2023
5e38727
add data attributes directly
ZeeshanTamboli Aug 29, 2023
26650cf
remove redundant onlyChild
ZeeshanTamboli Aug 29, 2023
d7665e4
rename one to single
ZeeshanTamboli Aug 29, 2023
dc7f138
rename variables
ZeeshanTamboli Aug 29, 2023
77f651d
use "is" prefix in first and last button variable names
ZeeshanTamboli Aug 31, 2023
1939e1b
use classnames instead of data attributes
ZeeshanTamboli Sep 6, 2023
30b5ba8
add unit tests
ZeeshanTamboli Sep 6, 2023
69b97f6
move button position classes logic to button group component
ZeeshanTamboli Sep 6, 2023
9f3dd29
rename method name and type
ZeeshanTamboli Sep 6, 2023
e76692f
prettier
ZeeshanTamboli Sep 6, 2023
c5f8b76
Merge branch 'master' into determine-first-last-buttons-in-button-group
ZeeshanTamboli Sep 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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': {
DiegoAndai marked this conversation as resolved.
Show resolved Hide resolved
...(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;
}
DiegoAndai marked this conversation as resolved.
Show resolved Hide resolved

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
Loading