From 6bc9bab40bc49a479a54ffabfc5730a963163211 Mon Sep 17 00:00:00 2001 From: Lucas Hilgert <77863078+lhilgert9@users.noreply.github.com> Date: Tue, 7 Nov 2023 16:40:04 +0100 Subject: [PATCH] [material-next][ButtonGroup] Copy `ButtonGroup` to material next (#39739) Signed-off-by: Lucas Hilgert <77863078+lhilgert9@users.noreply.github.com> Co-authored-by: Albert Yu --- .../mui-material-next/src/Button/Button.tsx | 16 +- .../src/ButtonGroup/ButtonGroup.d.ts | 107 +++++ .../src/ButtonGroup/ButtonGroup.js | 392 ++++++++++++++++++ .../src/ButtonGroup/ButtonGroup.test.js | 273 ++++++++++++ .../ButtonGroup/ButtonGroupButtonContext.ts | 16 + .../src/ButtonGroup/ButtonGroupContext.ts | 25 ++ .../src/ButtonGroup/buttonGroupClasses.ts | 105 +++++ .../src/ButtonGroup/index.d.ts | 4 + .../src/ButtonGroup/index.js | 4 + 9 files changed, 940 insertions(+), 2 deletions(-) create mode 100644 packages/mui-material-next/src/ButtonGroup/ButtonGroup.d.ts create mode 100644 packages/mui-material-next/src/ButtonGroup/ButtonGroup.js create mode 100644 packages/mui-material-next/src/ButtonGroup/ButtonGroup.test.js create mode 100644 packages/mui-material-next/src/ButtonGroup/ButtonGroupButtonContext.ts create mode 100644 packages/mui-material-next/src/ButtonGroup/ButtonGroupContext.ts create mode 100644 packages/mui-material-next/src/ButtonGroup/buttonGroupClasses.ts create mode 100644 packages/mui-material-next/src/ButtonGroup/index.d.ts create mode 100644 packages/mui-material-next/src/ButtonGroup/index.js diff --git a/packages/mui-material-next/src/Button/Button.tsx b/packages/mui-material-next/src/Button/Button.tsx index d44b739fa40cde..9782e03f54e6c6 100644 --- a/packages/mui-material-next/src/Button/Button.tsx +++ b/packages/mui-material-next/src/Button/Button.tsx @@ -1,7 +1,11 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import { unstable_capitalize as capitalize } from '@mui/utils'; +import clsx from 'clsx'; +import { + unstable_capitalize as capitalize, + internal_resolveProps as resolveProps, +} from '@mui/utils'; import { useSlotProps } from '@mui/base/utils'; import { unstable_composeClasses as composeClasses } from '@mui/base/composeClasses'; import { useThemeProps, alpha, shouldForwardProp } from '@mui/system'; @@ -10,6 +14,8 @@ import { getButtonUtilityClass } from './buttonClasses'; import buttonBaseClasses from '../ButtonBase/buttonBaseClasses'; import { ButtonProps, ExtendButton, ButtonTypeMap, ButtonOwnerState } from './Button.types'; import ButtonBase from '../ButtonBase'; +import ButtonGroupButtonContext from '../ButtonGroup/ButtonGroupButtonContext'; +import ButtonGroupContext from '../ButtonGroup/ButtonGroupContext'; const useUtilityClasses = (ownerState: ButtonOwnerState) => { const { classes, color, disableElevation, fullWidth, size, variant } = ownerState; @@ -366,7 +372,10 @@ const ButtonEndIcon = styled('span', { const Button = React.forwardRef(function Button< BaseComponentType extends React.ElementType = ButtonTypeMap['defaultComponent'], >(inProps: ButtonProps, ref: React.ForwardedRef) { - const props = useThemeProps({ props: inProps, name: 'MuiButton' }); + const contextProps = React.useContext(ButtonGroupContext); + const buttonGroupButtonContextPositionClassName = React.useContext(ButtonGroupButtonContext); + const resolvedProps = resolveProps(contextProps as ButtonProps, inProps); + const props = useThemeProps({ props: resolvedProps, name: 'MuiButton' }); const { children, classes: classesProp, @@ -392,6 +401,8 @@ const Button = React.forwardRef(function Button< const classes = useUtilityClasses(ownerState); + const positionClassName = buttonGroupButtonContextPositionClassName ?? ''; + const rootProps = useSlotProps({ elementType: ButtonRoot, externalForwardedProps: other, @@ -401,6 +412,7 @@ const Button = React.forwardRef(function Button< ref, }, ownerState, + className: clsx(contextProps.className, positionClassName), }); const startIcon = startIconProp && ( diff --git a/packages/mui-material-next/src/ButtonGroup/ButtonGroup.d.ts b/packages/mui-material-next/src/ButtonGroup/ButtonGroup.d.ts new file mode 100644 index 00000000000000..2215539bb411c6 --- /dev/null +++ b/packages/mui-material-next/src/ButtonGroup/ButtonGroup.d.ts @@ -0,0 +1,107 @@ +import * as React from 'react'; +import { SxProps } from '@mui/system'; +import { OverridableComponent, OverridableStringUnion, OverrideProps } from '@mui/types'; +import { Theme } from '../styles'; +import { ButtonGroupClasses } from './buttonGroupClasses'; + +export interface ButtonGroupPropsColorOverrides {} +export interface ButtonGroupPropsVariantOverrides {} +export interface ButtonGroupPropsSizeOverrides {} + +export interface ButtonGroupOwnProps { + /** + * The content of the component. + */ + children?: React.ReactNode; + /** + * Override or extend the styles applied to the component. + */ + classes?: Partial; + /** + * The color of the component. + * It supports both default and custom theme colors, which can be added as shown in the + * [palette customization guide](https://mui.com/material-ui/customization/palette/#custom-colors). + * @default 'primary' + */ + color?: OverridableStringUnion< + 'inherit' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning', + ButtonGroupPropsColorOverrides + >; + /** + * If `true`, the component is disabled. + * @default false + */ + disabled?: boolean; + /** + * If `true`, no elevation is used. + * @default false + */ + disableElevation?: boolean; + /** + * If `true`, the button ripple effect is disabled. + * @default false + */ + disableRipple?: boolean; + /** + * If `true`, the touch ripple effect is disabled. + * @default false + */ + disableTouchRipple?: boolean; + /** + * If `true`, the buttons will take up the full width of its container. + * @default false + */ + fullWidth?: boolean; + /** + * The component orientation (layout flow direction). + * @default 'horizontal' + */ + orientation?: 'vertical' | 'horizontal'; + /** + * The size of the component. + * `small` is equivalent to the dense button styling. + * @default 'medium' + */ + size?: OverridableStringUnion<'small' | 'medium' | 'large', ButtonGroupPropsSizeOverrides>; + /** + * The variant to use. + * @default 'outlined' + */ + variant?: OverridableStringUnion< + 'text' | 'outlined' | 'contained', + ButtonGroupPropsVariantOverrides + >; + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx?: SxProps; +} + +export interface ButtonGroupTypeMap< + AdditionalProps = {}, + RootComponent extends React.ElementType = 'div', +> { + props: AdditionalProps & ButtonGroupOwnProps; + defaultComponent: RootComponent; +} + +/** + * + * Demos: + * + * - [Button Group](https://mui.com/material-ui/react-button-group/) + * + * API: + * + * - [ButtonGroup API](https://mui.com/material-ui/api/button-group/) + */ +declare const ButtonGroup: OverridableComponent; + +export type ButtonGroupProps< + RootComponent extends React.ElementType = ButtonGroupTypeMap['defaultComponent'], + AdditionalProps = {}, +> = OverrideProps, RootComponent> & { + component?: React.ElementType; +}; + +export default ButtonGroup; diff --git a/packages/mui-material-next/src/ButtonGroup/ButtonGroup.js b/packages/mui-material-next/src/ButtonGroup/ButtonGroup.js new file mode 100644 index 00000000000000..b22bd12819c49a --- /dev/null +++ b/packages/mui-material-next/src/ButtonGroup/ButtonGroup.js @@ -0,0 +1,392 @@ +'use client'; +import * as React from 'react'; +import PropTypes from 'prop-types'; +import clsx from 'clsx'; +import { unstable_composeClasses as composeClasses } from '@mui/base/composeClasses'; +import { alpha } from '@mui/system'; +import { getValidReactChildren, unstable_capitalize as capitalize } from '@mui/utils'; +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; + + return [ + { [`& .${buttonGroupClasses.grouped}`]: styles.grouped }, + { + [`& .${buttonGroupClasses.grouped}`]: styles[`grouped${capitalize(ownerState.orientation)}`], + }, + { [`& .${buttonGroupClasses.grouped}`]: styles[`grouped${capitalize(ownerState.variant)}`] }, + { + [`& .${buttonGroupClasses.grouped}`]: + styles[`grouped${capitalize(ownerState.variant)}${capitalize(ownerState.orientation)}`], + }, + { + [`& .${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, + ownerState.fullWidth && styles.fullWidth, + ownerState.orientation === 'vertical' && styles.vertical, + ]; +}; + +const useUtilityClasses = (ownerState) => { + const { classes, color, disabled, disableElevation, fullWidth, orientation, variant } = + ownerState; + + const slots = { + root: [ + 'root', + variant, + orientation === 'vertical' && 'vertical', + fullWidth && 'fullWidth', + disableElevation && 'disableElevation', + ], + grouped: [ + 'grouped', + `grouped${capitalize(orientation)}`, + `grouped${capitalize(variant)}`, + `grouped${capitalize(variant)}${capitalize(orientation)}`, + `grouped${capitalize(variant)}${capitalize(color)}`, + disabled && 'disabled', + ], + firstButton: ['firstButton'], + lastButton: ['lastButton'], + middleButton: ['middleButton'], + }; + + return composeClasses(slots, getButtonGroupUtilityClass, classes); +}; + +const ButtonGroupRoot = styled('div', { + name: 'MuiButtonGroup', + slot: 'Root', + overridesResolver, +})(({ theme, ownerState }) => ({ + display: 'inline-flex', + borderRadius: (theme.vars || theme).shape.borderRadius, + ...(ownerState.variant === 'contained' && { + boxShadow: (theme.vars || theme).shadows[2], + }), + ...(ownerState.disableElevation && { + boxShadow: 'none', + }), + ...(ownerState.fullWidth && { + width: '100%', + }), + ...(ownerState.orientation === 'vertical' && { + flexDirection: 'column', + }), + [`& .${buttonGroupClasses.grouped}`]: { + minWidth: 40, + '&:hover': { + ...(ownerState.variant === 'contained' && { + boxShadow: 'none', + }), + }, + ...(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.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 === '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: 'currentColor', + }), + ...(ownerState.variant === 'outlined' && + ownerState.orientation === 'vertical' && { + borderBottomColor: 'currentColor', + }), + }, + }, + [`& .${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, + }), + }, +})); + +const ButtonGroup = React.forwardRef(function ButtonGroup(inProps, ref) { + const props = useThemeProps({ props: inProps, name: 'MuiButtonGroup' }); + const { + children, + className, + color = 'primary', + component = 'div', + disabled = false, + disableElevation = false, + disableTouchRipple = false, + disableRipple = false, + fullWidth = false, + orientation = 'horizontal', + size = 'medium', + variant = 'outlined', + ...other + } = props; + + const ownerState = { + ...props, + color, + component, + disabled, + disableElevation, + disableTouchRipple, + disableRipple, + fullWidth, + orientation, + size, + variant, + }; + + const classes = useUtilityClasses(ownerState); + + const context = React.useMemo( + () => ({ + className: classes.grouped, + color, + disabled, + disableElevation, + disableTouchRipple, + disableRipple, + fullWidth, + size, + variant, + }), + [ + color, + disabled, + disableElevation, + disableTouchRipple, + disableRipple, + fullWidth, + size, + variant, + classes.grouped, + ], + ); + + 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 ( + + + {validChildren.map((child, index) => { + return ( + + {child} + + ); + })} + + + ); +}); + +ButtonGroup.propTypes /* remove-proptypes */ = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the d.ts file and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * The content of the component. + */ + children: PropTypes.node, + /** + * Override or extend the styles applied to the component. + */ + classes: PropTypes.object, + /** + * @ignore + */ + className: PropTypes.string, + /** + * The color of the component. + * It supports both default and custom theme colors, which can be added as shown in the + * [palette customization guide](https://mui.com/material-ui/customization/palette/#custom-colors). + * @default 'primary' + */ + color: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([ + PropTypes.oneOf(['inherit', 'primary', 'secondary', 'error', 'info', 'success', 'warning']), + PropTypes.string, + ]), + /** + * The component used for the root node. + * Either a string to use a HTML element or a component. + */ + component: PropTypes.elementType, + /** + * If `true`, the component is disabled. + * @default false + */ + disabled: PropTypes.bool, + /** + * If `true`, no elevation is used. + * @default false + */ + disableElevation: PropTypes.bool, + /** + * If `true`, the button ripple effect is disabled. + * @default false + */ + disableRipple: PropTypes.bool, + /** + * If `true`, the touch ripple effect is disabled. + * @default false + */ + disableTouchRipple: PropTypes.bool, + /** + * If `true`, the buttons will take up the full width of its container. + * @default false + */ + fullWidth: PropTypes.bool, + /** + * The component orientation (layout flow direction). + * @default 'horizontal' + */ + orientation: PropTypes.oneOf(['horizontal', 'vertical']), + /** + * The size of the component. + * `small` is equivalent to the dense button styling. + * @default 'medium' + */ + size: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([ + PropTypes.oneOf(['small', 'medium', 'large']), + PropTypes.string, + ]), + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), + PropTypes.func, + PropTypes.object, + ]), + /** + * The variant to use. + * @default 'outlined' + */ + variant: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([ + PropTypes.oneOf(['contained', 'outlined', 'text']), + PropTypes.string, + ]), +}; + +export default ButtonGroup; diff --git a/packages/mui-material-next/src/ButtonGroup/ButtonGroup.test.js b/packages/mui-material-next/src/ButtonGroup/ButtonGroup.test.js new file mode 100644 index 00000000000000..74c70dabaeabe6 --- /dev/null +++ b/packages/mui-material-next/src/ButtonGroup/ButtonGroup.test.js @@ -0,0 +1,273 @@ +import * as React from 'react'; +import { expect } from 'chai'; +import { createRenderer, describeConformance, screen } from '@mui-internal/test-utils'; +import ButtonGroup, { buttonGroupClasses as classes } from '@mui/material-next/ButtonGroup'; +import { CssVarsProvider, extendTheme } from '@mui/material-next/styles'; +import Button, { buttonClasses } from '@mui/material-next/Button'; +import ButtonGroupContext from './ButtonGroupContext'; + +describe('', () => { + const { render } = createRenderer(); + + let originalMatchmedia; + + beforeEach(() => { + originalMatchmedia = window.matchMedia; + window.matchMedia = () => ({ + addListener: () => {}, + removeListener: () => {}, + }); + }); + afterEach(() => { + window.matchMedia = originalMatchmedia; + }); + + describeConformance( + + + , + () => ({ + classes, + inheritComponent: 'div', + render, + refInstanceof: window.HTMLDivElement, + testComponentPropWith: 'span', + muiName: 'MuiButtonGroup', + testVariantProps: { variant: 'contained' }, + skip: ['componentsProp'], + ThemeProvider: CssVarsProvider, + createTheme: extendTheme, + }), + ); + + it('should render with the root class but no others', () => { + const { container } = render( + + + , + ); + const buttonGroup = container.firstChild; + expect(buttonGroup).to.have.class(classes.root); + expect(buttonGroup).not.to.have.class(classes.contained); + expect(buttonGroup).not.to.have.class(classes.fullWidth); + }); + + it('should render an outlined button', () => { + const { getByRole } = render( + + + , + ); + const button = getByRole('button'); + expect(button).to.have.class(buttonClasses.outlined); + expect(button).to.have.class(classes.grouped); + expect(button).to.have.class(classes.groupedOutlined); + expect(button).to.have.class(classes.groupedOutlinedPrimary); + expect(button).not.to.have.class(classes.groupedOutlinedSecondary); + }); + + it('can render an outlined primary button', () => { + const { getByRole } = render( + + + , + ); + const button = getByRole('button'); + expect(button).to.have.class(buttonClasses.outlined); + expect(button).to.have.class(buttonClasses.colorPrimary); + expect(button).to.have.class(classes.grouped); + expect(button).to.have.class(classes.groupedOutlined); + expect(button).to.have.class(classes.groupedOutlinedPrimary); + expect(button).not.to.have.class(classes.groupedOutlinedSecondary); + }); + + // TODO v6: change to filled when MD3 styles are applied + it('can render a text button', () => { + const { getByRole } = render( + + + , + ); + const button = getByRole('button'); + expect(button).to.have.class(buttonClasses.text); + expect(button).to.have.class(classes.grouped); + expect(button).to.have.class(classes.groupedText); + expect(button).to.have.class(classes.groupedTextPrimary); + expect(button).not.to.have.class(classes.groupedTextSecondary); + }); + + it('can render a small button', () => { + const { getByRole } = render( + + + , + ); + const button = getByRole('button'); + expect(button).to.have.class(buttonClasses.outlined); + expect(button).to.have.class(buttonClasses.sizeSmall); + }); + + it('can render a large button', () => { + const { getByRole } = render( + + + , + ); + const button = getByRole('button'); + expect(button).to.have.class(buttonClasses.outlined); + expect(button).to.have.class(buttonClasses.sizeLarge); + }); + + it('should have a ripple by default', () => { + const { container } = render( + + + , + ); + expect(container.querySelector('.touchRipple')).not.to.equal(null); + }); + + it('can disable the elevation', () => { + const { getByRole } = render( + + + , + ); + const button = getByRole('button'); + expect(button).to.have.class(buttonClasses.disableElevation); + }); + + it('can disable the ripple', () => { + const { container } = render( + + + , + ); + expect(container.querySelector('.touchRipple')).to.equal(null); + }); + + it('should not be fullWidth by default', () => { + const { container, getByRole } = render( + + + , + ); + const button = getByRole('button'); + const buttonGroup = container.firstChild; + expect(buttonGroup).not.to.have.class(classes.fullWidth); + expect(button).not.to.have.class(buttonClasses.fullWidth); + }); + + it('can pass fullWidth to Button', () => { + const { container, getByRole } = render( + + + , + ); + const buttonGroup = container.firstChild; + const button = getByRole('button'); + expect(buttonGroup).to.have.class(classes.fullWidth); + expect(button).to.have.class(buttonClasses.fullWidth); + }); + + it('classes.grouped should be merged with Button className', () => { + render( + + + , + ); + expect(screen.getByRole('button')).to.have.class(classes.grouped); + expect(screen.getByRole('button')).to.have.class('foo-bar'); + }); + + it('should forward the context to children', () => { + let context; + render( + + + {(value) => { + context = value; + }} + + , + ); + expect(context.variant).to.equal('contained'); + expect(context.size).to.equal('large'); + expect(context.fullWidth).to.equal(false); + expect(context.disableRipple).to.equal(false); + expect(context.disableTouchRipple).to.equal(false); + expect(context.disableElevation).to.equal(false); + expect(context.disabled).to.equal(false); + expect(context.color).to.equal('primary'); + }); + + describe('theme default props on Button', () => { + it('should override default variant prop', () => { + render( + + + + + , + ); + + expect(screen.getByRole('button')).to.have.class(buttonClasses.outlined); + expect(screen.getByRole('button')).to.have.class(buttonClasses.sizeSmall); + expect(screen.getByRole('button')).to.have.class(buttonClasses.colorSecondary); + }); + }); + + 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-next/src/ButtonGroup/ButtonGroupButtonContext.ts b/packages/mui-material-next/src/ButtonGroup/ButtonGroupButtonContext.ts new file mode 100644 index 00000000000000..8a93fe171954ef --- /dev/null +++ b/packages/mui-material-next/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-next/src/ButtonGroup/ButtonGroupContext.ts b/packages/mui-material-next/src/ButtonGroup/ButtonGroupContext.ts new file mode 100644 index 00000000000000..25cea93815c950 --- /dev/null +++ b/packages/mui-material-next/src/ButtonGroup/ButtonGroupContext.ts @@ -0,0 +1,25 @@ +import * as React from 'react'; +import type { ButtonGroupProps } from './ButtonGroup'; + +interface ButtonGroupContextType { + className?: string; + color?: ButtonGroupProps['color']; + disabled?: boolean; + disableElevation?: boolean; + disableTouchRipple?: boolean; + disableRipple?: boolean; + fullWidth?: boolean; + size?: ButtonGroupProps['size']; + variant?: ButtonGroupProps['variant']; +} + +/** + * @ignore - internal component. + */ +const ButtonGroupContext = React.createContext({}); + +if (process.env.NODE_ENV !== 'production') { + ButtonGroupContext.displayName = 'ButtonGroupContext'; +} + +export default ButtonGroupContext; diff --git a/packages/mui-material-next/src/ButtonGroup/buttonGroupClasses.ts b/packages/mui-material-next/src/ButtonGroup/buttonGroupClasses.ts new file mode 100644 index 00000000000000..c824685b07fa57 --- /dev/null +++ b/packages/mui-material-next/src/ButtonGroup/buttonGroupClasses.ts @@ -0,0 +1,105 @@ +import { + unstable_generateUtilityClass as generateUtilityClass, + unstable_generateUtilityClasses as generateUtilityClasses, +} from '@mui/utils'; + +export interface ButtonGroupClasses { + /** Styles applied to the root element. */ + root: string; + /** Styles applied to the root element if `variant="contained"`. */ + contained: string; + /** Styles applied to the root element if `variant="outlined"`. */ + outlined: string; + /** Styles applied to the root element if `variant="text"`. */ + text: string; + /** Styles applied to the root element if `disableElevation={true}`. */ + 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"`. */ + vertical: string; + /** Styles applied to the children. */ + grouped: string; + /** Styles applied to the children if `orientation="horizontal"`. */ + groupedHorizontal: string; + /** Styles applied to the children if `orientation="vertical"`. */ + groupedVertical: string; + /** Styles applied to the children if `variant="text"`. */ + groupedText: string; + /** Styles applied to the children if `variant="text"` and `orientation="horizontal"`. */ + groupedTextHorizontal: string; + /** Styles applied to the children if `variant="text"` and `orientation="vertical"`. */ + groupedTextVertical: string; + /** Styles applied to the children if `variant="text"` and `color="primary"`. */ + groupedTextPrimary: string; + /** Styles applied to the children if `variant="text"` and `color="secondary"`. */ + groupedTextSecondary: string; + /** Styles applied to the children if `variant="outlined"`. */ + groupedOutlined: string; + /** Styles applied to the children if `variant="outlined"` and `orientation="horizontal"`. */ + groupedOutlinedHorizontal: string; + /** Styles applied to the children if `variant="outlined"` and `orientation="vertical"`. */ + groupedOutlinedVertical: string; + /** Styles applied to the children if `variant="outlined"` and `color="primary"`. */ + groupedOutlinedPrimary: string; + /** Styles applied to the children if `variant="outlined"` and `color="secondary"`. */ + groupedOutlinedSecondary: string; + /** Styles applied to the children if `variant="contained"`. */ + groupedContained: string; + /** Styles applied to the children if `variant="contained"` and `orientation="horizontal"`. */ + groupedContainedHorizontal: string; + /** Styles applied to the children if `variant="contained"` and `orientation="vertical"`. */ + groupedContainedVertical: string; + /** Styles applied to the children if `variant="contained"` and `color="primary"`. */ + 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; + +export function getButtonGroupUtilityClass(slot: string): string { + return generateUtilityClass('MuiButtonGroup', slot); +} + +const buttonGroupClasses: ButtonGroupClasses = generateUtilityClasses('MuiButtonGroup', [ + 'root', + 'contained', + 'outlined', + 'text', + 'disableElevation', + 'disabled', + 'firstButton', + 'fullWidth', + 'vertical', + 'grouped', + 'groupedHorizontal', + 'groupedVertical', + 'groupedText', + 'groupedTextHorizontal', + 'groupedTextVertical', + 'groupedTextPrimary', + 'groupedTextSecondary', + 'groupedOutlined', + 'groupedOutlinedHorizontal', + 'groupedOutlinedVertical', + 'groupedOutlinedPrimary', + 'groupedOutlinedSecondary', + 'groupedContained', + 'groupedContainedHorizontal', + 'groupedContainedVertical', + 'groupedContainedPrimary', + 'groupedContainedSecondary', + 'lastButton', + 'middleButton', +]); + +export default buttonGroupClasses; diff --git a/packages/mui-material-next/src/ButtonGroup/index.d.ts b/packages/mui-material-next/src/ButtonGroup/index.d.ts new file mode 100644 index 00000000000000..cb8b2e7907c4b4 --- /dev/null +++ b/packages/mui-material-next/src/ButtonGroup/index.d.ts @@ -0,0 +1,4 @@ +export { default } from './ButtonGroup'; +export * from './ButtonGroup'; +export { default as buttonGroupClasses } from './buttonGroupClasses'; +export * from './buttonGroupClasses'; diff --git a/packages/mui-material-next/src/ButtonGroup/index.js b/packages/mui-material-next/src/ButtonGroup/index.js new file mode 100644 index 00000000000000..3a585146bc9596 --- /dev/null +++ b/packages/mui-material-next/src/ButtonGroup/index.js @@ -0,0 +1,4 @@ +'use client'; +export { default } from './ButtonGroup'; +export { default as buttonGroupClasses } from './buttonGroupClasses'; +export * from './buttonGroupClasses';