Large
diff --git a/docs/data/joy/components/select/SelectDecorators.js b/docs/data/joy/components/select/SelectDecorators.js
index 14bc12add8f252..ea9f053598d8ae 100644
--- a/docs/data/joy/components/select/SelectDecorators.js
+++ b/docs/data/joy/components/select/SelectDecorators.js
@@ -10,7 +10,7 @@ export default function SelectDecorators() {
placeholder="Select a pet…"
startDecorator={
}
endDecorator={
-
+
+5
}
diff --git a/packages/mui-joy/src/Button/Button.tsx b/packages/mui-joy/src/Button/Button.tsx
index 8d72ae83844aff..45103d8514dd57 100644
--- a/packages/mui-joy/src/Button/Button.tsx
+++ b/packages/mui-joy/src/Button/Button.tsx
@@ -90,6 +90,7 @@ export const ButtonRoot = styled('button', {
paddingBlock: '0.375rem',
paddingInline: '1.5rem',
}),
+ WebkitTapHighlightColor: 'transparent',
borderRadius: `var(--Button-radius, ${theme.vars.radius.sm})`, // to be controlled by other components, eg. Input
margin: `var(--Button-margin)`, // to be controlled by other components, eg. Input
border: 'none',
diff --git a/packages/mui-joy/src/Button/ButtonProps.ts b/packages/mui-joy/src/Button/ButtonProps.ts
index b1a5a2c25765be..76ec8a6186575e 100644
--- a/packages/mui-joy/src/Button/ButtonProps.ts
+++ b/packages/mui-joy/src/Button/ButtonProps.ts
@@ -104,7 +104,7 @@ export interface ButtonOwnerState extends ButtonProps {
/**
* If `true`, the button's focus is visible.
*/
- focusVisible: boolean;
+ focusVisible?: boolean;
}
export type ExtendButton = ((
diff --git a/packages/mui-joy/src/Checkbox/CheckboxProps.ts b/packages/mui-joy/src/Checkbox/CheckboxProps.ts
index cc59a12fd89ab9..b43a79a6fbccdd 100644
--- a/packages/mui-joy/src/Checkbox/CheckboxProps.ts
+++ b/packages/mui-joy/src/Checkbox/CheckboxProps.ts
@@ -112,5 +112,5 @@ export interface CheckboxOwnerState extends CheckboxProps {
/**
* If `true`, the checkbox's focus is visible.
*/
- focusVisible: boolean;
+ focusVisible?: boolean;
}
diff --git a/packages/mui-joy/src/Chip/Chip.tsx b/packages/mui-joy/src/Chip/Chip.tsx
index 849ad117f00f8f..e41a634f20ca28 100644
--- a/packages/mui-joy/src/Chip/Chip.tsx
+++ b/packages/mui-joy/src/Chip/Chip.tsx
@@ -57,24 +57,24 @@ const ChipRoot = styled('div', {
'--Chip-gap': '0.25rem',
'--Chip-paddingInline': '0.5rem',
'--Chip-decorator-childHeight':
- 'calc(min(1.5rem, var(--Chip-minHeight)) - 2 * var(--variant-borderWidth))',
- '--Icon-fontSize': '0.875rem',
+ 'calc(min(1.125rem, var(--Chip-minHeight)) - 2 * var(--variant-borderWidth))',
+ '--Icon-fontSize': 'calc(var(--Chip-minHeight, 1.5rem) / 1.714)', // 0.875rem by default
'--Chip-minHeight': '1.5rem',
fontSize: theme.vars.fontSize.xs,
}),
...(ownerState.size === 'md' && {
'--Chip-gap': '0.375rem',
'--Chip-paddingInline': '0.75rem',
- '--Chip-decorator-childHeight': 'min(1.5rem, var(--Chip-minHeight))',
- '--Icon-fontSize': '1.125rem',
+ '--Chip-decorator-childHeight': 'min(1.375rem, var(--Chip-minHeight))',
+ '--Icon-fontSize': 'calc(var(--Chip-minHeight, 2rem) / 1.778)', // 1.125rem by default
'--Chip-minHeight': '2rem',
fontSize: theme.vars.fontSize.sm,
}),
...(ownerState.size === 'lg' && {
'--Chip-gap': '0.5rem',
'--Chip-paddingInline': '1rem',
- '--Chip-decorator-childHeight': 'min(2rem, var(--Chip-minHeight))',
- '--Icon-fontSize': '1.25rem',
+ '--Chip-decorator-childHeight': 'min(1.75rem, var(--Chip-minHeight))',
+ '--Icon-fontSize': 'calc(var(--Chip-minHeight, 2.5rem) / 2)', // 1.25rem by default
'--Chip-minHeight': '2.5rem',
fontSize: theme.vars.fontSize.md,
}),
diff --git a/packages/mui-joy/src/Chip/ChipProps.ts b/packages/mui-joy/src/Chip/ChipProps.ts
index b021e0028e406d..9ef5020cf985e1 100644
--- a/packages/mui-joy/src/Chip/ChipProps.ts
+++ b/packages/mui-joy/src/Chip/ChipProps.ts
@@ -86,5 +86,5 @@ export interface ChipOwnerState extends ChipProps {
/**
* If `true`, the action slot's focus is visible.
*/
- focusVisible: boolean;
+ focusVisible?: boolean;
}
diff --git a/packages/mui-joy/src/ChipDelete/ChipDelete.tsx b/packages/mui-joy/src/ChipDelete/ChipDelete.tsx
index d4a708b8b323d4..ae77b64ae334c7 100644
--- a/packages/mui-joy/src/ChipDelete/ChipDelete.tsx
+++ b/packages/mui-joy/src/ChipDelete/ChipDelete.tsx
@@ -6,7 +6,7 @@ import { unstable_composeClasses as composeClasses, useButton } from '@mui/base'
import { useSlotProps } from '@mui/base/utils';
import { useThemeProps } from '../styles';
import styled from '../styles/styled';
-import Close from '../internal/svg-icons/Close';
+import Cancel from '../internal/svg-icons/Cancel';
import chipDeleteClasses, { getChipDeleteUtilityClass } from './chipDeleteClasses';
import { ChipDeleteProps, ChipDeleteOwnerState, ChipDeleteTypeMap } from './ChipDeleteProps';
import ChipContext from '../Chip/ChipContext';
@@ -113,7 +113,7 @@ const ChipDelete = React.forwardRef(function ChipDelete(inProps, ref) {
className: classes.root,
});
- return {children ?? };
+ return {children ?? };
}) as OverridableComponent;
ChipDelete.propTypes /* remove-proptypes */ = {
diff --git a/packages/mui-joy/src/ChipDelete/ChipDeleteProps.ts b/packages/mui-joy/src/ChipDelete/ChipDeleteProps.ts
index d9c7f0c6c9d9f1..efdb0a9d84b34f 100644
--- a/packages/mui-joy/src/ChipDelete/ChipDeleteProps.ts
+++ b/packages/mui-joy/src/ChipDelete/ChipDeleteProps.ts
@@ -45,5 +45,5 @@ export interface ChipDeleteOwnerState extends ChipDeleteProps {
/**
* If `true`, the element's focus is visible.
*/
- focusVisible: boolean;
+ focusVisible?: boolean;
}
diff --git a/packages/mui-joy/src/FormControl/FormControl.test.tsx b/packages/mui-joy/src/FormControl/FormControl.test.tsx
index 86b743fe277fdd..f01f1e41aabf5f 100644
--- a/packages/mui-joy/src/FormControl/FormControl.test.tsx
+++ b/packages/mui-joy/src/FormControl/FormControl.test.tsx
@@ -152,6 +152,18 @@ describe('', () => {
expect(getByLabelText('label')).toBeVisible();
});
+ it('should labeledby form label', () => {
+ const { container, getByRole } = render(
+
+ label
+
+ ,
+ );
+
+ const label = container.querySelector('label');
+ expect(getByRole('button')).to.have.attribute('aria-labelledby', label?.id);
+ });
+
it('should inherit color prop from FormControl', () => {
const { getByTestId } = render(
diff --git a/packages/mui-joy/src/FormControl/FormControl.tsx b/packages/mui-joy/src/FormControl/FormControl.tsx
index c1e797b5cbf7ff..b2bab374ed7c4d 100644
--- a/packages/mui-joy/src/FormControl/FormControl.tsx
+++ b/packages/mui-joy/src/FormControl/FormControl.tsx
@@ -32,6 +32,7 @@ export const FormControlRoot = styled('div', {
})<{ ownerState: FormControlOwnerState }>(({ theme, ownerState }) => ({
'--FormLabel-margin':
ownerState.orientation === 'horizontal' ? '0 0.375rem 0 0' : '0 0 0.25rem 0',
+ '--FormLabel-alignSelf': 'flex-start',
'--FormHelperText-margin': '0.375rem 0 0 0',
'--FormLabel-asterisk-color': theme.vars.palette.danger[500],
'--FormHelperText-color': theme.vars.palette[ownerState.color!]?.[500],
@@ -45,7 +46,7 @@ export const FormControlRoot = styled('div', {
}),
...(ownerState.size === 'lg' && {
'--FormLabel-fontSize': theme.vars.fontSize.md,
- '--FormHelperText-fontSize': theme.vars.fontSize.md,
+ '--FormHelperText-fontSize': theme.vars.fontSize.sm,
}),
[`&.${formControlClasses.error}`]: {
'--FormHelperText-color': theme.vars.palette.danger[500],
diff --git a/packages/mui-joy/src/FormLabel/FormLabel.tsx b/packages/mui-joy/src/FormLabel/FormLabel.tsx
index c979d0ae3dae47..1d0fb2b86f08f1 100644
--- a/packages/mui-joy/src/FormLabel/FormLabel.tsx
+++ b/packages/mui-joy/src/FormLabel/FormLabel.tsx
@@ -22,6 +22,8 @@ const FormLabelRoot = styled('label', {
slot: 'Root',
overridesResolver: (props, styles) => styles.root,
})<{ ownerState: FormLabelProps }>(({ theme }) => ({
+ WebkitTapHighlightColor: 'transparent',
+ alignSelf: 'var(--FormLabel-alignSelf)', // to not fill the block space. It seems like a bug when clicking on empty space (within the label area), even though it is not.
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
diff --git a/packages/mui-joy/src/IconButton/IconButton.tsx b/packages/mui-joy/src/IconButton/IconButton.tsx
index 6e809307157859..24d2f33d90a198 100644
--- a/packages/mui-joy/src/IconButton/IconButton.tsx
+++ b/packages/mui-joy/src/IconButton/IconButton.tsx
@@ -60,6 +60,7 @@ export const IconButtonRoot = styled('button', {
fontSize: theme.vars.fontSize.lg,
paddingInline: '0.375rem',
}),
+ WebkitTapHighlightColor: 'transparent',
paddingBlock: 0,
fontFamily: theme.vars.fontFamily.body,
fontWeight: theme.vars.fontWeight.md,
diff --git a/packages/mui-joy/src/IconButton/IconButtonProps.ts b/packages/mui-joy/src/IconButton/IconButtonProps.ts
index 3d47c1e9c3787a..09b624d4cfffd7 100644
--- a/packages/mui-joy/src/IconButton/IconButtonProps.ts
+++ b/packages/mui-joy/src/IconButton/IconButtonProps.ts
@@ -79,7 +79,7 @@ export interface IconButtonOwnerState extends IconButtonProps {
/**
* If `true`, the element's focus is visible.
*/
- focusVisible: boolean;
+ focusVisible?: boolean;
}
export type ExtendIconButton = ((
diff --git a/packages/mui-joy/src/Input/Input.tsx b/packages/mui-joy/src/Input/Input.tsx
index f23c56caceb21b..fc27605f5079ed 100644
--- a/packages/mui-joy/src/Input/Input.tsx
+++ b/packages/mui-joy/src/Input/Input.tsx
@@ -40,7 +40,7 @@ const InputRoot = styled('div', {
'--Input-radius': theme.vars.radius.sm,
'--Input-gap': '0.5rem',
'--Input-placeholderOpacity': 0.5,
- '--Input-focusedThickness': '2px',
+ '--Input-focusedThickness': theme.vars.focus.thickness,
'--Input-focusedHighlight':
theme.vars.palette[ownerState.color === 'neutral' ? 'primary' : ownerState.color!]?.[500],
...(ownerState.size === 'sm' && {
diff --git a/packages/mui-joy/src/Link/LinkProps.ts b/packages/mui-joy/src/Link/LinkProps.ts
index f5092930adacdc..00d53e19113da3 100644
--- a/packages/mui-joy/src/Link/LinkProps.ts
+++ b/packages/mui-joy/src/Link/LinkProps.ts
@@ -96,7 +96,7 @@ export interface LinkOwnerState extends LinkProps {
/**
* If `true`, the element's focus is visible.
*/
- focusVisible: boolean;
+ focusVisible?: boolean;
/**
* If `true`, the element is rendered by a Typography component.
*/
diff --git a/packages/mui-joy/src/List/List.tsx b/packages/mui-joy/src/List/List.tsx
index 53c68ce15491d7..a56e380e5106ce 100644
--- a/packages/mui-joy/src/List/List.tsx
+++ b/packages/mui-joy/src/List/List.tsx
@@ -125,6 +125,7 @@ export const ListRoot = styled('ul', {
}),
},
{
+ boxSizing: 'border-box',
borderRadius: 'var(--List-radius)',
listStyle: 'none',
display: 'flex',
diff --git a/packages/mui-joy/src/ListItemButton/ListItemButton.tsx b/packages/mui-joy/src/ListItemButton/ListItemButton.tsx
index 64e3319739e17a..79927efab9a184 100644
--- a/packages/mui-joy/src/ListItemButton/ListItemButton.tsx
+++ b/packages/mui-joy/src/ListItemButton/ListItemButton.tsx
@@ -51,6 +51,7 @@ export const ListItemButtonRoot = styled('div', {
'--List-decorator-color':
theme.vars.palette[ownerState.color!]?.[`${ownerState.variant!}DisabledColor`],
}),
+ WebkitTapHighlightColor: 'transparent',
boxSizing: 'border-box',
position: 'relative',
display: 'flex',
diff --git a/packages/mui-joy/src/ListItemButton/ListItemButtonProps.ts b/packages/mui-joy/src/ListItemButton/ListItemButtonProps.ts
index d51ec8b53522b8..ecbfdd857f73b5 100644
--- a/packages/mui-joy/src/ListItemButton/ListItemButtonProps.ts
+++ b/packages/mui-joy/src/ListItemButton/ListItemButtonProps.ts
@@ -98,12 +98,12 @@ export interface ListItemButtonOwnerState extends ListItemButtonProps {
/**
* If `true`, the element's focus is visible.
*/
- focusVisible: boolean;
+ focusVisible?: boolean;
/**
* If `true`, the element is rendered in a horizontal list.
* @internal
*/
- row: boolean;
+ row?: boolean;
/**
* @internal
* The internal prop for controlling CSS margin of the element.
diff --git a/packages/mui-joy/src/Menu/Menu.tsx b/packages/mui-joy/src/Menu/Menu.tsx
index 47176a18d851f1..f7af0a9b762eff 100644
--- a/packages/mui-joy/src/Menu/Menu.tsx
+++ b/packages/mui-joy/src/Menu/Menu.tsx
@@ -34,6 +34,7 @@ const MenuRoot = styled(ListRoot, {
})<{ ownerState: MenuOwnerState }>(({ theme, ownerState }) => {
const variantStyle = theme.variants[ownerState.variant!]?.[ownerState.color!];
return {
+ '--focus-outline-offset': `calc(${theme.vars.focus.thickness} * -1)`, // to prevent the focus outline from being cut by overflow
'--List-radius': theme.vars.radius.sm,
'--List-item-stickyBackground':
variantStyle?.backgroundColor ||
diff --git a/packages/mui-joy/src/MenuItem/MenuItem.test.js b/packages/mui-joy/src/MenuItem/MenuItem.test.js
index 61502467abe07a..e3fa6623c11482 100644
--- a/packages/mui-joy/src/MenuItem/MenuItem.test.js
+++ b/packages/mui-joy/src/MenuItem/MenuItem.test.js
@@ -50,6 +50,16 @@ describe('Joy ', () => {
skip: ['propsSpread', 'componentsProp', 'classesRoot', 'reactTestRenderer'],
}));
+ it('should render with the variant class', () => {
+ const { getByRole } = render();
+ expect(getByRole('menuitem')).to.have.class(classes.variantOutlined);
+ });
+
+ it('should render with primary color class', () => {
+ const { getByRole } = render();
+ expect(getByRole('menuitem')).to.have.class(classes.colorPrimary);
+ });
+
it('should render a focusable menuitem', () => {
render();
const menuitem = screen.getByRole('menuitem');
diff --git a/packages/mui-joy/src/MenuItem/MenuItem.tsx b/packages/mui-joy/src/MenuItem/MenuItem.tsx
index e062a32b52bb9e..7aeb20eeef5cb5 100644
--- a/packages/mui-joy/src/MenuItem/MenuItem.tsx
+++ b/packages/mui-joy/src/MenuItem/MenuItem.tsx
@@ -1,5 +1,6 @@
import * as React from 'react';
import PropTypes from 'prop-types';
+import { unstable_capitalize as capitalize } from '@mui/utils';
import composeClasses from '@mui/base/composeClasses';
import { useSlotProps } from '@mui/base/utils';
import { useMenuItem } from '@mui/base/MenuItemUnstyled';
@@ -14,12 +15,17 @@ import {
} from './MenuItemProps';
import RowListContext from '../List/RowListContext';
-const useUtilityClasses = (ownerState: MenuItemProps & { focusVisible: boolean }) => {
- const { focusVisible, disabled, selected } = ownerState;
- // Does not need to create state clases: focusVisible, disabled, and selected because ListItemButton already takes care of them.
- // Otherwise, there will be duplicated classes.
+const useUtilityClasses = (ownerState: MenuItemProps & { focusVisible?: boolean }) => {
+ const { focusVisible, disabled, selected, color, variant } = ownerState;
const slots = {
- root: ['root', focusVisible && 'focusVisible', disabled && 'disabled', selected && 'selected'],
+ root: [
+ 'root',
+ focusVisible && 'focusVisible',
+ disabled && 'disabled',
+ selected && 'selected',
+ color && `color${capitalize(color)}`,
+ variant && `variant${capitalize(variant)}`,
+ ],
};
const composedClasses = composeClasses(slots, getMenuItemUtilityClass, {});
@@ -98,7 +104,7 @@ MenuItem.propTypes /* remove-proptypes */ = {
* @default 'neutral'
*/
color: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([
- PropTypes.oneOf(['context', 'danger', 'info', 'neutral', 'primary', 'success', 'warning']),
+ PropTypes.oneOf(['danger', 'info', 'neutral', 'primary', 'success', 'warning']),
PropTypes.string,
]),
/**
diff --git a/packages/mui-joy/src/MenuItem/MenuItemProps.ts b/packages/mui-joy/src/MenuItem/MenuItemProps.ts
index 77255d77d2b66a..0a3c6e690b4c11 100644
--- a/packages/mui-joy/src/MenuItem/MenuItemProps.ts
+++ b/packages/mui-joy/src/MenuItem/MenuItemProps.ts
@@ -29,7 +29,7 @@ export interface MenuItemOwnerState extends MenuItemProps {
/**
* If `true`, the element's focus is visible.
*/
- focusVisible: boolean;
+ focusVisible?: boolean;
}
export type ExtendMenuItem = ((
diff --git a/packages/mui-joy/src/MenuItem/menuItemClasses.ts b/packages/mui-joy/src/MenuItem/menuItemClasses.ts
index 1242f88278f016..603847d79d2a76 100644
--- a/packages/mui-joy/src/MenuItem/menuItemClasses.ts
+++ b/packages/mui-joy/src/MenuItem/menuItemClasses.ts
@@ -9,6 +9,26 @@ export interface MenuItemClasses {
disabled: string;
/** State class applied to the root element if `selected={true}`. */
selected: 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;
+ /** State class applied to the root element if `variant="plain"`. */
+ variantPlain: string;
+ /** State class applied to the root element if `variant="soft"`. */
+ variantSoft: string;
+ /** State class applied to the root element if `variant="outlined"`. */
+ variantOutlined: string;
+ /** State class applied to the root element if `variant="solid"`. */
+ variantSolid: string;
}
export type MenuItemClassKey = keyof MenuItemClasses;
@@ -22,6 +42,16 @@ const menuItemClasses: MenuItemClasses = generateUtilityClasses('JoyMenuItem', [
'focusVisible',
'disabled',
'selected',
+ 'colorPrimary',
+ 'colorNeutral',
+ 'colorDanger',
+ 'colorInfo',
+ 'colorSuccess',
+ 'colorWarning',
+ 'variantPlain',
+ 'variantSoft',
+ 'variantOutlined',
+ 'variantSolid',
]);
export default menuItemClasses;
diff --git a/packages/mui-joy/src/MenuList/MenuList.tsx b/packages/mui-joy/src/MenuList/MenuList.tsx
index 03f24130d50cf2..e656ced8679774 100644
--- a/packages/mui-joy/src/MenuList/MenuList.tsx
+++ b/packages/mui-joy/src/MenuList/MenuList.tsx
@@ -32,6 +32,7 @@ const MenuListRoot = styled(ListRoot, {
})<{ ownerState: MenuListOwnerState }>(({ theme, ownerState }) => {
const variantStyle = theme.variants[ownerState.variant!]?.[ownerState.color!];
return {
+ '--focus-outline-offset': `calc(${theme.vars.focus.thickness} * -1)`, // to prevent the focus outline from being cut by overflow
'--List-radius': theme.vars.radius.sm,
'--List-item-stickyBackground':
variantStyle?.backgroundColor ||
diff --git a/packages/mui-joy/src/ModalClose/ModalClose.tsx b/packages/mui-joy/src/ModalClose/ModalClose.tsx
index da77ffde47e4cb..ea4e6ff67e1daf 100644
--- a/packages/mui-joy/src/ModalClose/ModalClose.tsx
+++ b/packages/mui-joy/src/ModalClose/ModalClose.tsx
@@ -14,7 +14,7 @@ import CloseModalContext from '../Modal/CloseModalContext';
import ModalDialogSizeContext from '../ModalDialog/ModalDialogSizeContext';
import ModalDialogVariantColorContext from '../ModalDialog/ModalDialogVariantColorContext';
-const useUtilityClasses = (ownerState: ModalCloseProps & { focusVisible: boolean }) => {
+const useUtilityClasses = (ownerState: ModalCloseProps & { focusVisible?: boolean }) => {
const { variant, color, disabled, focusVisible, size } = ownerState;
const slots = {
diff --git a/packages/mui-joy/src/Option/Option.tsx b/packages/mui-joy/src/Option/Option.tsx
index be460177edfef8..4538b5cb89802a 100644
--- a/packages/mui-joy/src/Option/Option.tsx
+++ b/packages/mui-joy/src/Option/Option.tsx
@@ -3,10 +3,9 @@ import PropTypes from 'prop-types';
import { unstable_useForkRef as useForkRef } from '@mui/utils';
import composeClasses from '@mui/base/composeClasses';
import { useSlotProps } from '@mui/base/utils';
-import { SelectUnstyledContext, SelectUnstyledContextType } from '@mui/base/SelectUnstyled';
+import { SelectUnstyledContext } from '@mui/base/SelectUnstyled';
import { ListItemButtonRoot } from '../ListItemButton/ListItemButton';
import { styled, useThemeProps } from '../styles';
-import { ColorPaletteProp } from '../styles/types';
import { OptionOwnerState, ExtendOption, OptionTypeMap } from './OptionProps';
import optionClasses, { getOptionUtilityClass } from './optionClasses';
import RowListContext from '../List/RowListContext';
@@ -44,14 +43,12 @@ const Option = React.forwardRef(function Option(inProps, ref) {
value,
label,
variant = 'plain',
- color: colorProp,
+ color: colorProp = 'neutral',
...other
} = props;
const row = React.useContext(RowListContext);
- const selectContext = React.useContext(SelectUnstyledContext) as SelectUnstyledContextType & {
- color: ColorPaletteProp;
- };
+ const selectContext = React.useContext(SelectUnstyledContext);
if (!selectContext) {
throw new Error('OptionUnstyled must be used within a SelectUnstyled');
@@ -67,13 +64,11 @@ const Option = React.forwardRef(function Option(inProps, ref) {
const optionProps = selectContext.getOptionProps(selectOption);
const listboxRef = selectContext.listboxRef;
- let color: typeof colorProp = 'neutral';
- if (optionState.selected) {
- color = selectContext.color === 'neutral' ? 'primary' : selectContext.color;
- }
- if (colorProp) {
- color = colorProp;
+ let color = colorProp;
+ if (optionState.selected && !inProps.color) {
+ color = 'primary';
}
+
const ownerState = {
...props,
...optionState,
diff --git a/packages/mui-joy/src/Radio/RadioProps.ts b/packages/mui-joy/src/Radio/RadioProps.ts
index d3fb2ae8d773c1..23a6ff1cb870fa 100644
--- a/packages/mui-joy/src/Radio/RadioProps.ts
+++ b/packages/mui-joy/src/Radio/RadioProps.ts
@@ -103,7 +103,7 @@ export interface RadioOwnerState extends RadioProps {
/**
* If `true`, the element's focus is visible.
*/
- focusVisible: boolean;
+ focusVisible?: boolean;
/**
* @internal
* The value from the RadioGroup component.
diff --git a/packages/mui-joy/src/Select/Select.tsx b/packages/mui-joy/src/Select/Select.tsx
index a79673b79f83aa..641dcc4da97bf0 100644
--- a/packages/mui-joy/src/Select/Select.tsx
+++ b/packages/mui-joy/src/Select/Select.tsx
@@ -67,10 +67,11 @@ const SelectRoot = styled('div', {
const variantStyle = theme.variants[`${ownerState.variant!}`]?.[ownerState.color!];
return [
{
+ '--focus-outline-offset': `calc(${theme.vars.focus.thickness} * -1)`, // to prevent the focus outline from being cut by overflow
'--Select-radius': theme.vars.radius.sm,
'--Select-gap': '0.5rem',
'--Select-placeholderOpacity': 0.5,
- '--Select-focusedThickness': '2px',
+ '--Select-focusedThickness': theme.vars.focus.thickness,
'--Select-focusedHighlight':
theme.vars.palette[ownerState.color === 'neutral' ? 'primary' : ownerState.color!]?.[500],
'--Select-indicator-color': theme.vars.palette.text.tertiary,
@@ -241,13 +242,25 @@ const SelectEndDecorator = styled('span', {
const SelectIndicator = styled('span', {
name: 'JoySelect',
slot: 'Indicator',
-})<{ ownerState: SelectOwnerState }>({
+})<{ ownerState: SelectOwnerState }>(({ ownerState }) => ({
+ ...(ownerState.size === 'sm' && {
+ '--Icon-fontSize': '1.125rem',
+ }),
+ ...(ownerState.size === 'md' && {
+ '--Icon-fontSize': '1.25rem',
+ }),
+ ...(ownerState.size === 'lg' && {
+ '--Icon-fontSize': '1.5rem',
+ }),
color: 'var(--Select-indicator-color)',
display: 'inherit',
alignItems: 'center',
marginInlineStart: 'var(--Select-gap)',
marginInlineEnd: 'calc(var(--Select-paddingInline) / -4)',
-});
+ [`.${selectClasses.endDecorator} + &`]: {
+ marginInlineStart: 'calc(var(--Select-gap) / 2)',
+ },
+}));
const Select = React.forwardRef(function Select(
inProps: SelectOwnProps,
@@ -434,7 +447,7 @@ const Select = React.forwardRef(function Select(
additionalProps: {
'aria-describedby': ariaDescribedby ?? formControl?.['aria-describedby'],
'aria-label': ariaLabel,
- 'aria-labelledby': ariaLabelledby,
+ 'aria-labelledby': ariaLabelledby ?? formControl?.labelId,
id: id ?? formControl?.htmlFor,
name,
},
diff --git a/packages/mui-joy/src/Select/SelectProps.ts b/packages/mui-joy/src/Select/SelectProps.ts
index 94ab89926d99c7..134f345ff529a2 100644
--- a/packages/mui-joy/src/Select/SelectProps.ts
+++ b/packages/mui-joy/src/Select/SelectProps.ts
@@ -131,7 +131,7 @@ export interface SelectOwnerState extends SelectOwnProps', () => {
expect(root.childNodes[0]).to.have.property('tagName', 'SPAN');
expect(root.childNodes[0]).to.have.class(classes.rail);
});
+
+ it('should show formatted label', () => {
+ const { getByText } = render(
+ `${value}px`} />,
+ );
+
+ expect(getByText('10px')).toBeVisible();
+ });
});
diff --git a/packages/mui-joy/src/Slider/Slider.tsx b/packages/mui-joy/src/Slider/Slider.tsx
index 3e06c6ca701ba1..6154087f2db885 100644
--- a/packages/mui-joy/src/Slider/Slider.tsx
+++ b/packages/mui-joy/src/Slider/Slider.tsx
@@ -598,7 +598,9 @@ const Slider = React.forwardRef(function Slider(inProps, ref) {
open === index || active === index || valueLabelDisplay === 'on',
})}
>
- {value}
+ {typeof valueLabelFormat === 'function'
+ ? valueLabelFormat(scale(value), index)
+ : valueLabelFormat}
) : null}
diff --git a/packages/mui-joy/src/Switch/SwitchProps.ts b/packages/mui-joy/src/Switch/SwitchProps.ts
index e4477063d750b5..a01eec6d904ddc 100644
--- a/packages/mui-joy/src/Switch/SwitchProps.ts
+++ b/packages/mui-joy/src/Switch/SwitchProps.ts
@@ -70,5 +70,5 @@ export interface SwitchOwnerState extends SwitchProps {
/**
* If `true`, the switch's focus is visible.
*/
- focusVisible: boolean;
+ focusVisible?: boolean;
}
diff --git a/packages/mui-joy/src/Tab/Tab.test.tsx b/packages/mui-joy/src/Tab/Tab.test.tsx
index d4d692918f9a0d..c131df1ce5ad38 100644
--- a/packages/mui-joy/src/Tab/Tab.test.tsx
+++ b/packages/mui-joy/src/Tab/Tab.test.tsx
@@ -22,6 +22,7 @@ describe('Joy ', () => {
muiName: 'JoyTab',
refInstanceof: window.HTMLButtonElement,
testVariantProps: { variant: 'solid' },
+ testCustomVariant: true,
skip: ['componentsProp', 'classesRoot', 'reactTestRenderer'],
}));
diff --git a/packages/mui-joy/src/Tab/Tab.tsx b/packages/mui-joy/src/Tab/Tab.tsx
index 88ff2fe77927bc..346e499181445f 100644
--- a/packages/mui-joy/src/Tab/Tab.tsx
+++ b/packages/mui-joy/src/Tab/Tab.tsx
@@ -43,7 +43,7 @@ const TabRoot = styled(ListItemButtonRoot, {
...(ownerState.selected && {
boxShadow: theme.vars.shadow.sm,
fontWeight: 'initial',
- ...(!variantStyle.backgroundColor && {
+ ...(!variantStyle?.backgroundColor && {
backgroundColor: theme.vars.palette.background.body,
'&:hover': {
backgroundColor: theme.vars.palette.background.body,
diff --git a/packages/mui-joy/src/Tab/TabProps.ts b/packages/mui-joy/src/Tab/TabProps.ts
index 77acff62b4a799..5c61bec704e315 100644
--- a/packages/mui-joy/src/Tab/TabProps.ts
+++ b/packages/mui-joy/src/Tab/TabProps.ts
@@ -65,7 +65,7 @@ export interface TabOwnerState extends TabProps {
/**
* If `true`, the tab's focus is visible.
*/
- focusVisible: boolean;
+ focusVisible?: boolean;
/**
* If `true`, the tab is disabled.
*/
diff --git a/packages/mui-joy/src/TabPanel/TabPanel.tsx b/packages/mui-joy/src/TabPanel/TabPanel.tsx
index 575b1693651f87..db00b9ed3a6333 100644
--- a/packages/mui-joy/src/TabPanel/TabPanel.tsx
+++ b/packages/mui-joy/src/TabPanel/TabPanel.tsx
@@ -43,6 +43,7 @@ const TabPanelRoot = styled('div', {
fontSize: theme.vars.fontSize.lg,
}),
flexGrow: 1,
+ fontFamily: theme.vars.fontFamily.body,
}));
const TabPanel = React.forwardRef(function TabPanel(inProps, ref) {
diff --git a/packages/mui-joy/src/Textarea/Textarea.tsx b/packages/mui-joy/src/Textarea/Textarea.tsx
index 752ea710717a46..7c0d7a0426d21d 100644
--- a/packages/mui-joy/src/Textarea/Textarea.tsx
+++ b/packages/mui-joy/src/Textarea/Textarea.tsx
@@ -40,7 +40,7 @@ const TextareaRoot = styled('div', {
'--Textarea-radius': theme.vars.radius.sm,
'--Textarea-gap': '0.5rem',
'--Textarea-placeholderOpacity': 0.5,
- '--Textarea-focusedThickness': '2px',
+ '--Textarea-focusedThickness': theme.vars.focus.thickness,
'--Textarea-focusedHighlight':
theme.vars.palette[ownerState.color === 'neutral' ? 'primary' : ownerState.color!]?.[500],
...(ownerState.size === 'sm' && {
diff --git a/packages/mui-joy/src/internal/svg-icons/Cancel.tsx b/packages/mui-joy/src/internal/svg-icons/Cancel.tsx
new file mode 100644
index 00000000000000..c7d894363dcf6f
--- /dev/null
+++ b/packages/mui-joy/src/internal/svg-icons/Cancel.tsx
@@ -0,0 +1,10 @@
+import * as React from 'react';
+import createSvgIcon from '../../utils/createSvgIcon';
+
+/**
+ * @ignore - internal component.
+ */
+export default createSvgIcon(
+ ,
+ 'Cancel',
+);
diff --git a/packages/mui-joy/src/styles/CssVarsProvider.test.tsx b/packages/mui-joy/src/styles/CssVarsProvider.test.tsx
index 9376e3e631bbb2..0c3cf89a27bbce 100644
--- a/packages/mui-joy/src/styles/CssVarsProvider.test.tsx
+++ b/packages/mui-joy/src/styles/CssVarsProvider.test.tsx
@@ -1,7 +1,7 @@
import * as React from 'react';
import { expect } from 'chai';
import { createRenderer, screen } from 'test/utils';
-import { CssVarsProvider, useTheme } from '@mui/joy/styles';
+import { CssVarsProvider, useTheme, shouldSkipGeneratingVar } from '@mui/joy/styles';
describe('[Joy] CssVarsProvider', () => {
let originalMatchmedia: typeof window.matchMedia;
@@ -29,6 +29,26 @@ describe('[Joy] CssVarsProvider', () => {
window.matchMedia = originalMatchmedia;
});
+ describe('shouldSkipGeneratingVar', () => {
+ it('skip typography', () => {
+ expect(shouldSkipGeneratingVar(['typography'])).to.equal(true);
+ });
+
+ it('skip variants', () => {
+ expect(shouldSkipGeneratingVar(['variants'])).to.equal(true);
+ });
+
+ it('skip breakpoints', () => {
+ expect(shouldSkipGeneratingVar(['breakpoints'])).to.equal(true);
+ });
+
+ it('skip focus', () => {
+ expect(shouldSkipGeneratingVar(['focus'])).to.equal(true);
+ expect(shouldSkipGeneratingVar(['focus', 'selector'])).to.equal(true);
+ expect(shouldSkipGeneratingVar(['focus', 'thickness'])).to.equal(false);
+ });
+ });
+
describe('All CSS vars', () => {
it('palette', () => {
const Vars = () => {
@@ -476,7 +496,7 @@ describe('[Joy] CssVarsProvider', () => {
,
);
- expect(container.firstChild?.textContent).to.equal('selector,default');
+ expect(container.firstChild?.textContent).to.equal('thickness,selector,default');
});
});
@@ -606,11 +626,10 @@ describe('[Joy] CssVarsProvider', () => {
expect(container.firstChild?.textContent).not.to.equal('typography');
});
- it('should not contain `focus` in theme.vars', () => {
+ it('should contain only `focus.thickness` in theme.vars', () => {
const Consumer = () => {
const theme = useTheme();
- // @ts-expect-error
- return {theme.vars.focus ? 'focus' : ''}
;
+ return {JSON.stringify(theme.vars.focus)}
;
};
const { container } = render(
@@ -619,7 +638,9 @@ describe('[Joy] CssVarsProvider', () => {
,
);
- expect(container.firstChild?.textContent).not.to.equal('focus');
+ expect(container.firstChild?.textContent).not.to.equal(
+ JSON.stringify({ focus: { thickness: '2px' } }),
+ );
});
});
});
diff --git a/packages/mui-joy/src/styles/CssVarsProvider.tsx b/packages/mui-joy/src/styles/CssVarsProvider.tsx
index 48d405cd6bbc41..55c61211c4e093 100644
--- a/packages/mui-joy/src/styles/CssVarsProvider.tsx
+++ b/packages/mui-joy/src/styles/CssVarsProvider.tsx
@@ -3,7 +3,8 @@ import extendTheme from './extendTheme';
import type { DefaultColorScheme, ExtendedColorScheme } from './types';
const shouldSkipGeneratingVar = (keys: string[]) =>
- !!keys[0].match(/(typography|variants|focus|breakpoints)/);
+ !!keys[0].match(/(typography|variants|breakpoints)/) ||
+ (keys[0] === 'focus' && keys[1] !== 'thickness');
const { CssVarsProvider, useColorScheme, getInitColorSchemeScript } = createCssVarsProvider<
DefaultColorScheme | ExtendedColorScheme
diff --git a/packages/mui-joy/src/styles/defaultTheme.ts b/packages/mui-joy/src/styles/defaultTheme.ts
index 5f162c2b539d71..79b8c71965ddcc 100644
--- a/packages/mui-joy/src/styles/defaultTheme.ts
+++ b/packages/mui-joy/src/styles/defaultTheme.ts
@@ -8,6 +8,7 @@ export const getThemeWithVars = (
) => {
const {
colorSchemes,
+ focus,
fontFamily,
fontSize,
fontWeight,
@@ -29,6 +30,7 @@ export const getThemeWithVars = (
} = colorSchemePalette as RuntimeColorSystem['palette'];
return {
+ focus,
fontFamily,
fontSize,
fontWeight,
@@ -46,7 +48,17 @@ export const getThemeWithVars = (
mode,
colorScheme,
},
- vars: { fontFamily, fontSize, fontWeight, letterSpacing, lineHeight, radius, shadow, palette },
+ vars: {
+ focus,
+ fontFamily,
+ fontSize,
+ fontWeight,
+ letterSpacing,
+ lineHeight,
+ radius,
+ shadow,
+ palette,
+ },
} as unknown as Theme;
};
diff --git a/packages/mui-joy/src/styles/extendTheme.ts b/packages/mui-joy/src/styles/extendTheme.ts
index 89efee18fdfbf5..2bc8df32d808a4 100644
--- a/packages/mui-joy/src/styles/extendTheme.ts
+++ b/packages/mui-joy/src/styles/extendTheme.ts
@@ -236,7 +236,7 @@ export default function extendTheme(themeOptions?: CssVarsThemeOptions): Theme {
backdrop: 'rgba(255 255 255 / 0.5)',
},
divider: `rgba(${getCssVar('palette-neutral-mainChannel')} / 0.28)`,
- focusVisible: getCssVar('palette-primary-200'),
+ focusVisible: getCssVar('palette-primary-500'),
},
shadowRing: '0 0 #000',
shadowChannel: '187 187 187',
@@ -369,10 +369,11 @@ export default function extendTheme(themeOptions?: CssVarsThemeOptions): Theme {
xl3: 900,
},
focus: {
+ thickness: '2px',
selector: `&.${generateUtilityClass('', 'focusVisible')}, &:focus-visible`,
default: {
- outlineOffset: getCssVar('focus-outlineOffset', '0px'), // reset user agent stylesheet
- outline: `4px solid ${getCssVar('palette-focusVisible')}`,
+ outlineOffset: `var(--focus-outline-offset, ${getCssVar('focus-thickness')})`,
+ outline: `${getCssVar('focus-thickness')} solid ${getCssVar('palette-focusVisible')}`,
},
},
lineHeight: {
@@ -607,7 +608,7 @@ export default function extendTheme(themeOptions?: CssVarsThemeOptions): Theme {
const channelMapping = {
// Need type casting due to module augmentation inside the repo
main: '500' as keyof PaletteRange,
- light: '100' as keyof PaletteRange,
+ light: '200' as keyof PaletteRange,
dark: '900' as keyof PaletteRange,
};
if (!palette[key].mainChannel && palette[key][channelMapping.main]) {
diff --git a/packages/mui-joy/src/styles/types/focus.ts b/packages/mui-joy/src/styles/types/focus.ts
index 6ceef95be021be..43a7699d08b36a 100644
--- a/packages/mui-joy/src/styles/types/focus.ts
+++ b/packages/mui-joy/src/styles/types/focus.ts
@@ -1,6 +1,7 @@
import { CSSObject } from '@mui/system';
export interface Focus {
+ thickness: string;
selector: string;
default: CSSObject;
}
diff --git a/packages/mui-joy/src/styles/types/theme.ts b/packages/mui-joy/src/styles/types/theme.ts
index 3c75addbcaa767..f278f59b7d7ae0 100644
--- a/packages/mui-joy/src/styles/types/theme.ts
+++ b/packages/mui-joy/src/styles/types/theme.ts
@@ -51,6 +51,7 @@ export interface RuntimeColorSystem extends Omit {
export interface ThemeScales {
radius: Radius;
shadow: Shadow;
+ focus: { thickness: string };
fontFamily: FontFamily;
fontSize: FontSize;
fontWeight: FontWeight;