;
+
+AutocompleteListbox.propTypes /* remove-proptypes */ = {
+ // ----------------------------- Warning --------------------------------
+ // | These PropTypes are generated from the TypeScript type definitions |
+ // | To update them edit TypeScript types and run "yarn proptypes" |
+ // ----------------------------------------------------------------------
+ /**
+ * @ignore
+ */
+ children: PropTypes.node,
+ /**
+ * The color of the component. It supports those theme colors that make sense for this component.
+ * @default 'neutral'
+ */
+ color: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([
+ PropTypes.oneOf(['danger', 'info', 'neutral', 'primary', 'success', 'warning']),
+ PropTypes.string,
+ ]),
+ /**
+ * The component used for the root node.
+ * Either a string to use a HTML element or a component.
+ */
+ component: PropTypes.elementType,
+ /**
+ * The size of the component (affect other nested list* components).
+ * @default 'md'
+ */
+ size: PropTypes.oneOf(['sm', 'md', 'lg']),
+ /**
+ * The system prop that allows defining system overrides as well as additional CSS styles.
+ */
+ sx: PropTypes.oneOfType([
+ PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])),
+ PropTypes.func,
+ PropTypes.object,
+ ]),
+ /**
+ * The variant to use.
+ * @default 'outlined'
+ */
+ variant: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([
+ PropTypes.oneOf(['contained', 'light', 'outlined', 'text']),
+ PropTypes.string,
+ ]),
+} as any;
+
+export default AutocompleteListbox;
diff --git a/packages/mui-joy/src/AutocompleteListbox/AutocompleteListboxProps.ts b/packages/mui-joy/src/AutocompleteListbox/AutocompleteListboxProps.ts
new file mode 100644
index 00000000000000..d48cfd219c1d0a
--- /dev/null
+++ b/packages/mui-joy/src/AutocompleteListbox/AutocompleteListboxProps.ts
@@ -0,0 +1,40 @@
+import * as React from 'react';
+import { OverrideProps } from '@mui/types';
+import { ListProps } from '../List/ListProps';
+import { SxProps } from '../styles/types';
+
+export type AutocompleteListboxSlot = 'root';
+
+export interface AutocompleteListboxTypeMap {
+ props: P & {
+ /**
+ * The color of the component. It supports those theme colors that make sense for this component.
+ * @default 'neutral'
+ */
+ color?: ListProps['color'];
+ /**
+ * The variant to use.
+ * @default 'outlined'
+ */
+ variant?: ListProps['variant'];
+ /**
+ * The size of the component (affect other nested list* components).
+ * @default 'md'
+ */
+ size?: ListProps['size'];
+ /**
+ * The system prop that allows defining system overrides as well as additional CSS styles.
+ */
+ sx?: SxProps;
+ };
+ defaultComponent: D;
+}
+
+export type AutocompleteListboxProps<
+ D extends React.ElementType = AutocompleteListboxTypeMap['defaultComponent'],
+ P = {
+ component?: React.ElementType;
+ },
+> = OverrideProps, D>;
+
+export interface AutocompleteListboxOwnerState extends AutocompleteListboxProps {}
diff --git a/packages/mui-joy/src/AutocompleteListbox/autocompleteListboxClasses.ts b/packages/mui-joy/src/AutocompleteListbox/autocompleteListboxClasses.ts
new file mode 100644
index 00000000000000..d3fa27a8717fe7
--- /dev/null
+++ b/packages/mui-joy/src/AutocompleteListbox/autocompleteListboxClasses.ts
@@ -0,0 +1,60 @@
+import { generateUtilityClass, generateUtilityClasses } from '../className';
+
+export interface AutocompleteListboxClasses {
+ /** Styles applied to the root element. */
+ root: string;
+ /** Classname applied to the root element if `size="sm"`. */
+ sizeSm: string;
+ /** Classname applied to the root element if `size="md"`. */
+ sizeMd: string;
+ /** Classname 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 AutocompleteListboxClassKey = keyof AutocompleteListboxClasses;
+
+export function getAutocompleteListboxUtilityClass(slot: string): string {
+ return generateUtilityClass('JoyAutocompleteListbox', slot);
+}
+
+const autocompleteListboxClasses: AutocompleteListboxClasses = generateUtilityClasses(
+ 'JoyAutocompleteListbox',
+ [
+ 'root',
+ 'sizeSm',
+ 'sizeMd',
+ 'sizeLg',
+ 'colorPrimary',
+ 'colorNeutral',
+ 'colorDanger',
+ 'colorInfo',
+ 'colorSuccess',
+ 'colorWarning',
+ 'variantPlain',
+ 'variantOutlined',
+ 'variantSoft',
+ 'variantSolid',
+ ],
+);
+
+export default autocompleteListboxClasses;
diff --git a/packages/mui-joy/src/AutocompleteListbox/index.ts b/packages/mui-joy/src/AutocompleteListbox/index.ts
new file mode 100644
index 00000000000000..0fc430b15af660
--- /dev/null
+++ b/packages/mui-joy/src/AutocompleteListbox/index.ts
@@ -0,0 +1,4 @@
+export { default } from './AutocompleteListbox';
+export * from './autocompleteListboxClasses';
+export { default as autocompleteListboxClasses } from './autocompleteListboxClasses';
+export * from './AutocompleteListboxProps';
diff --git a/packages/mui-joy/src/AutocompleteOption/AutocompleteOption.test.js b/packages/mui-joy/src/AutocompleteOption/AutocompleteOption.test.js
new file mode 100644
index 00000000000000..205980e72aa61f
--- /dev/null
+++ b/packages/mui-joy/src/AutocompleteOption/AutocompleteOption.test.js
@@ -0,0 +1,43 @@
+import * as React from 'react';
+import { expect } from 'chai';
+import { describeConformance, createRenderer } from 'test/utils';
+import { ThemeProvider } from '@mui/joy/styles';
+import AutocompleteOption, {
+ autocompleteOptionClasses as classes,
+} from '@mui/joy/AutocompleteOption';
+
+describe('Joy ', () => {
+ const { render } = createRenderer();
+
+ describeConformance(, () => ({
+ classes,
+ inheritComponent: 'li',
+ render,
+ ThemeProvider,
+ muiName: 'JoyAutocompleteOption',
+ refInstanceof: window.HTMLLIElement,
+ testVariantProps: { color: 'primary' },
+ testCustomVariant: true,
+ skip: ['componentsProp', 'classesRoot'],
+ }));
+
+ it('should have li tag', () => {
+ const { getByRole } = render();
+ expect(getByRole('option')).to.have.tagName('li');
+ });
+
+ it('should render with the variant class', () => {
+ const { getByRole } = render();
+ expect(getByRole('option')).to.have.class(classes.variantOutlined);
+ });
+
+ it('should render with primary color class', () => {
+ const { getByRole } = render();
+ expect(getByRole('option')).to.have.class(classes.colorPrimary);
+ });
+
+ it('should accept className prop', () => {
+ const { container } = render();
+ expect(container.firstChild).to.have.class('foo-bar');
+ });
+});
diff --git a/packages/mui-joy/src/AutocompleteOption/AutocompleteOption.tsx b/packages/mui-joy/src/AutocompleteOption/AutocompleteOption.tsx
new file mode 100644
index 00000000000000..b54098681d529d
--- /dev/null
+++ b/packages/mui-joy/src/AutocompleteOption/AutocompleteOption.tsx
@@ -0,0 +1,135 @@
+import * as React from 'react';
+import clsx from 'clsx';
+import PropTypes from 'prop-types';
+import { OverridableComponent } from '@mui/types';
+import { unstable_capitalize as capitalize } from '@mui/utils';
+import composeClasses from '@mui/base/composeClasses';
+import { StyledListItemButton } from '../ListItemButton/ListItemButton';
+import { styled, useThemeProps } from '../styles';
+import autocompleteOptionClasses, {
+ getAutocompleteOptionUtilityClass,
+} from './autocompleteOptionClasses';
+import { AutocompleteOptionOwnerState, AutocompleteOptionTypeMap } from './AutocompleteOptionProps';
+
+const useUtilityClasses = (ownerState: AutocompleteOptionOwnerState) => {
+ const { color, variant } = ownerState;
+
+ const slots = {
+ root: [
+ 'root',
+ color && `color${capitalize(color)}`,
+ variant && `variant${capitalize(variant)}`,
+ ],
+ };
+
+ return composeClasses(slots, getAutocompleteOptionUtilityClass, {});
+};
+
+export const StyledAutocompleteOption = styled(StyledListItemButton as unknown as 'li')<{
+ ownerState: AutocompleteOptionOwnerState;
+}>(({ theme, ownerState }) => ({
+ '&:not(:hover)': {
+ transition: 'none', // prevent flicker when using keyboard arrows to move between options
+ },
+ '&[aria-disabled="true"]': theme.variants[`${ownerState.variant!}Disabled`]?.[ownerState.color!],
+ '&[aria-selected="true"]': {
+ color: theme.vars.palette.primary.softColor,
+ backgroundColor: theme.vars.palette.primary.softBg,
+ fontWeight: theme.vars.fontWeight.md,
+ },
+ [`&.${autocompleteOptionClasses.focused}:not([aria-selected="true"]):not(:hover)`]: {
+ // create the focused style similar to the hover state
+ backgroundColor:
+ theme.variants[`${ownerState.variant!}Hover`]?.[ownerState.color!]?.backgroundColor,
+ },
+}));
+
+const AutocompleteOptionRoot = styled(StyledAutocompleteOption, {
+ name: 'JoyAutocompleteOption',
+ slot: 'Root',
+ overridesResolver: (props, styles) => styles.root,
+})({});
+
+const AutocompleteOption = React.forwardRef(function AutocompleteOption(inProps, ref) {
+ const props = useThemeProps({
+ props: inProps,
+ name: 'JoyAutocompleteOption',
+ });
+
+ const {
+ children,
+ component = 'li',
+ color = 'neutral',
+ variant = 'plain',
+ className,
+ ...other
+ } = props;
+
+ const ownerState = {
+ ...props,
+ component,
+ color,
+ variant,
+ };
+
+ const classes = useUtilityClasses(ownerState);
+
+ return (
+
+ {children}
+
+ );
+}) as OverridableComponent;
+
+AutocompleteOption.propTypes /* remove-proptypes */ = {
+ // ----------------------------- Warning --------------------------------
+ // | These PropTypes are generated from the TypeScript type definitions |
+ // | To update them edit TypeScript types and run "yarn proptypes" |
+ // ----------------------------------------------------------------------
+ /**
+ * @ignore
+ */
+ children: PropTypes.node,
+ /**
+ * @ignore
+ */
+ className: PropTypes.string,
+ /**
+ * The color of the component. It supports those theme colors that make sense for this component.
+ * @default 'neutral'
+ */
+ color: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([
+ PropTypes.oneOf(['danger', 'info', 'neutral', 'primary', 'success', 'warning']),
+ PropTypes.string,
+ ]),
+ /**
+ * The component used for the root node.
+ * Either a string to use a HTML element or a component.
+ */
+ component: PropTypes.elementType,
+ /**
+ * 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 /* @typescript-to-proptypes-ignore */.oneOfType([
+ PropTypes.oneOf(['contained', 'light', 'outlined', 'text']),
+ PropTypes.string,
+ ]),
+} as any;
+
+export default AutocompleteOption;
diff --git a/packages/mui-joy/src/AutocompleteOption/AutocompleteOptionProps.ts b/packages/mui-joy/src/AutocompleteOption/AutocompleteOptionProps.ts
new file mode 100644
index 00000000000000..4d2e672dc482b4
--- /dev/null
+++ b/packages/mui-joy/src/AutocompleteOption/AutocompleteOptionProps.ts
@@ -0,0 +1,35 @@
+import * as React from 'react';
+import { OverrideProps } from '@mui/types';
+import { ListItemButtonProps } from '../ListItemButton/ListItemButtonProps';
+import { SxProps } from '../styles/types';
+
+export type AutocompleteOptionSlot = 'root';
+
+export interface AutocompleteOptionTypeMap {
+ props: P & {
+ /**
+ * The color of the component. It supports those theme colors that make sense for this component.
+ * @default 'neutral'
+ */
+ color?: ListItemButtonProps['color'];
+ /**
+ * The variant to use.
+ * @default 'plain'
+ */
+ variant?: ListItemButtonProps['variant'];
+ /**
+ * The system prop that allows defining system overrides as well as additional CSS styles.
+ */
+ sx?: SxProps;
+ };
+ defaultComponent: D;
+}
+
+export type AutocompleteOptionProps<
+ D extends React.ElementType = AutocompleteOptionTypeMap['defaultComponent'],
+ P = {
+ component?: React.ElementType;
+ },
+> = OverrideProps, D>;
+
+export interface AutocompleteOptionOwnerState extends AutocompleteOptionProps {}
diff --git a/packages/mui-joy/src/AutocompleteOption/autocompleteOptionClasses.ts b/packages/mui-joy/src/AutocompleteOption/autocompleteOptionClasses.ts
new file mode 100644
index 00000000000000..711ee09fc73203
--- /dev/null
+++ b/packages/mui-joy/src/AutocompleteOption/autocompleteOptionClasses.ts
@@ -0,0 +1,57 @@
+import { generateUtilityClass, generateUtilityClasses } from '../className';
+
+export interface AutocompleteOptionClasses {
+ /** Styles applied to the root element. */
+ root: string;
+ /** State class applied to the root element if focused. */
+ focused: string;
+ /** State class applied to the `component`'s `focusVisibleClassName` prop. */
+ focusVisible: 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 AutocompleteOptionClassKey = keyof AutocompleteOptionClasses;
+
+export function getAutocompleteOptionUtilityClass(slot: string): string {
+ return generateUtilityClass('JoyAutocompleteOption', slot);
+}
+
+const autocompleteOptionClasses: AutocompleteOptionClasses = generateUtilityClasses(
+ 'JoyAutocompleteOption',
+ [
+ 'root',
+ 'focused',
+ 'focusVisible',
+ 'colorPrimary',
+ 'colorNeutral',
+ 'colorDanger',
+ 'colorInfo',
+ 'colorSuccess',
+ 'colorWarning',
+ 'variantPlain',
+ 'variantSoft',
+ 'variantOutlined',
+ 'variantSolid',
+ ],
+);
+
+export default autocompleteOptionClasses;
diff --git a/packages/mui-joy/src/AutocompleteOption/index.ts b/packages/mui-joy/src/AutocompleteOption/index.ts
new file mode 100644
index 00000000000000..dda31b68a6b501
--- /dev/null
+++ b/packages/mui-joy/src/AutocompleteOption/index.ts
@@ -0,0 +1,4 @@
+export { default } from './AutocompleteOption';
+export * from './autocompleteOptionClasses';
+export { default as autocompleteOptionClasses } from './autocompleteOptionClasses';
+export * from './AutocompleteOptionProps';
diff --git a/packages/mui-joy/src/Chip/Chip.tsx b/packages/mui-joy/src/Chip/Chip.tsx
index 9a58948fca42c8..93ca70b547a065 100644
--- a/packages/mui-joy/src/Chip/Chip.tsx
+++ b/packages/mui-joy/src/Chip/Chip.tsx
@@ -39,48 +39,48 @@ const ChipRoot = styled('div', {
})<{ ownerState: ChipOwnerState }>(({ theme, ownerState }) => {
return [
{
- '--Chip-radius': '1.5rem',
// for controlling chip delete margin offset
'--Chip-decorator-childOffset':
- 'min(calc(var(--Chip-paddingInline) - (var(--Chip-minHeight) - 2 * var(--variant-borderWidth) - var(--Chip-decorator-childHeight)) / 2), var(--Chip-paddingInline))',
- '--internal-paddingBlock':
- 'max((var(--Chip-minHeight) - 2 * var(--variant-borderWidth) - var(--Chip-decorator-childHeight)) / 2, 0px)',
+ 'min(calc(var(--Chip-paddingInline) - (var(--_Chip-minHeight) - 2 * var(--variant-borderWidth) - var(--Chip-decorator-childHeight)) / 2), var(--Chip-paddingInline))',
'--Chip-decorator-childRadius':
- 'max(var(--Chip-radius) - var(--internal-paddingBlock), min(var(--internal-paddingBlock) / 2, var(--Chip-radius) / 2))',
+ 'max(var(--_Chip-radius) - var(--_Chip-paddingBlock), min(var(--_Chip-paddingBlock) / 2, var(--_Chip-radius) / 2))',
'--Chip-delete-radius': 'var(--Chip-decorator-childRadius)',
'--Chip-delete-size': 'var(--Chip-decorator-childHeight)',
'--Avatar-radius': 'var(--Chip-decorator-childRadius)',
'--Avatar-size': 'var(--Chip-decorator-childHeight)',
'--Icon-margin': 'initial', // reset the icon's margin.
- '--internal-action-radius': 'var(--Chip-radius)', // to be used with Radio or Checkbox
+ '--internal-action-radius': 'var(--_Chip-radius)', // to be used with Radio or Checkbox
...(ownerState.size === 'sm' && {
'--Chip-gap': '0.25rem',
'--Chip-paddingInline': '0.5rem',
'--Chip-decorator-childHeight':
- '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',
+ 'calc(min(1.125rem, var(--_Chip-minHeight)) - 2 * var(--variant-borderWidth))',
+ '--Icon-fontSize': 'calc(var(--_Chip-minHeight) / 1.714)', // 0.875rem by default
+ '--_Chip-minHeight': 'var(--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.375rem, var(--Chip-minHeight))',
- '--Icon-fontSize': 'calc(var(--Chip-minHeight, 2rem) / 1.778)', // 1.125rem by default
- '--Chip-minHeight': '2rem',
+ '--Chip-decorator-childHeight': 'min(1.375rem, var(--_Chip-minHeight))',
+ '--Icon-fontSize': 'calc(var(--_Chip-minHeight) / 1.778)', // 1.125rem by default
+ '--_Chip-minHeight': 'var(--Chip-minHeight, 2rem)',
fontSize: theme.vars.fontSize.sm,
}),
...(ownerState.size === 'lg' && {
'--Chip-gap': '0.5rem',
'--Chip-paddingInline': '1rem',
- '--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',
+ '--Chip-decorator-childHeight': 'min(1.75rem, var(--_Chip-minHeight))',
+ '--Icon-fontSize': 'calc(var(--_Chip-minHeight) / 2)', // 1.25rem by default
+ '--_Chip-minHeight': 'var(--Chip-minHeight, 2.5rem)',
fontSize: theme.vars.fontSize.md,
}),
- minHeight: 'var(--Chip-minHeight)',
+ '--_Chip-radius': 'var(--Chip-radius, 1.5rem)',
+ '--_Chip-paddingBlock':
+ 'max((var(--_Chip-minHeight) - 2 * var(--variant-borderWidth) - var(--Chip-decorator-childHeight)) / 2, 0px)',
+ minHeight: 'var(--_Chip-minHeight)',
paddingInline: 'var(--Chip-paddingInline)',
- borderRadius: 'var(--Chip-radius)',
+ borderRadius: 'var(--_Chip-radius)',
position: 'relative',
fontWeight: theme.vars.fontWeight.md,
fontFamily: theme.vars.fontFamily.body,
@@ -119,9 +119,11 @@ const ChipLabel = styled('span', {
slot: 'Label',
overridesResolver: (props, styles) => styles.label,
})<{ ownerState: ChipOwnerState }>(({ ownerState }) => ({
- display: 'inherit',
- alignItems: 'center',
+ display: 'inline-block',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
order: 1,
+ minInlineSize: 0,
flexGrow: 1,
...(ownerState.clickable && {
zIndex: 1,
diff --git a/packages/mui-joy/src/FormControl/FormControl.test.tsx b/packages/mui-joy/src/FormControl/FormControl.test.tsx
index f01f1e41aabf5f..f89bdfe390ac62 100644
--- a/packages/mui-joy/src/FormControl/FormControl.test.tsx
+++ b/packages/mui-joy/src/FormControl/FormControl.test.tsx
@@ -13,6 +13,7 @@ import Textarea, { textareaClasses } from '@mui/joy/Textarea';
import RadioGroup from '@mui/joy/RadioGroup';
import Radio, { radioClasses } from '@mui/joy/Radio';
import Switch, { switchClasses } from '@mui/joy/Switch';
+import Autocomplete, { autocompleteClasses } from '@mui/joy/Autocomplete';
describe('', () => {
const { render } = createRenderer();
@@ -376,4 +377,48 @@ describe('', () => {
expect(getByLabelText('label')).to.have.attribute('disabled');
});
});
+
+ describe('Autocomplete', () => {
+ it('should linked the label', () => {
+ const { getByLabelText } = render(
+
+ label
+
+ ,
+ );
+
+ expect(getByLabelText('label')).toBeVisible();
+ });
+
+ it('should inherit color prop from FormControl', () => {
+ const { getByTestId } = render(
+
+
+ ,
+ );
+
+ expect(getByTestId('input')).to.have.class(autocompleteClasses.colorSuccess);
+ });
+
+ it('should inherit error prop from FormControl', () => {
+ const { getByTestId } = render(
+
+
+ ,
+ );
+
+ expect(getByTestId('input')).to.have.class(autocompleteClasses.colorDanger);
+ });
+
+ it('should inherit disabled from FormControl', () => {
+ const { getByRole } = render(
+
+ label
+
+ ,
+ );
+
+ expect(getByRole('combobox')).to.have.attribute('disabled');
+ });
+ });
});
diff --git a/packages/mui-joy/src/IconButton/IconButton.tsx b/packages/mui-joy/src/IconButton/IconButton.tsx
index 24d2f33d90a198..f008725acebb9a 100644
--- a/packages/mui-joy/src/IconButton/IconButton.tsx
+++ b/packages/mui-joy/src/IconButton/IconButton.tsx
@@ -31,61 +31,65 @@ const useUtilityClasses = (ownerState: IconButtonOwnerState) => {
return composedClasses;
};
-export const IconButtonRoot = styled('button', {
+export const StyledIconButton = styled('button')<{ ownerState: IconButtonOwnerState }>(
+ ({ theme, ownerState }) => [
+ {
+ '--Icon-margin': 'initial', // reset the icon's margin.
+ '--CircularProgress-size': 'var(--Icon-fontSize)',
+ ...(ownerState.size === 'sm' && {
+ '--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,
+ paddingInline: '2px', // add a gap, in case the content is long, e.g. multiple icons
+ }),
+ ...(ownerState.size === 'md' && {
+ '--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,
+ paddingInline: '0.25rem',
+ }),
+ ...(ownerState.size === 'lg' && {
+ '--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,
+ paddingInline: '0.375rem',
+ }),
+ WebkitTapHighlightColor: 'transparent',
+ paddingBlock: 0,
+ fontFamily: theme.vars.fontFamily.body,
+ fontWeight: theme.vars.fontWeight.md,
+ margin: `var(--IconButton-margin)`, // to be controlled by other components, eg. Input
+ borderRadius: `var(--IconButton-radius, ${theme.vars.radius.sm})`, // to be controlled by other components, eg. Input
+ border: 'none',
+ boxSizing: 'border-box',
+ backgroundColor: 'transparent',
+ display: 'inline-flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ position: 'relative',
+ // 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, box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, border-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
+ [theme.focus.selector]: theme.focus.default,
+ },
+ theme.variants[ownerState.variant!]?.[ownerState.color!],
+ { '&:hover': theme.variants[`${ownerState.variant!}Hover`]?.[ownerState.color!] },
+ { '&:active': theme.variants[`${ownerState.variant!}Active`]?.[ownerState.color!] },
+ {
+ [`&.${iconButtonClasses.disabled}`]:
+ theme.variants[`${ownerState.variant!}Disabled`]?.[ownerState.color!],
+ },
+ ],
+);
+
+export const IconButtonRoot = styled(StyledIconButton, {
name: 'JoyIconButton',
slot: 'Root',
overridesResolver: (props, styles) => styles.root,
-})<{ ownerState: IconButtonOwnerState }>(({ theme, ownerState }) => [
- {
- '--Icon-margin': 'initial', // reset the icon's margin.
- '--CircularProgress-size': 'var(--Icon-fontSize)',
- ...(ownerState.size === 'sm' && {
- '--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,
- paddingInline: '2px', // add a gap, in case the content is long, e.g. multiple icons
- }),
- ...(ownerState.size === 'md' && {
- '--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,
- paddingInline: '0.25rem',
- }),
- ...(ownerState.size === 'lg' && {
- '--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,
- paddingInline: '0.375rem',
- }),
- WebkitTapHighlightColor: 'transparent',
- paddingBlock: 0,
- fontFamily: theme.vars.fontFamily.body,
- fontWeight: theme.vars.fontWeight.md,
- margin: `var(--IconButton-margin)`, // to be controlled by other components, eg. Input
- borderRadius: `var(--IconButton-radius, ${theme.vars.radius.sm})`, // to be controlled by other components, eg. Input
- border: 'none',
- boxSizing: 'border-box',
- backgroundColor: 'transparent',
- display: 'inline-flex',
- alignItems: 'center',
- justifyContent: 'center',
- position: 'relative',
- // 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, box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, border-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
- [theme.focus.selector]: theme.focus.default,
- },
- theme.variants[ownerState.variant!]?.[ownerState.color!],
- { '&:hover': theme.variants[`${ownerState.variant!}Hover`]?.[ownerState.color!] },
- { '&:active': theme.variants[`${ownerState.variant!}Active`]?.[ownerState.color!] },
- {
- [`&.${iconButtonClasses.disabled}`]:
- theme.variants[`${ownerState.variant!}Disabled`]?.[ownerState.color!],
- },
-]);
+})({});
const IconButton = React.forwardRef(function IconButton(inProps, ref) {
const props = useThemeProps({
diff --git a/packages/mui-joy/src/Input/Input.tsx b/packages/mui-joy/src/Input/Input.tsx
index f735b35d708c17..06353aa3f932ae 100644
--- a/packages/mui-joy/src/Input/Input.tsx
+++ b/packages/mui-joy/src/Input/Input.tsx
@@ -29,118 +29,112 @@ const useUtilityClasses = (ownerState: InputOwnerState) => {
return composeClasses(slots, getInputUtilityClass, {});
};
-const InputRoot = styled('div', {
- name: 'JoyInput',
- slot: 'Root',
- overridesResolver: (props, styles) => styles.root,
-})<{ ownerState: InputOwnerState }>(({ theme, ownerState }) => {
- const variantStyle = theme.variants[`${ownerState.variant!}`]?.[ownerState.color!];
- return [
- {
- '--Input-radius': theme.vars.radius.sm,
- '--Input-gap': '0.5rem',
- '--Input-placeholderOpacity': 0.5,
- '--Input-focusedThickness': theme.vars.focus.thickness,
- ...(ownerState.color === 'context'
- ? {
- '--Input-focusedHighlight': theme.vars.palette.focusVisible,
- }
- : {
- '--Input-focusedHighlight':
- theme.vars.palette[
- ownerState.color === 'neutral' ? 'primary' : ownerState.color!
- ]?.[500],
- }),
- ...(ownerState.size === 'sm' && {
- '--Input-minHeight': '2rem',
- '--Input-paddingInline': '0.5rem',
- '--Input-decorator-childHeight': 'min(1.5rem, var(--Input-minHeight))',
- '--Icon-fontSize': '1.25rem',
- }),
- ...(ownerState.size === 'md' && {
- '--Input-minHeight': '2.5rem',
- '--Input-paddingInline': '0.75rem',
- '--Input-decorator-childHeight': 'min(2rem, var(--Input-minHeight))',
- '--Icon-fontSize': '1.5rem',
- }),
- ...(ownerState.size === 'lg' && {
- '--Input-minHeight': '3rem',
- '--Input-paddingInline': '1rem',
- '--Input-gap': '0.75rem',
- '--Input-decorator-childHeight': 'min(2.375rem, var(--Input-minHeight))',
- '--Icon-fontSize': '1.75rem',
- }),
- // variables for controlling child components
- '--Input-decorator-childOffset':
- 'min(calc(var(--Input-paddingInline) - (var(--Input-minHeight) - 2 * var(--variant-borderWidth) - var(--Input-decorator-childHeight)) / 2), var(--Input-paddingInline))',
- '--internal-paddingBlock':
- 'max((var(--Input-minHeight) - 2 * var(--variant-borderWidth) - var(--Input-decorator-childHeight)) / 2, 0px)',
- '--Input-decorator-childRadius':
- 'max(var(--Input-radius) - var(--internal-paddingBlock), min(var(--internal-paddingBlock) / 2, var(--Input-radius) / 2))',
- '--Button-minHeight': 'var(--Input-decorator-childHeight)',
- '--IconButton-size': 'var(--Input-decorator-childHeight)',
- '--Button-radius': 'var(--Input-decorator-childRadius)',
- '--IconButton-radius': 'var(--Input-decorator-childRadius)',
- boxSizing: 'border-box',
- minWidth: 0,
- minHeight: 'var(--Input-minHeight)',
- ...(ownerState.fullWidth && {
- width: '100%',
- }),
- cursor: 'text',
- position: 'relative',
- display: 'flex',
- alignItems: 'center',
- paddingInline: `var(--Input-paddingInline)`,
- borderRadius: 'var(--Input-radius)',
- 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:
- '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': {
+export const StyledInputRoot = styled('div')<{ ownerState: InputOwnerState }>(
+ ({ theme, ownerState }) => {
+ const variantStyle = theme.variants[`${ownerState.variant!}`]?.[ownerState.color!];
+ return [
+ {
+ '--Input-radius': theme.vars.radius.sm,
+ '--Input-gap': '0.5rem',
+ '--Input-placeholderOpacity': 0.5,
+ '--Input-focusedThickness': theme.vars.focus.thickness,
+ ...(ownerState.color === 'context'
+ ? {
+ '--Input-focusedHighlight': theme.vars.palette.focusVisible,
+ }
+ : {
+ '--Input-focusedHighlight':
+ theme.vars.palette[
+ ownerState.color === 'neutral' ? 'primary' : ownerState.color!
+ ]?.[500],
+ }),
+ ...(ownerState.size === 'sm' && {
+ '--Input-minHeight': '2rem',
+ '--Input-paddingInline': '0.5rem',
+ '--Input-decorator-childHeight': 'min(1.5rem, var(--Input-minHeight))',
+ '--Icon-fontSize': '1.25rem',
+ }),
+ ...(ownerState.size === 'md' && {
+ '--Input-minHeight': '2.5rem',
+ '--Input-paddingInline': '0.75rem',
+ '--Input-decorator-childHeight': 'min(2rem, var(--Input-minHeight))',
+ '--Icon-fontSize': '1.5rem',
+ }),
+ ...(ownerState.size === 'lg' && {
+ '--Input-minHeight': '3rem',
+ '--Input-paddingInline': '1rem',
+ '--Input-gap': '0.75rem',
+ '--Input-decorator-childHeight': 'min(2.375rem, var(--Input-minHeight))',
+ '--Icon-fontSize': '1.75rem',
+ }),
+ // variables for controlling child components
+ '--Input-decorator-childOffset':
+ 'min(calc(var(--Input-paddingInline) - (var(--Input-minHeight) - 2 * var(--variant-borderWidth) - var(--Input-decorator-childHeight)) / 2), var(--Input-paddingInline))',
+ '--_Input-paddingBlock':
+ 'max((var(--Input-minHeight) - 2 * var(--variant-borderWidth) - var(--Input-decorator-childHeight)) / 2, 0px)',
+ '--Input-decorator-childRadius':
+ 'max(var(--Input-radius) - var(--_Input-paddingBlock), min(var(--_Input-paddingBlock) / 2, var(--Input-radius) / 2))',
+ '--Button-minHeight': 'var(--Input-decorator-childHeight)',
+ '--IconButton-size': 'var(--Input-decorator-childHeight)',
+ '--Button-radius': 'var(--Input-decorator-childRadius)',
+ '--IconButton-radius': 'var(--Input-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
- },
- },
- {
- // variant styles
- ...variantStyle,
- backgroundColor: variantStyle?.backgroundColor ?? theme.vars.palette.background.surface,
- [`&:hover:not(.${inputClasses.focused})`]: {
- ...theme.variants[`${ownerState.variant!}Hover`]?.[ownerState.color!],
- backgroundColor: null, // it is not common to change background on hover for Input
+ minWidth: 0,
+ minHeight: 'var(--Input-minHeight)',
+ ...(ownerState.fullWidth && {
+ width: '100%',
+ }),
cursor: 'text',
- },
- [`&.${inputClasses.disabled}`]:
- theme.variants[`${ownerState.variant!}Disabled`]?.[ownerState.color!],
- [`&.${inputClasses.focused}`]: {
+ position: 'relative',
+ display: 'flex',
+ alignItems: 'center',
+ paddingInline: `var(--Input-paddingInline)`,
+ borderRadius: 'var(--Input-radius)',
+ 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:
+ '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': {
- boxShadow: `inset 0 0 0 var(--Input-focusedThickness) var(--Input-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
},
},
- },
- ];
-});
+ {
+ // variant styles
+ ...variantStyle,
+ backgroundColor: variantStyle?.backgroundColor ?? theme.vars.palette.background.surface,
+ [`&:hover:not(.${inputClasses.focused})`]: {
+ ...theme.variants[`${ownerState.variant!}Hover`]?.[ownerState.color!],
+ backgroundColor: null, // it is not common to change background on hover for Input
+ cursor: 'text',
+ },
+ [`&.${inputClasses.disabled}`]:
+ theme.variants[`${ownerState.variant!}Disabled`]?.[ownerState.color!],
+ [`&.${inputClasses.focused}`]: {
+ '&:before': {
+ boxShadow: `inset 0 0 0 var(--Input-focusedThickness) var(--Input-focusedHighlight)`,
+ },
+ },
+ },
+ ];
+ },
+);
-const InputInput = styled('input', {
- name: 'JoyInput',
- slot: 'Input',
- overridesResolver: (props, styles) => styles.input,
-})<{ ownerState: InputOwnerState }>({
+export const StyledInputHtml = styled('input')<{ ownerState: InputOwnerState }>({
border: 'none', // remove the native input width
minWidth: 0, // remove the native input width
outline: 0, // remove the native input outline
@@ -165,44 +159,66 @@ const InputInput = styled('input', {
'&::-ms-input-placeholder': { opacity: 'var(--Input-placeholderOpacity)', color: 'inherit' }, // Edge
});
-const InputStartDecorator = styled('span', {
+export const StyledInputStartDecorator = styled('span')<{ ownerState: InputOwnerState }>(
+ ({ theme, ownerState }) => ({
+ '--Button-margin': '0 0 0 calc(var(--Input-decorator-childOffset) * -1)',
+ '--IconButton-margin': '0 0 0 calc(var(--Input-decorator-childOffset) * -1)',
+ '--Icon-margin': '0 0 0 calc(var(--Input-paddingInline) / -4)',
+ display: 'inherit',
+ alignItems: 'center',
+ paddingBlock: 'var(--unstable_Input-paddingBlock)', // for wrapping Autocomplete's tags
+ flexWrap: 'wrap', // for wrapping Autocomplete's tags
+ marginInlineEnd: 'var(--Input-gap)',
+ color: theme.vars.palette.text.tertiary,
+ cursor: 'initial',
+ ...(ownerState.focused && {
+ color: theme.variants[ownerState.variant!]?.[ownerState.color!]?.color,
+ }),
+ ...(ownerState.disabled && {
+ color: theme.variants[`${ownerState.variant!}Disabled`]?.[ownerState.color!]?.color,
+ }),
+ }),
+);
+
+export const StyledInputEndDecorator = styled('span')<{ ownerState: InputOwnerState }>(
+ ({ theme, ownerState }) => ({
+ '--Button-margin': '0 calc(var(--Input-decorator-childOffset) * -1) 0 0',
+ '--IconButton-margin': '0 calc(var(--Input-decorator-childOffset) * -1) 0 0',
+ '--Icon-margin': '0 calc(var(--Input-paddingInline) / -4) 0 0',
+ display: 'inherit',
+ alignItems: 'center',
+ marginInlineStart: 'var(--Input-gap)',
+ color: theme.variants[ownerState.variant!]?.[ownerState.color!]?.color,
+ cursor: 'initial',
+ ...(ownerState.disabled && {
+ color: theme.variants[`${ownerState.variant!}Disabled`]?.[ownerState.color!]?.color,
+ }),
+ }),
+);
+
+const InputRoot = styled(StyledInputRoot, {
+ name: 'JoyInput',
+ slot: 'Root',
+ overridesResolver: (props, styles) => styles.root,
+})({});
+
+const InputInput = styled(StyledInputHtml, {
+ name: 'JoyInput',
+ slot: 'Input',
+ overridesResolver: (props, styles) => styles.input,
+})({});
+
+const InputStartDecorator = styled(StyledInputStartDecorator, {
name: 'JoyInput',
slot: 'StartDecorator',
overridesResolver: (props, styles) => styles.startDecorator,
-})<{ ownerState: InputOwnerState }>(({ theme, ownerState }) => ({
- '--Button-margin': '0 0 0 calc(var(--Input-decorator-childOffset) * -1)',
- '--IconButton-margin': '0 0 0 calc(var(--Input-decorator-childOffset) * -1)',
- '--Icon-margin': '0 0 0 calc(var(--Input-paddingInline) / -4)',
- display: 'inherit',
- alignItems: 'center',
- marginInlineEnd: 'var(--Input-gap)',
- color: theme.vars.palette.text.tertiary,
- cursor: 'initial',
- ...(ownerState.focused && {
- color: theme.variants[ownerState.variant!]?.[ownerState.color!]?.color,
- }),
- ...(ownerState.disabled && {
- color: theme.variants[`${ownerState.variant!}Disabled`]?.[ownerState.color!]?.color,
- }),
-}));
+})({});
-const InputEndDecorator = styled('span', {
+const InputEndDecorator = styled(StyledInputEndDecorator, {
name: 'JoyInput',
slot: 'EndDecorator',
overridesResolver: (props, styles) => styles.endDecorator,
-})<{ ownerState: InputOwnerState }>(({ theme, ownerState }) => ({
- '--Button-margin': '0 calc(var(--Input-decorator-childOffset) * -1) 0 0',
- '--IconButton-margin': '0 calc(var(--Input-decorator-childOffset) * -1) 0 0',
- '--Icon-margin': '0 calc(var(--Input-paddingInline) / -4) 0 0',
- display: 'inherit',
- alignItems: 'center',
- marginInlineStart: 'var(--Input-gap)',
- color: theme.variants[ownerState.variant!]?.[ownerState.color!]?.color,
- cursor: 'initial',
- ...(ownerState.disabled && {
- color: theme.variants[`${ownerState.variant!}Disabled`]?.[ownerState.color!]?.color,
- }),
-}));
+})({});
const Input = React.forwardRef(function Input(inProps, ref) {
const props = useThemeProps({
diff --git a/packages/mui-joy/src/List/List.tsx b/packages/mui-joy/src/List/List.tsx
index 5dd7f40655d5ac..7d1a9dffbf7e09 100644
--- a/packages/mui-joy/src/List/List.tsx
+++ b/packages/mui-joy/src/List/List.tsx
@@ -31,11 +31,7 @@ const useUtilityClasses = (ownerState: ListOwnerState) => {
return composeClasses(slots, getListUtilityClass, {});
};
-export const ListRoot = styled('ul', {
- name: 'JoyList',
- slot: 'Root',
- overridesResolver: (props, styles) => styles.root,
-})<{ ownerState: ListOwnerState }>(({ theme, ownerState }) => {
+export const StyledList = styled('ul')<{ ownerState: ListOwnerState }>(({ theme, ownerState }) => {
function applySizeVars(size: ListProps['size']) {
if (size === 'sm') {
return {
@@ -140,6 +136,12 @@ export const ListRoot = styled('ul', {
];
});
+export const ListRoot = styled(StyledList, {
+ name: 'JoyList',
+ slot: 'Root',
+ overridesResolver: (props, styles) => styles.root,
+})({});
+
const List = React.forwardRef(function List(inProps, ref) {
const nesting = React.useContext(NestedListContext);
const menuContext = React.useContext(MenuUnstyledContext);
diff --git a/packages/mui-joy/src/List/ListProps.ts b/packages/mui-joy/src/List/ListProps.ts
index 4d3e601f22e4a1..6de57cf9b48b5b 100644
--- a/packages/mui-joy/src/List/ListProps.ts
+++ b/packages/mui-joy/src/List/ListProps.ts
@@ -68,5 +68,5 @@ export interface ListOwnerState extends ListProps {
* @internal
* If `true`, the element is rendered in a nested list item.
*/
- nesting: boolean | string;
+ nesting?: boolean | string;
}
diff --git a/packages/mui-joy/src/List/ListProvider.tsx b/packages/mui-joy/src/List/ListProvider.tsx
index eabf0697665e14..67202fad1fe0cd 100644
--- a/packages/mui-joy/src/List/ListProvider.tsx
+++ b/packages/mui-joy/src/List/ListProvider.tsx
@@ -7,7 +7,7 @@ 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.
+ * Used in `Menu`, `MenuList`, `TabList`, `Select`, and `Autocomplete` to communicate with nested List.
*
* e.g. menu group:
*