diff --git a/docs/data/joy/components/button/ButtonVariables.js b/docs/data/joy/components/button/ButtonVariables.js new file mode 100644 index 00000000000000..b26e75a2a32a9a --- /dev/null +++ b/docs/data/joy/components/button/ButtonVariables.js @@ -0,0 +1,25 @@ +import * as React from 'react'; +import JoyVariablesDemo from 'docs/src/modules/components/JoyVariablesDemo'; +import Button from '@mui/joy/Button'; +import FavoriteBorder from '@mui/icons-material/FavoriteBorder'; + +export default function ButtonVariables() { + return ( + ` + )} + /> + ); +} diff --git a/docs/data/joy/components/button/IconButtonVariables.js b/docs/data/joy/components/button/IconButtonVariables.js new file mode 100644 index 00000000000000..7c89190d93fd15 --- /dev/null +++ b/docs/data/joy/components/button/IconButtonVariables.js @@ -0,0 +1,28 @@ +import * as React from 'react'; +import JoyVariablesDemo from 'docs/src/modules/components/JoyVariablesDemo'; +import IconButton from '@mui/joy/IconButton'; +import FavoriteBorder from '@mui/icons-material/FavoriteBorder'; + +export default function IconButtonVariables() { + return ( + + `` : '\n>'} + +` + } + data={[ + { + var: '--IconButton-size', + defaultValue: '40px', + }, + ]} + renderDemo={(sx) => ( + + + + )} + /> + ); +} diff --git a/docs/data/joy/components/button/button.md b/docs/data/joy/components/button/button.md index 1a803d8e412831..7ffe89296caadc 100644 --- a/docs/data/joy/components/button/button.md +++ b/docs/data/joy/components/button/button.md @@ -68,7 +68,7 @@ Use the `IconButton` component if you want width and height to be the same while Every prop previously covered are available for this component as well. ```jsx -import Button from '@mui/joy/IconButton'; +import IconButton from '@mui/joy/IconButton'; ``` {{"demo": "IconButtons.js"}} @@ -93,3 +93,9 @@ Since links are the most appropriate component for navigating through pages, tha Doing so will automatically change the rendered HTML tag from ` + + { + const nextIndex = SIZES.indexOf(size) - 1; + const value = nextIndex < 0 ? SIZES[SIZES.length - 1] : SIZES[nextIndex]; + setSize(value); + handleClose(); + }} + > + Smaller + + { + const nextIndex = SIZES.indexOf(size) + 1; + const value = nextIndex > SIZES.length - 1 ? SIZES[0] : SIZES[nextIndex]; + setSize(value); + handleClose(); + }} + > + Larger + + + + + {SIZES.map((item) => ( + { + setSize(item); + handleClose(); + }} + > + + {item === size && } + {' '} + {item} + + ))} + + + + + ); +} diff --git a/docs/data/joy/components/menu/MenuListComposition.js b/docs/data/joy/components/menu/MenuListComposition.js index 01c3f4cdbfc21f..529e31c8168a86 100644 --- a/docs/data/joy/components/menu/MenuListComposition.js +++ b/docs/data/joy/components/menu/MenuListComposition.js @@ -1,10 +1,15 @@ import * as React from 'react'; import PopperUnstyled from '@mui/base/PopperUnstyled'; import ClickAwayListener from '@mui/base/ClickAwayListener'; +import { styled } from '@mui/joy/styles'; import Button from '@mui/joy/Button'; import MenuList from '@mui/joy/MenuList'; import MenuItem from '@mui/joy/MenuItem'; +const Popup = styled(PopperUnstyled)({ + zIndex: 1000, +}); + export default function MenuListComposition() { const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); @@ -38,7 +43,7 @@ export default function MenuListComposition() { > Open menu - Custom: 1.2 - + ); } diff --git a/docs/data/joy/components/menu/MenuListGroup.js b/docs/data/joy/components/menu/MenuListGroup.js new file mode 100644 index 00000000000000..45e017c1bf5aa8 --- /dev/null +++ b/docs/data/joy/components/menu/MenuListGroup.js @@ -0,0 +1,40 @@ +import * as React from 'react'; +import List from '@mui/joy/List'; +import ListItem from '@mui/joy/ListItem'; +import MenuList from '@mui/joy/MenuList'; +import MenuItem from '@mui/joy/MenuItem'; +import Typography from '@mui/joy/Typography'; + +export default function MenuListGroup() { + return ( + + {[...Array(5)].map((_, categoryIndex) => ( + + + + Category {categoryIndex + 1} + + + {[...Array(10)].map((__, index) => ( + Action {index + 1} + ))} + + ))} + + ); +} diff --git a/docs/data/joy/components/menu/menu.md b/docs/data/joy/components/menu/menu.md index 818cbacfe3d057..1d9d1f2f44e264 100644 --- a/docs/data/joy/components/menu/menu.md +++ b/docs/data/joy/components/menu/menu.md @@ -70,6 +70,10 @@ For example, this is how you'd go for displaying the menu on the bottom-end of t {{"demo": "PositionedMenu.js"}} +### Group menus + +{{"demo": "GroupMenu.js"}} + ### `MenuList` composition To get full control of the DOM structure, use the `MenuList` component. @@ -78,6 +82,10 @@ The primary responsibility of this component is handling the focus state. {{"demo": "MenuListComposition.js"}} +Or display the menu without a popup: + +{{"demo": "MenuListGroup.js"}} + ## Debugging To keep the list box open for inspecting elements, enable the `Emulate a focused page` option from the [Chrome DevTool Rendering](https://developer.chrome.com/docs/devtools/rendering/apply-effects/#emulate-a-focused-page) tab. diff --git a/docs/data/joy/components/radio/ExampleAlignmentButtons.js b/docs/data/joy/components/radio/ExampleAlignmentButtons.js new file mode 100644 index 00000000000000..2b12fa8c0dcc76 --- /dev/null +++ b/docs/data/joy/components/radio/ExampleAlignmentButtons.js @@ -0,0 +1,69 @@ +import * as React from 'react'; +import Box from '@mui/joy/Box'; +import Radio, { radioClasses } from '@mui/joy/Radio'; +import RadioGroup from '@mui/joy/RadioGroup'; +import FormatAlignCenterIcon from '@mui/icons-material/FormatAlignCenter'; +import FormatAlignJustifyIcon from '@mui/icons-material/FormatAlignJustify'; +import FormatAlignLeftIcon from '@mui/icons-material/FormatAlignLeft'; +import FormatAlignRightIcon from '@mui/icons-material/FormatAlignRight'; + +export default function RadioButtonsGroup() { + const [alignment, setAlignment] = React.useState('left'); + return ( + setAlignment(event.target.value)} + > + {['left', 'center', 'right', 'justify'].map((item) => ( + ({ + position: 'relative', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + width: 48, + height: 48, + '&:not([data-first-child])': { + borderLeft: '1px solid', + borderColor: 'divider', + }, + [`&[data-first-child] .${radioClasses.action}`]: { + borderTopLeftRadius: `calc(${theme.vars.radius.sm} - 1px)`, + borderBottomLeftRadius: `calc(${theme.vars.radius.sm} - 1px)`, + }, + [`&[data-last-child] .${radioClasses.action}`]: { + borderTopRightRadius: `calc(${theme.vars.radius.sm} - 1px)`, + borderBottomRightRadius: `calc(${theme.vars.radius.sm} - 1px)`, + }, + })} + > + , + right: , + center: , + justify: , + }[item] + } + variant={alignment === item ? 'solid' : 'plain'} + componentsProps={{ + input: { 'aria-label': item }, + }} + sx={{ + [`& .${radioClasses.action}`]: { borderRadius: 0, transition: 'none' }, + [`& .${radioClasses.label}`]: { lineHeight: 0 }, + }} + /> + + ))} + + ); +} diff --git a/docs/data/joy/components/radio/RadioUsage.js b/docs/data/joy/components/radio/RadioUsage.js index cc51971af2f73f..60e3ce673ecec9 100644 --- a/docs/data/joy/components/radio/RadioUsage.js +++ b/docs/data/joy/components/radio/RadioUsage.js @@ -1,5 +1,7 @@ import * as React from 'react'; -import JoyUsageDemo from 'docs/src/modules/components/JoyUsageDemo'; +import JoyUsageDemo, { + prependLinesSpace, +} from 'docs/src/modules/components/JoyUsageDemo'; import FormLabel from '@mui/joy/FormLabel'; import RadioGroup from '@mui/joy/RadioGroup'; import Radio from '@mui/joy/Radio'; @@ -7,7 +9,7 @@ import Radio from '@mui/joy/Radio'; export default function RadioUsage() { return ( ( + getCodeBlock={(code, props) => ` +${prependLinesSpace(code, 2)} +`} + renderDemo={({ row, ...props }) => (
- - - + + +
)} diff --git a/docs/data/joy/components/radio/radio.md b/docs/data/joy/components/radio/radio.md index 8b92150ea34b87..2a4e0b353547e6 100644 --- a/docs/data/joy/components/radio/radio.md +++ b/docs/data/joy/components/radio/radio.md @@ -117,9 +117,15 @@ Visit the [WAI-ARIA documentation](https://www.w3.org/WAI/ARIA/apg/patterns/radi ## Common examples +### Alignment buttons + +Simply provide an icon as a label to the `Radio` to make the radio buttons concise. You need to provide `aria-label` to the input slot for users who rely on screen readers. + +{{"demo": "ExampleAlignmentButtons.js"}} + ### Payment methods -Mix raddio buttons with the [`List`](/joy-ui/react-list/)-related components to create a commonly seen vertical or horizontal payment method list. +Mix radio buttons with the [`List`](/joy-ui/react-list/)-related components to create a commonly seen vertical or horizontal payment method list. {{"demo": "ExamplePaymentChannels.js"}} diff --git a/docs/data/joy/components/select/SelectGroupedOptions.js b/docs/data/joy/components/select/SelectGroupedOptions.js index e87542c32f2a36..90e53edacd29f1 100644 --- a/docs/data/joy/components/select/SelectGroupedOptions.js +++ b/docs/data/joy/components/select/SelectGroupedOptions.js @@ -31,7 +31,7 @@ export default function SelectGroupedOptions() { sx: { maxHeight: 240, overflow: 'auto', - py: 0, + '--List-padding': '0px', }, }, }} diff --git a/docs/data/joy/components/slider/VerticalSlider.js b/docs/data/joy/components/slider/VerticalSlider.js new file mode 100644 index 00000000000000..80ae0d6a86a6a6 --- /dev/null +++ b/docs/data/joy/components/slider/VerticalSlider.js @@ -0,0 +1,42 @@ +import * as React from 'react'; +import Box from '@mui/joy/Box'; +import Slider from '@mui/joy/Slider'; + +const marks = [ + { + value: 0, + label: '0°C', + }, + { + value: 20, + label: '20°C', + }, + { + value: 37, + label: '37°C', + }, + { + value: 100, + label: '100°C', + }, +]; + +function valueText(value) { + return `${value}°C`; +} + +export default function VerticalSlider() { + return ( + + + + ); +} diff --git a/docs/data/joy/components/slider/VerticalSlider.tsx b/docs/data/joy/components/slider/VerticalSlider.tsx new file mode 100644 index 00000000000000..17fdff6ab03b3e --- /dev/null +++ b/docs/data/joy/components/slider/VerticalSlider.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import Box from '@mui/joy/Box'; +import Slider from '@mui/joy/Slider'; + +const marks = [ + { + value: 0, + label: '0°C', + }, + { + value: 20, + label: '20°C', + }, + { + value: 37, + label: '37°C', + }, + { + value: 100, + label: '100°C', + }, +]; + +function valueText(value: number) { + return `${value}°C`; +} + +export default function VerticalSlider() { + return ( + + + + ); +} diff --git a/docs/data/joy/components/slider/VerticalSlider.tsx.preview b/docs/data/joy/components/slider/VerticalSlider.tsx.preview new file mode 100644 index 00000000000000..382ae9e8272fb3 --- /dev/null +++ b/docs/data/joy/components/slider/VerticalSlider.tsx.preview @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/docs/data/joy/components/slider/slider.md b/docs/data/joy/components/slider/slider.md index 26bf6a7c0c5f6a..d54b6c7f64a506 100644 --- a/docs/data/joy/components/slider/slider.md +++ b/docs/data/joy/components/slider/slider.md @@ -47,6 +47,12 @@ To make the thumb label always visible, toggle on the `valueLabelDisplay` prop. {{"demo": "AlwaysVisibleLabelSlider.js"}} +### Vertical + +Set `orientation="vertical"` to display the vertical slider. + +{{"demo": "VerticalSlider.js"}} + ### Keep the label at edges Apply the following styles to ensure that the label doesn't get cut off on mobile when it hits the edge of the slider. diff --git a/docs/data/joy/components/typography/NestedTypography.js b/docs/data/joy/components/typography/NestedTypography.js new file mode 100644 index 00000000000000..28dc7a8aa6f89d --- /dev/null +++ b/docs/data/joy/components/typography/NestedTypography.js @@ -0,0 +1,19 @@ +import * as React from 'react'; +import Typography from '@mui/joy/Typography'; + +export default function NestedTypography() { + return ( + + Typography lets you create nested{' '} + typography. Use your{' '} + + imagination + {' '} + to build wonderful{' '} + + user interface + + . + + ); +} diff --git a/docs/data/joy/components/typography/typography.md b/docs/data/joy/components/typography/typography.md index 9c8a4096056204..9e07593e2dcdd3 100644 --- a/docs/data/joy/components/typography/typography.md +++ b/docs/data/joy/components/typography/typography.md @@ -109,6 +109,8 @@ Nested `Typography` components will render as a `` tag by default, unless ``` +{{"demo": "NestedTypography.js"}} + ## Create a new scale To create your own typographic scale at the theme level, define the keys and values to `theme.typography` node. diff --git a/packages/mui-joy/src/Checkbox/Checkbox.tsx b/packages/mui-joy/src/Checkbox/Checkbox.tsx index fd134f48ee9d49..0b1ae4d0adcaed 100644 --- a/packages/mui-joy/src/Checkbox/Checkbox.tsx +++ b/packages/mui-joy/src/Checkbox/Checkbox.tsx @@ -59,6 +59,7 @@ const CheckboxRoot = styled('span', { display: 'inline-flex', fontFamily: theme.vars.fontFamily.body, lineHeight: 'var(--Checkbox-size)', // prevent label from having larger height than the checkbox + color: theme.vars.palette.text.primary, [`&.${checkboxClasses.disabled}`]: { color: theme.vars.palette[ownerState.color!]?.plainDisabledColor, }, diff --git a/packages/mui-joy/src/IconButton/IconButton.tsx b/packages/mui-joy/src/IconButton/IconButton.tsx index e4713035c913a2..28d11b5e7ca19b 100644 --- a/packages/mui-joy/src/IconButton/IconButton.tsx +++ b/packages/mui-joy/src/IconButton/IconButton.tsx @@ -40,19 +40,19 @@ const IconButtonRoot = styled('button', { { '--Icon-margin': 'initial', // reset the icon's margin. ...(ownerState.size === 'sm' && { - '--Icon-fontSize': '1.25rem', + '--Icon-fontSize': 'calc(var(--IconButton-size, 2rem) / 1.6)', // 1.25rem by default minWidth: 'var(--IconButton-size, 2rem)', // use min-width instead of height to make the button resilient to its content minHeight: 'var(--IconButton-size, 2rem)', // use min-height instead of height to make the button resilient to its content fontSize: theme.vars.fontSize.sm, }), ...(ownerState.size === 'md' && { - '--Icon-fontSize': '1.5rem', // control the SvgIcon font-size + '--Icon-fontSize': 'calc(var(--IconButton-size, 2.5rem) / 1.667)', // 1.5rem by default minWidth: 'var(--IconButton-size, 2.5rem)', minHeight: 'var(--IconButton-size, 2.5rem)', fontSize: theme.vars.fontSize.md, }), ...(ownerState.size === 'lg' && { - '--Icon-fontSize': '1.75rem', + '--Icon-fontSize': 'calc(var(--IconButton-size, 3rem) - 1.714)', // 1.75rem by default minWidth: 'var(--IconButton-size, 3rem)', minHeight: 'var(--IconButton-size, 3rem)', fontSize: theme.vars.fontSize.lg, diff --git a/packages/mui-joy/src/Input/Input.test.js b/packages/mui-joy/src/Input/Input.test.js index 195e781de2678e..b2a0c3b0be4170 100644 --- a/packages/mui-joy/src/Input/Input.test.js +++ b/packages/mui-joy/src/Input/Input.test.js @@ -39,6 +39,11 @@ describe('Joy ', () => { expect(screen.getByTestId('end')).toBeVisible(); }); + it('should change to textarea', () => { + const { container } = render(); + expect(container.firstChild.firstChild).to.have.tagName('textarea'); + }); + describe('prop: disabled', () => { it('should have disabled classes', () => { const { container } = render(); diff --git a/packages/mui-joy/src/Input/Input.tsx b/packages/mui-joy/src/Input/Input.tsx index 2c6602de43b19b..d0bac46d44b106 100644 --- a/packages/mui-joy/src/Input/Input.tsx +++ b/packages/mui-joy/src/Input/Input.tsx @@ -1,10 +1,9 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import clsx from 'clsx'; -import { unstable_capitalize as capitalize, unstable_useForkRef as useForkRef } from '@mui/utils'; +import { unstable_capitalize as capitalize } from '@mui/utils'; import { OverridableComponent } from '@mui/types'; import composeClasses from '@mui/base/composeClasses'; -import { appendOwnerState } from '@mui/base/utils'; +import { useSlotProps, EventHandlers } from '@mui/base/utils'; import { useInput, InputUnstyledOwnerState } from '@mui/base/InputUnstyled'; import { styled, useThemeProps } from '../styles'; import { InputTypeMap, InputProps } from './InputProps'; @@ -122,6 +121,7 @@ const InputRoot = styled('div', { [`&.${inputClasses.disabled}`]: theme.variants[`${ownerState.variant!}Disabled`]?.[ownerState.color!], }, + // This style has to come after the global variant to set the background to surface ownerState.variant !== 'solid' && { [`&.${inputClasses.focused}`]: { backgroundColor: theme.vars.palette.background.surface, @@ -205,7 +205,6 @@ const Input = React.forwardRef(function Input(inProps, ref) { className, color = 'neutral', component, - components = {}, componentsProps = {}, defaultValue, disabled, @@ -291,45 +290,46 @@ const Input = React.forwardRef(function Input(inProps, ref) { type, }; - const Root = component ?? InputRoot; - const rootProps = appendOwnerState( - Root, - { - ...getRootProps({ ...other, ...componentsProps.root }), - className: clsx(classes.root, rootStateClasses, className, componentsProps.root?.className), + const rootProps = useSlotProps({ + elementType: InputRoot, + getSlotProps: getRootProps, + externalSlotProps: componentsProps.root, + externalForwardedProps: other, + additionalProps: { + ref, + as: component, }, ownerState, - ); - - rootProps.ref = useForkRef(ref, useForkRef(rootProps.ref, componentsProps.root?.ref)); + className: [classes.root, rootStateClasses, className], + }); - const InputComponent = components.Input ?? InputInput; - const inputProps = appendOwnerState( - InputComponent, - { - ...getInputProps({ ...componentsProps.input, ...propsToForward }), - className: clsx(classes.input, inputStateClasses, componentsProps.input?.className), + const inputProps = useSlotProps({ + elementType: InputInput, + getSlotProps: (otherHandlers: EventHandlers) => + getInputProps({ ...otherHandlers, ...propsToForward }), + externalSlotProps: componentsProps.input, + additionalProps: { + as: componentsProps.input?.component, }, ownerState, - ); - - inputProps.ref = useForkRef(componentsProps.input?.ref, inputProps.ref); + className: [classes.input, inputStateClasses], + }); return ( - + {startDecorator && ( {startDecorator} )} - + {endDecorator && ( {endDecorator} )} - + ); }) as OverridableComponent; @@ -385,14 +385,6 @@ Input.propTypes /* remove-proptypes */ = { * Either a string to use a HTML element or a component. */ component: PropTypes.elementType, - /** - * The components used for each slot inside the InputBase. - * Either a string to use a HTML element or a component. - */ - components: PropTypes.shape({ - Input: PropTypes.elementType, - Root: PropTypes.elementType, - }), /** * The props used for each slot inside the Input. * @default {} diff --git a/packages/mui-joy/src/Input/InputProps.ts b/packages/mui-joy/src/Input/InputProps.ts index 67f01b4472e9b4..c137c5ebcb0e3b 100644 --- a/packages/mui-joy/src/Input/InputProps.ts +++ b/packages/mui-joy/src/Input/InputProps.ts @@ -41,21 +41,16 @@ export interface InputTypeMap

