Skip to content

[material-ui][Chip] Add slots and slotProps #46098

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
May 29, 2025
37 changes: 25 additions & 12 deletions docs/pages/material-ui/api/chip.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@
"default": "'medium'"
},
"skipFocusWhenDisabled": { "type": { "name": "bool" }, "default": "false" },
"slotProps": {
"type": {
"name": "shape",
"description": "{ label?: func<br>&#124;&nbsp;object, root?: func<br>&#124;&nbsp;object }"
},
"default": "{}"
},
"slots": {
"type": { "name": "shape", "description": "{ label?: elementType, root?: elementType }" },
"default": "{}"
},
"sx": {
"type": {
"name": "union",
Expand All @@ -42,6 +53,20 @@
},
"name": "Chip",
"imports": ["import Chip from '@mui/material/Chip';", "import { Chip } from '@mui/material';"],
"slots": [
{
"name": "root",
"description": "The component that renders the root.",
"default": "div",
"class": "MuiChip-root"
},
{
"name": "label",
"description": "The component that renders the label.",
"default": "span",
"class": "MuiChip-label"
}
],
"classes": [
{
"key": "avatar",
Expand Down Expand Up @@ -287,12 +312,6 @@
"isGlobal": false,
"isDeprecated": true
},
{
"key": "label",
"className": "MuiChip-label",
"description": "Styles applied to the label `span` element.",
"isGlobal": false
},
{
"key": "labelMedium",
"className": "MuiChip-labelMedium",
Expand Down Expand Up @@ -327,12 +346,6 @@
"isGlobal": false,
"isDeprecated": true
},
{
"key": "root",
"className": "MuiChip-root",
"description": "Styles applied to the root element.",
"isGlobal": false
},
{
"key": "sizeMedium",
"className": "MuiChip-sizeMedium",
Expand Down
11 changes: 6 additions & 5 deletions docs/translations/api-docs/chip/chip.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
"skipFocusWhenDisabled": {
"description": "If <code>true</code>, allows the disabled chip to escape focus. If <code>false</code>, allows the disabled chip to receive focus."
},
"slotProps": { "description": "The props used for each slot inside." },
"slots": { "description": "The components used for each slot inside." },
"sx": {
"description": "The system prop that allows defining system overrides as well as additional CSS styles."
},
Expand Down Expand Up @@ -235,10 +237,6 @@
"conditions": "<code>size=\"small\"</code>",
"deprecationInfo": "Combine the <a href=\"/material-ui/api/chip/#chip-classes-icon\">.MuiChip-icon</a> and <a href=\"/material-ui/api/chip/#chip-classes-sizeSmall\">.MuiChip-sizeSmall</a> classes instead. See <a href=\"/material-ui/migration/migrating-from-deprecated-apis/\">Migrating from deprecated APIs</a> for more details."
},
"label": {
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the label <code>span</code> element"
},
"labelMedium": {
"description": "Styles applied to {{nodeName}} if {{conditions}}.",
"nodeName": "the label <code>span</code> element",
Expand Down Expand Up @@ -268,7 +266,6 @@
"conditions": "<code>variant=\"outlined\"</code> and <code>color=\"secondary\"</code>",
"deprecationInfo": "Combine the <a href=\"/material-ui/api/chip/#chip-classes-outlined\">.MuiChip-outlined</a> and <a href=\"/material-ui/api/chip/#chip-classes-colorSecondary\">.MuiChip-colorSecondary</a> classes instead. See <a href=\"/material-ui/migration/migrating-from-deprecated-apis/\">Migrating from deprecated APIs</a> for more details."
},
"root": { "description": "Styles applied to the root element." },
"sizeMedium": {
"description": "Styles applied to {{nodeName}} if {{conditions}}.",
"nodeName": "the root element",
Expand All @@ -279,5 +276,9 @@
"nodeName": "the root element",
"conditions": "<code>size=\"small\"</code>"
}
},
"slotDescriptions": {
"label": "The component that renders the label.",
"root": "The component that renders the root."
}
}
34 changes: 33 additions & 1 deletion packages/mui-material/src/Chip/Chip.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,42 @@
import * as React from 'react';
import { OverridableStringUnion } from '@mui/types';
import { SxProps } from '@mui/system';
import { CreateSlotsAndSlotProps, SlotProps } from '..';
import { Theme } from '../styles';
import { OverridableComponent, OverrideProps } from '../OverridableComponent';
import { ChipClasses } from './chipClasses';

export interface ChipSlots {
/**
* The component that renders the root.
* @default div
*/
root: React.ElementType;
/**
* The component that renders the label.
* @default span
*/
label: React.ElementType;
}

export type ChipSlotsAndSlotProps = CreateSlotsAndSlotProps<
ChipSlots,
{
/**
* Props forwarded to the root slot.
* By default, the avaible props are based on the div element.
*/
root: SlotProps<'div', {}, ChipOwnerState>;
/**
* Props forwarded to the label slot.
* By default, the avaible props are based on the span element.
*/
label: SlotProps<'span', {}, ChipOwnerState>;
}
>;

export interface ChipOwnerState extends Omit<ChipProps, 'slots' | 'slotProps'> {}

export interface ChipPropsVariantOverrides {}