{ * @default 'neutral' */ color?: OverridableStringUnion; - /** - * The components used for each slot inside the InputBase. - * Either a string to use a HTML element or a component. - */ - components?: { - Root?: React.ElementType; - Input?: React.ElementType; - }; /** * The props used for each slot inside the Input. * @default {} */ componentsProps?: { root?: React.ComponentPropsWithRef<'div'>; - input?: React.ComponentPropsWithRef<'input'>; + input?: React.ComponentPropsWithRef<'input'> & { + component?: React.ElementType; + sx?: SxProps; + }; }; /** * Trailing adornment for this input. diff --git a/packages/mui-joy/src/List/List.test.js b/packages/mui-joy/src/List/List.test.js index 3d76ebd075b2d8..bd65e5f6181d61 100644 --- a/packages/mui-joy/src/List/List.test.js +++ b/packages/mui-joy/src/List/List.test.js @@ -56,7 +56,7 @@ describe('Joy ', () => { expect(getByRole('list')).to.have.class(classes.row); }); - describe('Semantics', () => { + describe('MenuList - integration', () => { it('should have role="group" inside MenuList', () => { render( @@ -66,6 +66,26 @@ describe('Joy ', () => { expect(screen.getByRole('group')).toBeVisible(); }); + it('should inherit size', () => { + render( + + + , + ); + expect(screen.getByRole('group')).to.have.class(classes.nesting); + }); + + it('should use instance size', () => { + render( + + + , + ); + expect(screen.getByRole('group')).to.have.class(classes.sizeLg); + }); + }); + + describe('Menu - integration', () => { it('should have role="group" inside Menu', () => { render(

document.createElement('div')}> @@ -75,6 +95,26 @@ describe('Joy ', () => { expect(screen.getByRole('group')).toBeVisible(); }); + it('should inherit size', () => { + render( + document.createElement('div')}> + + , + ); + expect(screen.getByRole('group')).to.have.class(classes.nesting); + }); + + it('should use instance size', () => { + render( + document.createElement('div')}> + + , + ); + expect(screen.getByRole('group')).to.have.class(classes.sizeLg); + }); + }); + + describe('Select - integration', () => { it('should have role="group" inside Select', () => { render( + + , + ); + expect(screen.getByRole('group')).to.have.class(classes.nesting); + }); + + it('should use instance size', () => { + render( + , + ); + expect(screen.getByRole('group')).to.have.class(classes.sizeLg); + }); }); }); diff --git a/packages/mui-joy/src/List/List.tsx b/packages/mui-joy/src/List/List.tsx index 384adc579f044e..a3ef0df0bde419 100644 --- a/packages/mui-joy/src/List/List.tsx +++ b/packages/mui-joy/src/List/List.tsx @@ -10,21 +10,22 @@ import { styled, useThemeProps } from '../styles'; import { ListProps, ListTypeMap } from './ListProps'; import { getListUtilityClass } from './listClasses'; import NestedListContext from './NestedListContext'; -import RowListContext from './RowListContext'; -import WrapListContext from './WrapListContext'; import ComponentListContext from './ComponentListContext'; +import ListProvider from './ListProvider'; -const useUtilityClasses = (ownerState: ListProps & { nesting: boolean }) => { - const { variant, color, size, nesting, row, scoped } = ownerState; +const useUtilityClasses = ( + ownerState: ListProps & { nesting: boolean; instanceSize: ListProps['size'] }, +) => { + const { variant, color, size, nesting, row, instanceSize } = ownerState; const slots = { root: [ 'root', variant && `variant${capitalize(variant)}`, color && `color${capitalize(color)}`, - size && `size${capitalize(size)}`, + !instanceSize && !nesting && size && `size${capitalize(size)}`, + instanceSize && `size${capitalize(instanceSize)}`, nesting && 'nesting', row && 'row', - scoped && 'scoped', ], }; @@ -74,23 +75,22 @@ export const ListRoot = styled('ul', { return {}; } return [ - ownerState.nesting && - !ownerState.scoped && { - // instanceSize is the specified size of the rendered element - // only apply size variables if instanceSize is provided so that the variables can be pass down to children by default. - ...applySizeVars(ownerState.instanceSize), - '--List-item-paddingRight': 'var(--List-item-paddingX)', - '--List-item-paddingLeft': 'var(--NestedList-item-paddingLeft)', - // reset ListItem, ListItemButton negative margin (caused by NestedListItem) - '--List-itemButton-marginBlock': '0px', - '--List-itemButton-marginInline': '0px', - '--List-item-marginBlock': '0px', - '--List-item-marginInline': '0px', - padding: 0, - marginInlineStart: 'var(--NestedList-marginLeft)', - marginInlineEnd: 'var(--NestedList-marginRight)', - marginBlockStart: 'var(--List-gap)', - }, + ownerState.nesting && { + // instanceSize is the specified size of the rendered element + // only apply size variables if instanceSize is provided so that the variables can be pass down to children by default. + ...applySizeVars(ownerState.instanceSize), + '--List-item-paddingRight': 'var(--List-item-paddingX)', + '--List-item-paddingLeft': 'var(--NestedList-item-paddingLeft)', + // reset ListItem, ListItemButton negative margin (caused by NestedListItem) + '--List-itemButton-marginBlock': '0px', + '--List-itemButton-marginInline': '0px', + '--List-item-marginBlock': '0px', + '--List-item-marginInline': '0px', + padding: 0, + marginInlineStart: 'var(--NestedList-marginLeft)', + marginInlineEnd: 'var(--NestedList-marginRight)', + marginBlockStart: 'var(--List-gap)', + }, !ownerState.nesting && { ...applySizeVars(ownerState.size), '--List-gap': '0px', @@ -98,12 +98,6 @@ export const ListRoot = styled('ul', { '--List-nestedInsetStart': '0px', '--List-item-paddingLeft': 'var(--List-item-paddingX)', '--List-item-paddingRight': 'var(--List-item-paddingX)', - ...(ownerState.scoped && { - '--List-itemButton-marginBlock': '0px', - '--List-itemButton-marginInline': '0px', - '--List-item-marginBlock': '0px', - '--List-item-marginInline': '0px', - }), '--internal-child-radius': 'max(var(--List-radius, 0px) - var(--List-padding), min(var(--List-padding) / 2, var(--List-radius, 0px) / 2))', // If --List-padding is 0, the --List-item-radius will be 0. @@ -163,7 +157,6 @@ const List = React.forwardRef(function List(inProps, ref) { size = 'md', row = false, wrap = false, - scoped = false, variant = 'plain', color = 'neutral', role: roleProp, @@ -174,7 +167,6 @@ const List = React.forwardRef(function List(inProps, ref) { instanceSize: inProps.size, size, nesting, - scoped, row, wrap, variant, @@ -186,31 +178,22 @@ const List = React.forwardRef(function List(inProps, ref) { const role = roleProp ?? (menuContext || selectContext ? 'group' : undefined); return ( - - - - - {React.Children.map(children, (child, index) => - React.isValidElement(child) - ? React.cloneElement(child, { - // to let List(Item|ItemButton) knows when to apply margin(Inline|Block)Start - ...(index === 0 && { 'data-first-child': '' }), - }) - : child, - )} - - - - + + + + {children} + + + ); }) as OverridableComponent; @@ -249,12 +232,6 @@ List.propTypes /* remove-proptypes */ = { * @default false */ row: PropTypes.bool, - /** - * If `true`, this list creates new list CSS variables scope to prevent the children from inheriting variables from the upper parent. - * This props is used in the listbox of Menu, Select. - * @default false - */ - scoped: PropTypes.bool, /** * The size of the component (affect other nested list* components). * @default 'md' diff --git a/packages/mui-joy/src/List/ListProps.ts b/packages/mui-joy/src/List/ListProps.ts index 13eddf14591dfd..a5b99a7510b67c 100644 --- a/packages/mui-joy/src/List/ListProps.ts +++ b/packages/mui-joy/src/List/ListProps.ts @@ -26,12 +26,6 @@ export interface ListTypeMap

{ * @default false */ row?: boolean; - /** - * If `true`, this list creates new list CSS variables scope to prevent the children from inheriting variables from the upper parent. - * This props is used in the listbox of Menu, Select. - * @default false - */ - scoped?: boolean; /** * The size of the component (affect other nested list* components). * @default 'md' diff --git a/packages/mui-joy/src/List/ListProvider.tsx b/packages/mui-joy/src/List/ListProvider.tsx new file mode 100644 index 00000000000000..0a9cb2d154b8a0 --- /dev/null +++ b/packages/mui-joy/src/List/ListProvider.tsx @@ -0,0 +1,76 @@ +import * as React from 'react'; +import RowListContext from './RowListContext'; +import WrapListContext from './WrapListContext'; +import NestedListContext from './NestedListContext'; + +/** + * This variables should be used in a List to create a scope + * that will not inherit variables from the upper scope. + * + * Used in `Menu`, `MenuList`, `TabList`, `Select` to communicate with nested List. + * + * e.g. menu group: + *

+ * ... + * ... + * + */ +export const scopedVariables = { + '--NestedList-marginRight': '0px', + '--NestedList-marginLeft': '0px', + '--NestedList-item-paddingLeft': 'var(--List-item-paddingX)', + // reset ListItem, ListItemButton negative margin (caused by NestedListItem) + '--List-itemButton-marginBlock': '0px', + '--List-itemButton-marginInline': '0px', + '--List-item-marginBlock': '0px', + '--List-item-marginInline': '0px', +}; + +export interface ListProviderProps { + /** + * If `undefined`, there is no effect. + * If `true` or `false`, affects the nested List styles. + */ + nested?: boolean; + /** + * If `true`, display the list in horizontal direction. + * @default false + */ + row?: boolean; + /** + * Only for horizontal list. + * If `true`, the list sets the flex-wrap to "wrap" and adjust margin to have gap-like behavior (will move to `gap` in the future). + * + * @default false + */ + wrap?: boolean; +} + +// internal component +const ListProvider = ({ + children, + nested, + row = false, + wrap = false, +}: React.PropsWithChildren) => { + const baseProviders = ( + + + {React.Children.map(children, (child, index) => + React.isValidElement(child) + ? React.cloneElement(child, { + // to let List(Item|ItemButton) knows when to apply margin(Inline|Block)Start + ...(index === 0 && { 'data-first-child': '' }), + }) + : child, + )} + + + ); + if (nested === undefined) { + return baseProviders; + } + return {baseProviders}; +}; + +export default ListProvider; diff --git a/packages/mui-joy/src/ListDivider/ListDivider.tsx b/packages/mui-joy/src/ListDivider/ListDivider.tsx index 356d56e0fde86d..32737eab25cd0e 100644 --- a/packages/mui-joy/src/ListDivider/ListDivider.tsx +++ b/packages/mui-joy/src/ListDivider/ListDivider.tsx @@ -26,7 +26,7 @@ const ListDividerRoot = styled('li', { border: 'none', // reset the border for `hr` tag ...(ownerState.row && { borderInlineStart: '1px solid', - marginBlock: 0, + marginBlock: ownerState.inset === 'gutter' ? 'var(--List-item-paddingY)' : 0, marginInline: 'var(--List-divider-gap)', ...(ownerState['data-first-child'] === undefined && { // combine --List-gap and --List-divider-gap to replicate flexbox gap behavior @@ -120,8 +120,9 @@ ListDivider.propTypes /* remove-proptypes */ = { */ component: PropTypes.elementType, /** - * The empty space on the side(s) of the divider. - * This prop has no effect on the divider if the nearest parent List has `row` prop set to `true`. + * The empty space on the side(s) of the divider in a vertical list. + * + * For horizontal list (the nearest parent List has `row` prop set to `true`), only `inset="gutter"` affects the list divider. */ inset: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([ PropTypes.oneOf(['gutter', 'startDecorator', 'startContent']), diff --git a/packages/mui-joy/src/ListDivider/ListDividerProps.ts b/packages/mui-joy/src/ListDivider/ListDividerProps.ts index b2dc68f1040442..4a4ddb66063591 100644 --- a/packages/mui-joy/src/ListDivider/ListDividerProps.ts +++ b/packages/mui-joy/src/ListDivider/ListDividerProps.ts @@ -18,8 +18,9 @@ export interface ListDividerTypeMap

*/ classes?: Partial; /** - * The empty space on the side(s) of the divider. - * This prop has no effect on the divider if the nearest parent List has `row` prop set to `true`. + * The empty space on the side(s) of the divider in a vertical list. + * + * For horizontal list (the nearest parent List has `row` prop set to `true`), only `inset="gutter"` affects the list divider. */ inset?: OverridableStringUnion< 'gutter' | 'startDecorator' | 'startContent', diff --git a/packages/mui-joy/src/ListItem/ListItem.tsx b/packages/mui-joy/src/ListItem/ListItem.tsx index f2f5e19419ecb9..e45dabcfd96042 100644 --- a/packages/mui-joy/src/ListItem/ListItem.tsx +++ b/packages/mui-joy/src/ListItem/ListItem.tsx @@ -98,7 +98,7 @@ const ListItemRoot = styled('li', { fontFamily: theme.vars.fontFamily.body, ...(ownerState.sticky && { position: 'sticky', - top: 0, + top: 'var(--List-item-stickyTop, 0px)', // integration with Menu and Select. zIndex: 1, background: 'var(--List-item-stickyBackground)', }), diff --git a/packages/mui-joy/src/ListItemButton/ListItemButton.tsx b/packages/mui-joy/src/ListItemButton/ListItemButton.tsx index 769731cb0587da..35f5e38cadabe7 100644 --- a/packages/mui-joy/src/ListItemButton/ListItemButton.tsx +++ b/packages/mui-joy/src/ListItemButton/ListItemButton.tsx @@ -62,12 +62,13 @@ export const ListItemButtonRoot = styled('div', { marginInlineStart: ownerState.row ? 'var(--List-gap)' : undefined, marginBlockStart: ownerState.row ? undefined : 'var(--List-gap)', }), - // account for the border width + // account for the border width, so that all of the ListItemButtons content aligned horizontally paddingBlock: 'calc(var(--List-item-paddingY) - var(--variant-borderWidth))', + // account for the border width, so that all of the ListItemButtons content aligned vertically paddingInlineStart: - 'calc(var(--List-item-paddingLeft) + var(--List-item-startActionWidth, var(--internal-startActionWidth, 0px)) - var(--variant-borderWidth))', // --internal variable makes it possible to customize the actionWidth from the top List + 'calc(var(--List-item-paddingLeft) + var(--List-item-startActionWidth, var(--internal-startActionWidth, 0px)))', // --internal variable makes it possible to customize the actionWidth from the top List paddingInlineEnd: - 'calc(var(--List-item-paddingRight) + var(--List-item-endActionWidth, var(--internal-endActionWidth, 0px)) - var(--variant-borderWidth))', // --internal variable makes it possible to customize the actionWidth from the top List + 'calc(var(--List-item-paddingRight) + var(--List-item-endActionWidth, var(--internal-endActionWidth, 0px)))', // --internal variable makes it possible to customize the actionWidth from the top List minBlockSize: 'var(--List-item-minHeight)', border: 'none', borderRadius: 'var(--List-item-radius)', diff --git a/packages/mui-joy/src/Menu/Menu.tsx b/packages/mui-joy/src/Menu/Menu.tsx index cd7999982bd32d..bbb7f9762d5b74 100644 --- a/packages/mui-joy/src/Menu/Menu.tsx +++ b/packages/mui-joy/src/Menu/Menu.tsx @@ -7,7 +7,7 @@ import { useSlotProps } from '@mui/base/utils'; import { useMenu, MenuUnstyledContext, MenuUnstyledContextType } from '@mui/base/MenuUnstyled'; import PopperUnstyled from '@mui/base/PopperUnstyled'; import { ListRoot } from '../List/List'; -import RowListContext from '../List/RowListContext'; +import ListProvider, { scopedVariables } from '../List/ListProvider'; import { styled, useThemeProps } from '../styles'; import { MenuTypeMap, MenuProps } from './MenuProps'; import { getMenuUtilityClass } from './menuClasses'; @@ -34,21 +34,24 @@ const MenuRoot = styled(ListRoot, { })<{ ownerState: MenuProps }>(({ theme, ownerState }) => { const variantStyle = theme.variants[ownerState.variant!]?.[ownerState.color!]; return { - boxShadow: theme.vars.shadow.md, - zIndex: 1000, - ...(!variantStyle.backgroundColor && { - backgroundColor: theme.vars.palette.background.surface, - }), '--List-radius': theme.vars.radius.sm, '--List-item-stickyBackground': variantStyle?.backgroundColor || variantStyle?.background || theme.vars.palette.background.surface, // for sticky List + '--List-item-stickyTop': 'calc(var(--List-padding, var(--List-divider-gap)) * -1)', // negative amount of the List's padding block + ...scopedVariables, + boxShadow: theme.vars.shadow.md, + overflow: 'auto', + zIndex: 1000, + ...(!variantStyle.backgroundColor && { + backgroundColor: theme.vars.palette.background.surface, + }), }; }); const Menu = React.forwardRef(function Menu(inProps, ref) { - const props = useThemeProps({ + const props = useThemeProps({ props: inProps, name: 'JoyMenu', }); @@ -116,7 +119,6 @@ const Menu = React.forwardRef(function Menu(inProps, ref) { modifiers, open, nesting: false, - scoped: true, row: false, }; @@ -152,16 +154,7 @@ const Menu = React.forwardRef(function Menu(inProps, ref) { return ( - - {React.Children.map(children, (child, index) => - React.isValidElement(child) - ? React.cloneElement(child, { - // to let MenuItem knows when to apply margin(Inline|Block)Start - ...(index === 0 && { 'data-first-child': '' }), - }) - : child, - )} - + {children} ); @@ -264,6 +257,14 @@ Menu.propTypes /* remove-proptypes */ = { PropTypes.oneOf(['sm', 'md', 'lg']), 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 'plain' diff --git a/packages/mui-joy/src/Menu/MenuProps.ts b/packages/mui-joy/src/Menu/MenuProps.ts index 3bfe3d48331ab1..1e20543742bf33 100644 --- a/packages/mui-joy/src/Menu/MenuProps.ts +++ b/packages/mui-joy/src/Menu/MenuProps.ts @@ -2,7 +2,7 @@ import * as React from 'react'; import { OverrideProps, OverridableStringUnion } from '@mui/types'; import { PopperUnstyledProps } from '@mui/base/PopperUnstyled'; import { MenuUnstyledActions } from '@mui/base/MenuUnstyled'; -import { ColorPaletteProp, VariantProp } from '../styles/types'; +import { ColorPaletteProp, VariantProp, SxProps } from '../styles/types'; export type MenuSlot = 'root' | 'listbox'; @@ -38,6 +38,10 @@ export interface MenuTypeMap

{ * The size of the component (affect other nested list* components because the `Menu` inherits `List`). */ size?: OverridableStringUnion<'sm' | 'md' | 'lg', MenuPropsSizeOverrides>; + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx?: SxProps; /** * The variant to use. * @default 'plain' diff --git a/packages/mui-joy/src/MenuList/MenuList.test.js b/packages/mui-joy/src/MenuList/MenuList.test.js index 33fa8cf5a3284f..84ca8e211b7e9a 100644 --- a/packages/mui-joy/src/MenuList/MenuList.test.js +++ b/packages/mui-joy/src/MenuList/MenuList.test.js @@ -1,17 +1,15 @@ import * as React from 'react'; import { expect } from 'chai'; -import { describeConformance, createRenderer } from 'test/utils'; +import { describeConformance, createRenderer, screen } from 'test/utils'; import { ThemeProvider } from '@mui/joy/styles'; import MenuList, { menuListClasses as classes } from '@mui/joy/MenuList'; -import ListItem from '@mui/joy/ListItem'; -import List, { listClasses } from '@mui/joy/List'; describe('Joy ', () => { const { render } = createRenderer(); describeConformance(, () => ({ classes, - inheritComponent: List, + inheritComponent: 'ul', render, ThemeProvider, muiName: 'MuiMenuList', @@ -22,7 +20,7 @@ describe('Joy ', () => { it('should have root className', () => { const { container } = render(); expect(container.firstChild).to.have.class(classes.root); - expect(container.firstChild).to.have.class(listClasses.sizeMd); + expect(container.firstChild).to.have.class(classes.sizeMd); }); it('should accept className prop', () => { @@ -30,22 +28,18 @@ describe('Joy ', () => { expect(container.firstChild).to.have.class('foo-bar'); }); - it('should have sm classes', () => { - const { container } = render(); - expect(container.firstChild).to.have.class(listClasses.sizeSm); + it('prop: size', () => { + render(); + expect(screen.getByRole('menu')).to.have.class(classes.sizeSm); }); - it('should have lg classes', () => { - const { container } = render(); - expect(container.firstChild).to.have.class(listClasses.sizeLg); + it('prop: variant', () => { + render(); + expect(screen.getByRole('menu')).to.have.class(classes.variantOutlined); }); - it('should have nested classes', () => { - const { getByRole } = render( - - - , - ); - expect(getByRole('menu')).to.have.class(listClasses.nesting); + it('prop: color', () => { + render(); + expect(screen.getByRole('menu')).to.have.class(classes.colorPrimary); }); }); diff --git a/packages/mui-joy/src/MenuList/MenuList.tsx b/packages/mui-joy/src/MenuList/MenuList.tsx index 7397a722ade93b..b4b66d53d2f694 100644 --- a/packages/mui-joy/src/MenuList/MenuList.tsx +++ b/packages/mui-joy/src/MenuList/MenuList.tsx @@ -1,35 +1,67 @@ import * as React from 'react'; import PropTypes from 'prop-types'; +import { unstable_capitalize as capitalize } from '@mui/utils'; import { OverridableComponent } from '@mui/types'; import composeClasses from '@mui/base/composeClasses'; import { useSlotProps } from '@mui/base/utils'; import { useMenu, MenuUnstyledContext, MenuUnstyledContextType } from '@mui/base/MenuUnstyled'; import { styled, useThemeProps } from '../styles'; -import List from '../List'; +import { ListRoot } from '../List/List'; +import ListProvider, { scopedVariables } from '../List/ListProvider'; import { MenuListProps, MenuListTypeMap } from './MenuListProps'; import { getMenuListUtilityClass } from './menuListClasses'; -const useUtilityClasses = () => { +const useUtilityClasses = (ownerState: MenuListProps) => { + const { variant, color, size } = ownerState; const slots = { - root: ['root'], + root: [ + 'root', + variant && `variant${capitalize(variant)}`, + color && `color${capitalize(color)}`, + size && `size${capitalize(size)}`, + ], }; return composeClasses(slots, getMenuListUtilityClass, {}); }; -const MenuListRoot = styled(List, { +const MenuListRoot = styled(ListRoot, { name: 'MuiMenuList', slot: 'Root', overridesResolver: (props, styles) => styles.root, -})<{ ownerState: MenuListProps; component?: React.ElementType }>({}); +})<{ ownerState: MenuListProps; component?: React.ElementType }>(({ theme, ownerState }) => { + const variantStyle = theme.variants[ownerState.variant!]?.[ownerState.color!]; + return { + '--List-radius': theme.vars.radius.sm, + '--List-item-stickyBackground': + variantStyle?.backgroundColor || + variantStyle?.background || + theme.vars.palette.background.surface, + '--List-item-stickyTop': 'calc(var(--List-padding, var(--List-divider-gap)) * -1)', // negative amount of the List's padding block + ...scopedVariables, + overflow: 'auto', + ...(!variantStyle.backgroundColor && { + backgroundColor: theme.vars.palette.background.surface, + }), + }; +}); const MenuList = React.forwardRef(function MenuList(inProps, ref) { - const props = useThemeProps({ + const props = useThemeProps({ props: inProps, name: 'MuiMenuList', }); - const { actions, id: idProp, children, size = 'md', ...other } = props; + const { + actions, + id: idProp, + component, + children, + size = 'md', + variant = 'outlined', + color = 'neutral', + ...other + } = props; const { registerItem, @@ -53,18 +85,26 @@ const MenuList = React.forwardRef(function MenuList(inProps, ref) { [highlightFirstItem, highlightLastItem], ); - const classes = useUtilityClasses(); const ownerState = { ...props, + variant, + color, size, + instanceSize: size, + nesting: false, + row: false, }; + const classes = useUtilityClasses(ownerState); + const listboxProps = useSlotProps({ elementType: MenuListRoot, getSlotProps: getListboxProps, externalSlotProps: {}, externalForwardedProps: other, - additionalProps: { size }, + additionalProps: { + as: component, + }, ownerState, className: classes.root, }); @@ -80,7 +120,9 @@ const MenuList = React.forwardRef(function MenuList(inProps, ref) { return ( - {children} + + {children} + ); }) as OverridableComponent; @@ -104,21 +146,43 @@ MenuList.propTypes /* remove-proptypes */ = { }), ]), /** - * The content of the component. + * @ignore */ children: PropTypes.node, + /** + * The color of the component. It supports those theme colors that make sense for this component. + * @default 'neutral' + */ + color: PropTypes.oneOf(['danger', 'info', 'neutral', 'primary', 'success', 'warning']), + /** + * The component used for the root node. + * Either a string to use a HTML element or a component. + */ + component: PropTypes.elementType, /** * @ignore */ id: PropTypes.string, /** - * The size of the component (affect other nested list* components). - * @default 'md' + * The size of the component (affect other nested list* components because the `Menu` inherits `List`). */ size: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([ PropTypes.oneOf(['sm', 'md', 'lg']), 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 'plain' + */ + variant: PropTypes.oneOf(['outlined', 'plain', 'soft', 'solid']), } as any; export default MenuList; diff --git a/packages/mui-joy/src/MenuList/MenuListProps.ts b/packages/mui-joy/src/MenuList/MenuListProps.ts index 9500a6da7d6d07..0ff209c34a8487 100644 --- a/packages/mui-joy/src/MenuList/MenuListProps.ts +++ b/packages/mui-joy/src/MenuList/MenuListProps.ts @@ -1,21 +1,42 @@ import * as React from 'react'; -import { OverrideProps } from '@mui/types'; +import { OverrideProps, OverridableStringUnion } from '@mui/types'; import { MenuUnstyledActions } from '@mui/base/MenuUnstyled'; -import { ListProps } from '../List/ListProps'; +import { ColorPaletteProp, VariantProp, SxProps } from '../styles/types'; export type MenuListSlot = 'root'; +export interface MenuListPropsSizeOverrides {} +export interface MenuListPropsColorOverrides {} +export interface MenuListPropsVariantOverrides {} + export interface MenuActions extends MenuUnstyledActions {} export interface MenuListTypeMap

{ - props: P & - ListProps & { - /** - * A ref with imperative actions. - * It allows to select the first or last menu item. - */ - actions?: React.Ref; - }; + props: P & { + /** + * A ref with imperative actions. + * It allows to select the first or last menu item. + */ + actions?: React.Ref; + /** + * The color of the component. It supports those theme colors that make sense for this component. + * @default 'neutral' + */ + color?: OverridableStringUnion; + /** + * The size of the component (affect other nested list* components because the `Menu` inherits `List`). + */ + size?: OverridableStringUnion<'sm' | 'md' | 'lg', MenuListPropsSizeOverrides>; + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx?: SxProps; + /** + * The variant to use. + * @default 'plain' + */ + variant?: OverridableStringUnion; + }; defaultComponent: D; } diff --git a/packages/mui-joy/src/MenuList/menuListClasses.ts b/packages/mui-joy/src/MenuList/menuListClasses.ts index 902d3a9f1af5e4..3323c394b6651a 100644 --- a/packages/mui-joy/src/MenuList/menuListClasses.ts +++ b/packages/mui-joy/src/MenuList/menuListClasses.ts @@ -3,14 +3,32 @@ import { generateUtilityClass, generateUtilityClasses } from '../className'; export interface MenuListClasses { /** Styles applied to the root element. */ root: string; - /** Styles applied to the root element if wrapped with nested context. */ - nested: string; /** Styles applied to the root element if `size="sm"`. */ sizeSm: string; /** Styles applied to the root element if `size="md"`. */ sizeMd: string; /** Styles applied to the root element if `size="lg"`. */ sizeLg: string; + /** Classname applied to the root element if `color="primary"`. */ + colorPrimary: string; + /** Classname applied to the root element if `color="neutral"`. */ + colorNeutral: string; + /** Classname applied to the root element if `color="danger"`. */ + colorDanger: string; + /** Classname applied to the root element if `color="info"`. */ + colorInfo: string; + /** Classname applied to the root element if `color="success"`. */ + colorSuccess: string; + /** Classname applied to the root element if `color="warning"`. */ + colorWarning: string; + /** Classname applied to the root element if `variant="plain"`. */ + variantPlain: string; + /** Classname applied to the root element if `variant="outlined"`. */ + variantOutlined: string; + /** Classname applied to the root element if `variant="soft"`. */ + variantSoft: string; + /** Classname applied to the root element if `variant="solid"`. */ + variantSolid: string; } export type MenuListClassKey = keyof MenuListClasses; @@ -25,6 +43,16 @@ const menuClasses: MenuListClasses = generateUtilityClasses('JoyMenuList', [ 'sizeSm', 'sizeMd', 'sizeLg', + 'colorPrimary', + 'colorNeutral', + 'colorDanger', + 'colorInfo', + 'colorSuccess', + 'colorWarning', + 'variantPlain', + 'variantOutlined', + 'variantSoft', + 'variantSolid', ]); export default menuClasses; diff --git a/packages/mui-joy/src/Option/Option.tsx b/packages/mui-joy/src/Option/Option.tsx index 562a28665ff1e9..f830119ed58e6c 100644 --- a/packages/mui-joy/src/Option/Option.tsx +++ b/packages/mui-joy/src/Option/Option.tsx @@ -153,7 +153,7 @@ Option.propTypes /* remove-proptypes */ = { * A text representation of the option's content. * Used for keyboard text navigation matching. */ - label: PropTypes.string, + label: PropTypes.oneOfType([PropTypes.element, PropTypes.string]), /** * The system prop that allows defining system overrides as well as additional CSS styles. */ diff --git a/packages/mui-joy/src/Option/OptionProps.ts b/packages/mui-joy/src/Option/OptionProps.ts index 554ddd68e6f6da..6d27359a738555 100644 --- a/packages/mui-joy/src/Option/OptionProps.ts +++ b/packages/mui-joy/src/Option/OptionProps.ts @@ -33,7 +33,7 @@ export interface OptionTypeMap

{ * A text representation of the option's content. * Used for keyboard text navigation matching. */ - label?: string; + label?: string | React.ReactElement; /** * The variant to use. * @default 'plain' diff --git a/packages/mui-joy/src/Radio/Radio.tsx b/packages/mui-joy/src/Radio/Radio.tsx index 69069a377d78e0..92a72ae8f7fbe3 100644 --- a/packages/mui-joy/src/Radio/Radio.tsx +++ b/packages/mui-joy/src/Radio/Radio.tsx @@ -73,6 +73,7 @@ const RadioRoot = styled('span', { minWidth: 0, fontFamily: theme.vars.fontFamily.body, lineHeight: 'var(--Radio-size)', // prevent label from having larger height than the checkbox + color: theme.vars.palette.text.primary, [`&.${radioClasses.disabled}`]: { color: theme.vars.palette[ownerState.color!]?.plainDisabledColor, }, @@ -232,8 +233,8 @@ const Radio = React.forwardRef(function Radio(inProps, ref) { onFocus, onFocusVisible, required, - color: colorProp, - variant: variantProp = 'outlined', + color, + variant = 'outlined', size: sizeProp = 'md', uncheckedIcon, value, @@ -241,10 +242,8 @@ const Radio = React.forwardRef(function Radio(inProps, ref) { } = props; const id = useId(idOverride); const radioGroup = React.useContext(RadioGroupContext); - const color = inProps.color || radioGroup.color || colorProp; const activeColor = color || 'primary'; const inactiveColor = color || 'neutral'; - const variant = inProps.variant || radioGroup.variant || variantProp; const size = inProps.size || radioGroup.size || sizeProp; const name = inProps.name || radioGroup.name || nameProp; const disableIcon = inProps.disableIcon || radioGroup.disableIcon || disableIconProp; diff --git a/packages/mui-joy/src/RadioGroup/RadioGroup.tsx b/packages/mui-joy/src/RadioGroup/RadioGroup.tsx index 0c3bccd288e159..3b95f9158f5f99 100644 --- a/packages/mui-joy/src/RadioGroup/RadioGroup.tsx +++ b/packages/mui-joy/src/RadioGroup/RadioGroup.tsx @@ -2,7 +2,11 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; import { OverridableComponent } from '@mui/types'; -import { unstable_useControlled as useControlled, unstable_useId as useId } from '@mui/utils'; +import { + unstable_capitalize as capitalize, + unstable_useControlled as useControlled, + unstable_useId as useId, +} from '@mui/utils'; import { unstable_composeClasses as composeClasses } from '@mui/base'; import { styled, useThemeProps } from '../styles'; import { getRadioGroupUtilityClass } from './radioGroupClasses'; @@ -10,9 +14,15 @@ import { RadioGroupProps, RadioGroupTypeMap } from './RadioGroupProps'; import RadioGroupContext from './RadioGroupContext'; const useUtilityClasses = (ownerState: RadioGroupProps) => { - const { row } = ownerState; + const { row, size, variant, color } = ownerState; const slots = { - root: ['root', row && 'row'], + root: [ + 'root', + row && 'row', + variant && `variant${capitalize(variant)}`, + color && `color${capitalize(color)}`, + size && `size${capitalize(size)}`, + ], }; return composeClasses(slots, getRadioGroupUtilityClass, {}); @@ -22,7 +32,7 @@ const RadioGroupRoot = styled('div', { name: 'JoyRadioGroup', slot: 'Root', overridesResolver: (props, styles) => styles.root, -})<{ ownerState: RadioGroupProps }>(({ ownerState }) => ({ +})<{ ownerState: RadioGroupProps }>(({ ownerState, theme }) => ({ ...(ownerState.size === 'sm' && { '--RadioGroup-gap': '0.625rem', }), @@ -34,6 +44,8 @@ const RadioGroupRoot = styled('div', { }), display: 'flex', flexDirection: ownerState.row ? 'row' : 'column', + borderRadius: theme.vars.radius.sm, + ...theme.variants[ownerState.variant!]?.[ownerState.color!], })); const RadioGroup = React.forwardRef(function RadioGroup(inProps, ref) { @@ -52,8 +64,8 @@ const RadioGroup = React.forwardRef(function RadioGroup(inProps, ref) { overlay, value: valueProp, onChange, - color, - variant, + color = 'neutral', + variant = 'plain', size = 'md', row = false, ...otherProps @@ -68,6 +80,8 @@ const RadioGroup = React.forwardRef(function RadioGroup(inProps, ref) { const ownerState = { row, size, + variant, + color, ...props, }; @@ -86,12 +100,10 @@ const RadioGroup = React.forwardRef(function RadioGroup(inProps, ref) { return ( & { + Pick & { row?: boolean; name?: string; value?: unknown; @@ -10,9 +10,7 @@ const RadioGroupContext = React.createContext< } >({ row: undefined, - variant: undefined, size: undefined, - color: undefined, name: undefined, value: undefined, onChange: undefined, diff --git a/packages/mui-joy/src/RadioGroup/radioGroupClasses.ts b/packages/mui-joy/src/RadioGroup/radioGroupClasses.ts index 55c9494c11f7be..c9479df822a1ff 100644 --- a/packages/mui-joy/src/RadioGroup/radioGroupClasses.ts +++ b/packages/mui-joy/src/RadioGroup/radioGroupClasses.ts @@ -5,6 +5,32 @@ export interface RadioGroupClasses { root: string; /** Styles applied to the root element, if `row` is true. */ row: string; + /** Styles applied to the root element if `size="sm"`. */ + sizeSm: string; + /** Styles applied to the root element if `size="md"`. */ + sizeMd: string; + /** Styles applied to the root element if `size="lg"`. */ + sizeLg: string; + /** Styles applied to the root element if `color="primary"`. */ + colorPrimary: string; + /** Styles applied to the root element if `color="neutral"`. */ + colorNeutral: string; + /** Styles applied to the root element if `color="danger"`. */ + colorDanger: string; + /** Styles applied to the root element if `color="info"`. */ + colorInfo: string; + /** Styles applied to the root element if `color="success"`. */ + colorSuccess: string; + /** Styles applied to the root element if `color="warning"`. */ + colorWarning: string; + /** Styles applied to the root element if `variant="plain"`. */ + variantPlain: string; + /** Styles applied to the root element if `variant="outlined"`. */ + variantOutlined: string; + /** Styles applied to the root element if `variant="soft"`. */ + variantSoft: string; + /** Styles applied to the root element if `variant="solid"`. */ + variantSolid: string; } export type RadioGroupClassKey = keyof RadioGroupClasses; @@ -16,6 +42,19 @@ export function getRadioGroupUtilityClass(slot: string): string { const radioGroupClasses: RadioGroupClasses = generateUtilityClasses('JoyRadioGroup', [ 'root', 'row', + 'colorPrimary', + 'colorNeutral', + 'colorDanger', + 'colorInfo', + 'colorSuccess', + 'colorWarning', + 'variantPlain', + 'variantOutlined', + 'variantSoft', + 'variantSolid', + 'sizeSm', + 'sizeMd', + 'sizeLg', ]); export default radioGroupClasses; diff --git a/packages/mui-joy/src/Select/Select.tsx b/packages/mui-joy/src/Select/Select.tsx index a6b570e55c881e..50e7ffdcf21d64 100644 --- a/packages/mui-joy/src/Select/Select.tsx +++ b/packages/mui-joy/src/Select/Select.tsx @@ -17,7 +17,7 @@ import type { SelectChild, SelectOption } from '@mui/base/SelectUnstyled'; import { useSlotProps } from '@mui/base/utils'; import composeClasses from '@mui/base/composeClasses'; import { ListRoot } from '../List/List'; -import RowListContext from '../List/RowListContext'; +import ListProvider, { scopedVariables } from '../List/ListProvider'; import Unfold from '../internal/svg-icons/Unfold'; import { styled, useThemeProps } from '../styles'; import { SelectOwnProps, SelectStaticProps, SelectOwnerState, SelectTypeMap } from './SelectProps'; @@ -61,98 +61,102 @@ const SelectRoot = styled('div', { name: 'JoySelect', slot: 'Root', overridesResolver: (props, styles) => styles.root, -})<{ ownerState: SelectStaticProps }>(({ theme, ownerState }) => [ - { - '--Select-radius': theme.vars.radius.sm, // radius is used by the decorator children - '--Select-gap': '0.5rem', - '--Select-placeholderOpacity': 0.5, - '--Select-focusedThickness': 'calc(var(--variant-borderWidth, 1px) + 1px)', - '--Select-focusedHighlight': - theme.vars.palette[ownerState.color === 'neutral' ? 'primary' : ownerState.color!]?.[500], - '--Select-indicator-color': theme.vars.palette.text.tertiary, - ...(ownerState.size === 'sm' && { - '--Select-minHeight': '2rem', - '--Select-paddingInline': '0.5rem', - '--Select-decorator-childHeight': 'min(1.5rem, var(--Select-minHeight))', - '--Icon-fontSize': '1.25rem', - }), - ...(ownerState.size === 'md' && { - '--Select-minHeight': '2.5rem', - '--Select-paddingInline': '0.75rem', - '--Select-decorator-childHeight': 'min(2rem, var(--Select-minHeight))', - '--Icon-fontSize': '1.5rem', - }), - ...(ownerState.size === 'lg' && { - '--Select-minHeight': '3rem', - '--Select-paddingInline': '1rem', - '--Select-decorator-childHeight': 'min(2.375rem, var(--Select-minHeight))', - '--Icon-fontSize': '1.75rem', - }), - // variables for controlling child components - '--Select-decorator-childOffset': - 'min(calc(var(--Select-paddingInline) - (var(--Select-minHeight) - 2 * var(--variant-borderWidth) - var(--Select-decorator-childHeight)) / 2), var(--Select-paddingInline))', - '--internal-paddingBlock': - 'max((var(--Select-minHeight) - 2 * var(--variant-borderWidth) - var(--Select-decorator-childHeight)) / 2, 0px)', - '--Select-decorator-childRadius': - 'max((var(--Select-radius) - var(--variant-borderWidth)) - var(--internal-paddingBlock), min(var(--internal-paddingBlock) / 2, (var(--Select-radius) - var(--variant-borderWidth)) / 2))', - '--Button-minHeight': 'var(--Select-decorator-childHeight)', - '--IconButton-size': 'var(--Select-decorator-childHeight)', - '--Button-radius': 'var(--Select-decorator-childRadius)', - '--IconButton-radius': 'var(--Select-decorator-childRadius)', - boxSizing: 'border-box', - minWidth: 0, // forces the Select to stay inside a container by default - minHeight: 'var(--Select-minHeight)', - position: 'relative', - display: 'flex', - alignItems: 'center', - borderRadius: 'var(--Select-radius)', - paddingInline: `var(--Select-paddingInline)`, - fontFamily: theme.vars.fontFamily.body, - fontSize: theme.vars.fontSize.md, - ...(ownerState.size === 'sm' && { - fontSize: theme.vars.fontSize.sm, - }), - // TODO: discuss the transition approach in a separate PR. This value is copied from mui-material Button. - transition: - 'background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, border-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms', - '&:before': { +})<{ ownerState: SelectStaticProps }>(({ theme, ownerState }) => { + const variantStyle = theme.variants[`${ownerState.variant!}`]?.[ownerState.color!]; + return [ + { + '--Select-radius': theme.vars.radius.sm, + '--Select-gap': '0.5rem', + '--Select-placeholderOpacity': 0.5, + '--Select-focusedThickness': '2px', + '--Select-focusedHighlight': + theme.vars.palette[ownerState.color === 'neutral' ? 'primary' : ownerState.color!]?.[500], + '--Select-indicator-color': theme.vars.palette.text.tertiary, + ...(ownerState.size === 'sm' && { + '--Select-minHeight': '2rem', + '--Select-paddingInline': '0.5rem', + '--Select-decorator-childHeight': 'min(1.5rem, var(--Select-minHeight))', + '--Icon-fontSize': '1.25rem', + }), + ...(ownerState.size === 'md' && { + '--Select-minHeight': '2.5rem', + '--Select-paddingInline': '0.75rem', + '--Select-decorator-childHeight': 'min(2rem, var(--Select-minHeight))', + '--Icon-fontSize': '1.5rem', + }), + ...(ownerState.size === 'lg' && { + '--Select-minHeight': '3rem', + '--Select-paddingInline': '1rem', + '--Select-decorator-childHeight': 'min(2.375rem, var(--Select-minHeight))', + '--Icon-fontSize': '1.75rem', + }), + // variables for controlling child components + '--Select-decorator-childOffset': + 'min(calc(var(--Select-paddingInline) - (var(--Select-minHeight) - 2 * var(--variant-borderWidth) - var(--Select-decorator-childHeight)) / 2), var(--Select-paddingInline))', + '--internal-paddingBlock': + 'max((var(--Select-minHeight) - 2 * var(--variant-borderWidth) - var(--Select-decorator-childHeight)) / 2, 0px)', + '--Select-decorator-childRadius': + 'max((var(--Select-radius) - var(--variant-borderWidth)) - var(--internal-paddingBlock), min(var(--internal-paddingBlock) / 2, (var(--Select-radius) - var(--variant-borderWidth)) / 2))', + '--Button-minHeight': 'var(--Select-decorator-childHeight)', + '--IconButton-size': 'var(--Select-decorator-childHeight)', + '--Button-radius': 'var(--Select-decorator-childRadius)', + '--IconButton-radius': 'var(--Select-decorator-childRadius)', boxSizing: 'border-box', - content: '""', - display: 'block', - position: 'absolute', - pointerEvents: 'none', - top: 0, - left: 0, - right: 0, - bottom: 0, - zIndex: 1, - borderRadius: 'inherit', - margin: 'calc(var(--variant-borderWidth) * -1)', // for outlined variant - }, - [`&.${selectClasses.focusVisible}`]: { - '--Select-indicator-color': 'var(--Select-focusedHighlight)', - }, - [`&.${selectClasses.disabled}`]: { - '--Select-indicator-color': 'inherit', - }, - }, - { - // apply global variant styles - ...theme.variants[`${ownerState.variant!}`]?.[ownerState.color!], - '&:hover': theme.variants[`${ownerState.variant!}Hover`]?.[ownerState.color!], - [`&.${selectClasses.disabled}`]: - theme.variants[`${ownerState.variant!}Disabled`]?.[ownerState.color!], - }, - ownerState.variant !== 'solid' && { - // This style has to come after the global variant to set the background to initial - [`&.${selectClasses.focusVisible}`]: { - backgroundColor: 'initial', + minWidth: 0, + minHeight: 'var(--Select-minHeight)', + position: 'relative', + display: 'flex', + alignItems: 'center', + borderRadius: 'var(--Select-radius)', + ...(!variantStyle.backgroundColor && { + backgroundColor: theme.vars.palette.background.surface, + }), + paddingInline: `var(--Select-paddingInline)`, + fontFamily: theme.vars.fontFamily.body, + fontSize: theme.vars.fontSize.md, + ...(ownerState.size === 'sm' && { + fontSize: theme.vars.fontSize.sm, + }), + // TODO: discuss the transition approach in a separate PR. + transition: + 'background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms', '&:before': { - boxShadow: `inset 0 0 0 var(--Select-focusedThickness) var(--Select-focusedHighlight)`, + boxSizing: 'border-box', + content: '""', + display: 'block', + position: 'absolute', + pointerEvents: 'none', + top: 0, + left: 0, + right: 0, + bottom: 0, + zIndex: 1, + borderRadius: 'inherit', + margin: 'calc(var(--variant-borderWidth) * -1)', // for outlined variant + }, + [`&.${selectClasses.focusVisible}`]: { + '--Select-indicator-color': 'var(--Select-focusedHighlight)', + }, + [`&.${selectClasses.disabled}`]: { + '--Select-indicator-color': 'inherit', }, + ...(ownerState.variant !== 'solid' && { + [`&.${selectClasses.focusVisible}`]: { + '&:before': { + boxShadow: `inset 0 0 0 var(--Select-focusedThickness) var(--Select-focusedHighlight)`, + }, + }, + }), }, - }, -]); + { + // apply global variant styles + ...variantStyle, + '&:hover': theme.variants[`${ownerState.variant!}Hover`]?.[ownerState.color!], + [`&.${selectClasses.disabled}`]: + theme.variants[`${ownerState.variant!}Disabled`]?.[ownerState.color!], + }, + ]; +}); const SelectButton = styled('button', { name: 'JoySelect', @@ -184,17 +188,19 @@ const SelectListbox = styled(ListRoot, { })<{ ownerState: SelectOwnerState }>(({ theme, ownerState }) => { const variantStyle = theme.variants[ownerState.variant!]?.[ownerState.color!]; return { + '--List-radius': theme.vars.radius.sm, + '--List-item-stickyBackground': + variantStyle?.backgroundColor || + variantStyle?.background || + theme.vars.palette.background.surface, // for sticky List + '--List-item-stickyTop': 'calc(var(--List-padding, var(--List-divider-gap)) * -1)', // negative amount of the List's padding block + ...scopedVariables, outline: 'none', boxShadow: theme.vars.shadow.md, zIndex: 1000, ...(!variantStyle.backgroundColor && { backgroundColor: theme.vars.palette.background.surface, }), - '--List-radius': theme.vars.radius.sm, - '--List-item-stickyBackground': - variantStyle?.backgroundColor || - variantStyle?.background || - theme.vars.palette.background.surface, // for sticky List }; }); @@ -458,7 +464,6 @@ const Select = React.forwardRef(function Select( ownerState: { ...ownerState, nesting: false, - scoped: true, row: false, }, className: classes.listbox, @@ -498,16 +503,7 @@ const Select = React.forwardRef(function Select( {anchorEl && ( - - {React.Children.map(children, (child, index) => - React.isValidElement(child) - ? React.cloneElement(child, { - // to let Option knows when to apply margin(Inline|Block)Start - ...(index === 0 && { 'data-first-child': '' }), - }) - : child, - )} - + {children} )} diff --git a/packages/mui-joy/src/Switch/Switch.tsx b/packages/mui-joy/src/Switch/Switch.tsx index a2de121891bfc2..3a6195c4138bea 100644 --- a/packages/mui-joy/src/Switch/Switch.tsx +++ b/packages/mui-joy/src/Switch/Switch.tsx @@ -148,6 +148,7 @@ const SwitchTrack = styled('span', { height: 'var(--Switch-track-height)', width: 'var(--Switch-track-width)', display: 'flex', + flexShrink: 0, justifyContent: 'space-between', alignItems: 'center', boxSizing: 'border-box', diff --git a/packages/mui-joy/src/TabList/TabList.tsx b/packages/mui-joy/src/TabList/TabList.tsx index de95b8b46bd488..d3b591daeb15e9 100644 --- a/packages/mui-joy/src/TabList/TabList.tsx +++ b/packages/mui-joy/src/TabList/TabList.tsx @@ -8,7 +8,7 @@ import { useSlotProps } from '@mui/base/utils'; import { useThemeProps } from '../styles'; import styled from '../styles/styled'; import { ListRoot } from '../List/List'; -import RowListContext from '../List/RowListContext'; +import ListProvider, { scopedVariables } from '../List/ListProvider'; import SizeTabsContext from '../Tabs/SizeTabsContext'; import { getTabListUtilityClass } from './tabListClasses'; import { TabListProps, TabListOwnerState, TabListTypeMap } from './TabListProps'; @@ -34,10 +34,11 @@ const TabListRoot = styled(ListRoot, { slot: 'Root', overridesResolver: (props, styles) => styles.root, })<{ ownerState: TabListProps }>({ + flexGrow: 'initial', '--List-gap': 'var(--Tabs-gap)', '--List-padding': 'var(--Tabs-gap)', '--List-divider-gap': '0px', - flexGrow: 'initial', + ...scopedVariables, }); const TabList = React.forwardRef(function TabList(inProps, ref) { @@ -71,7 +72,6 @@ const TabList = React.forwardRef(function TabList(inProps, ref) { size, row, nesting: false, - scoped: true, }; const classes = useUtilityClasses(ownerState); @@ -91,19 +91,12 @@ const TabList = React.forwardRef(function TabList(inProps, ref) { const processedChildren = processChildren(); return ( - - {/* @ts-ignore conflicted ref types */} - - {React.Children.map(processedChildren, (child, index) => - React.isValidElement(child) - ? React.cloneElement(child, { - // to let List(Item|ItemButton) knows when to apply margin(Inline|Block)Start - ...(index === 0 && { 'data-first-child': '' }), - }) - : child, - )} - - + // @ts-ignore conflicted ref types + + + {processedChildren} + + ); }) as OverridableComponent; diff --git a/packages/mui-joy/src/TabPanel/TabPanel.tsx b/packages/mui-joy/src/TabPanel/TabPanel.tsx index d123170a63421e..575b1693651f87 100644 --- a/packages/mui-joy/src/TabPanel/TabPanel.tsx +++ b/packages/mui-joy/src/TabPanel/TabPanel.tsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { unstable_capitalize as capitalize } from '@mui/utils'; import { unstable_composeClasses as composeClasses } from '@mui/base'; import { OverridableComponent } from '@mui/types'; +import { useTabContext } from '@mui/base/TabsUnstyled'; import { useTabPanel } from '@mui/base/TabPanelUnstyled'; import { useSlotProps } from '@mui/base/utils'; import { styled, useThemeProps } from '../styles'; @@ -25,7 +26,13 @@ const TabPanelRoot = styled('div', { slot: 'Root', overridesResolver: (props, styles) => styles.root, })<{ ownerState: TabPanelOwnerState }>(({ theme, ownerState }) => ({ - padding: 'var(--Tabs-gap)', + display: ownerState.hidden ? 'none' : 'initial', + ...(ownerState.orientation === 'horizontal' && { + paddingBlockStart: 'var(--Tabs-gap)', + }), + ...(ownerState.orientation === 'vertical' && { + paddingInlineStart: 'var(--Tabs-gap)', + }), ...(ownerState.size === 'sm' && { fontSize: theme.vars.fontSize.sm, }), @@ -44,6 +51,7 @@ const TabPanel = React.forwardRef(function TabPanel(inProps, ref) { name: 'JoyTabPanel', }); + const { orientation } = useTabContext() || { orientation: 'horizontal' }; const tabsSize = React.useContext(SizeTabsContext); const { children, value, component, size: sizeProp, ...other } = props; @@ -54,6 +62,7 @@ const TabPanel = React.forwardRef(function TabPanel(inProps, ref) { const ownerState = { ...props, + orientation, hidden, size, }; diff --git a/packages/mui-joy/src/TabPanel/TabPanelProps.ts b/packages/mui-joy/src/TabPanel/TabPanelProps.ts index b615a216abee32..6e8c1634af987c 100644 --- a/packages/mui-joy/src/TabPanel/TabPanelProps.ts +++ b/packages/mui-joy/src/TabPanel/TabPanelProps.ts @@ -29,4 +29,5 @@ export type TabPanelProps< export type TabPanelOwnerState = TabPanelProps & { hidden: boolean; + orientation?: 'horizontal' | 'vertical'; }; diff --git a/packages/mui-joy/src/Tabs/Tabs.tsx b/packages/mui-joy/src/Tabs/Tabs.tsx index 238f4632244f8a..63ba34cd0e0add 100644 --- a/packages/mui-joy/src/Tabs/Tabs.tsx +++ b/packages/mui-joy/src/Tabs/Tabs.tsx @@ -70,7 +70,7 @@ const Tabs = React.forwardRef(function Tabs(inProps, ref) { ...other } = props; - const { tabsContextValue } = useTabs(props); + const { tabsContextValue } = useTabs({ ...props, orientation }); const ownerState = { ...props, diff --git a/packages/mui-joy/src/Typography/Typography.tsx b/packages/mui-joy/src/Typography/Typography.tsx index 67aedeb6fed198..0889d7b93a52cc 100644 --- a/packages/mui-joy/src/Typography/Typography.tsx +++ b/packages/mui-joy/src/Typography/Typography.tsx @@ -90,8 +90,11 @@ const TypographyRoot = styled('span', { marginBottom: '0.35em', }), ...(ownerState.variant && { + borderRadius: theme.vars.radius.xs, paddingInline: '0.25em', // better than left, right because it also works with writing mode. - marginInline: '-0.25em', + ...(!ownerState.nested && { + marginInline: '-0.25em', + }), ...theme.variants[ownerState.variant]?.[ownerState.color!], }), }));