export interface ChipPropsSizeOverrides {}
Expand Down Expand Up @@ -96,7 +128,7 @@ export interface ChipTypeMap<
AdditionalProps = {},
RootComponent extends React.ElementType = 'div',
> {
props: AdditionalProps & ChipOwnProps;
props: AdditionalProps & ChipOwnProps & ChipSlotsAndSlotProps;
defaultComponent: RootComponent;
}

Expand Down
84 changes: 67 additions & 17 deletions packages/mui-material/src/Chip/Chip.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import memoTheme from '../utils/memoTheme';
import createSimplePaletteValueFilter from '../utils/createSimplePaletteValueFilter';
import { useDefaultProps } from '../DefaultPropsProvider';
import chipClasses, { getChipUtilityClass } from './chipClasses';
import useSlot from '../utils/useSlot';

const useUtilityClasses = (ownerState) => {
const { classes, disabled, size, color, iconColor, onDelete, clickable, variant } = ownerState;
Expand Down Expand Up @@ -400,6 +401,8 @@ const Chip = React.forwardRef(function Chip(inProps, ref) {
variant = 'filled',
tabIndex,
skipFocusWhenDisabled = false, // TODO v6: Rename to `focusableWhenDisabled`.
slots = {},
slotProps = {},
...other
} = props;

Expand Down Expand Up @@ -503,26 +506,57 @@ const Chip = React.forwardRef(function Chip(inProps, ref) {
}
}

const externalForwardedProps = {
slots,
slotProps,
};

const [RootSlot, rootProps] = useSlot('root', {
elementType: ChipRoot,
externalForwardedProps: {
...externalForwardedProps,
...other,
},
ownerState,
// The `component` prop is preserved because `Chip` relies on it for internal logic. If `shouldForwardComponentProp` were `false`, `useSlot` would remove the `component` prop, potentially breaking the component's behavior.
shouldForwardComponentProp: true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
shouldForwardComponentProp: true,

ChipRoot comes from div, no need to forward component prop

Copy link
Contributor Author

@sai6855 sai6855 May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The component prop was forwarded because, when shouldForwardComponentProp is set to false, the component prop returned from useSlot('root') gets removed in the useSlot.ts file and replaced with the as prop here.

While this behavior works fine for components that do not rely on component-specific logic, it breaks components like Chip, which access the component prop directly in their logic — for example, this line. Removing the component prop in such cases causes test failures.

To fix this, I added shouldForwardComponentProp: true to ensure that the component prop is preserved and Chip continues to behave correctly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. Can you add a comment above the line shouldForwardComponentProp.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, added here 29b9ea4

ref: handleRef,
className: clsx(classes.root, className),
additionalProps: {
disabled: clickable && disabled ? true : undefined,
tabIndex: skipFocusWhenDisabled && disabled ? -1 : tabIndex,
...moreProps,
},
getSlotProps: (handlers) => ({
...handlers,
onClick: (event) => {
handlers.onClick?.(event);
onClick(event);
},
onKeyDown: (event) => {
handlers.onKeyDown?.(event);
handleKeyDown(event);
},
onKeyUp: (event) => {
handlers.onKeyUp?.(event);
handleKeyUp(event);
},
}),
});

const [LabelSlot, labelProps] = useSlot('label', {
elementType: ChipLabel,
externalForwardedProps,
ownerState,
className: classes.label,
});

return (
<ChipRoot
as={component}
className={clsx(classes.root, className)}
disabled={clickable && disabled ? true : undefined}
onClick={onClick}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
ref={handleRef}
tabIndex={skipFocusWhenDisabled && disabled ? -1 : tabIndex}
ownerState={ownerState}
{...moreProps}
{...other}
>
<RootSlot as={component} {...rootProps}>
{avatar || icon}
<ChipLabel className={classes.label} ownerState={ownerState}>
{label}
</ChipLabel>
<LabelSlot {...labelProps}>{label}</LabelSlot>
{deleteIcon}
</ChipRoot>
</RootSlot>
);
});

Expand Down Expand Up @@ -620,6 +654,22 @@ Chip.propTypes /* remove-proptypes */ = {
* @default false
*/
skipFocusWhenDisabled: PropTypes.bool,
/**
* The props used for each slot inside.
* @default {}
*/
slotProps: PropTypes.shape({
label: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
root: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
}),
/**
* The components used for each slot inside.
* @default {}
*/
slots: PropTypes.shape({
label: PropTypes.elementType,
root: PropTypes.elementType,
}),
/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
Expand Down
8 changes: 8 additions & 0 deletions packages/mui-material/src/Chip/Chip.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ describe('<Chip />', () => {
refInstanceof: window.HTMLDivElement,
testComponentPropWith: 'span',
skip: ['componentsProp'],
slots: {
root: {
expectedClassName: classes.root,
},
label: {
expectedClassName: classes.label,
},
},
Copy link
Member

@siriwatknp siriwatknp May 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the comment about the shouldForwardComponentProp. I think we need to add more tests.

  • With clickable: true and a custom component prop.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it('should render link with the button base', () => {
const { container } = render(<Chip component="a" clickable label="My text Chip" />);
expect(container.firstChild).to.have.class('MuiButtonBase-root');
expect(container.firstChild).to.have.tagName('a');
});
There is test which tests with same props combination as mentioned. is this test sufficient or do you have any other test in mind?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good one.

}));

describe('text only', () => {
Expand Down
Loading