init project
Some checks failed
No response / noResponse (push) Has been cancelled
CI / Continuous releases (push) Has been cancelled
CI / test-dev (macos-latest) (push) Has been cancelled
CI / test-dev (ubuntu-latest) (push) Has been cancelled
CI / test-dev (windows-latest) (push) Has been cancelled
Maintenance / main (push) Has been cancelled
Scorecards supply-chain security / Scorecards analysis (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled

This commit is contained in:
how2ice
2025-12-12 14:26:25 +09:00
commit 005cf56baf
43188 changed files with 1079531 additions and 0 deletions

View File

@@ -0,0 +1,185 @@
import * as React from 'react';
import { SxProps } from '@mui/system';
import { OverridableStringUnion, Simplify } from '@mui/types';
import { CreateSlotsAndSlotProps, SlotProps } from '../utils/types';
import { Theme } from '../styles';
import { OverridableComponent, OverrideProps } from '../OverridableComponent';
import { BadgeClasses } from './badgeClasses';
export interface BadgePropsVariantOverrides {}
export interface BadgePropsColorOverrides {}
export interface BadgeRootSlotPropsOverrides {}
export interface BadgeBadgeSlotPropsOverrides {}
export interface BadgeSlots {
/**
* The component that renders the root.
* @default span
*/
root: React.ElementType;
/**
* The component that renders the badge.
* @default span
*/
badge: React.ElementType;
}
export type BadgeSlotsAndSlotProps = CreateSlotsAndSlotProps<
BadgeSlots,
{
/**
* Props forwarded to the root slot.
* By default, the available props are based on the span element.
*/
root: SlotProps<'span', BadgeRootSlotPropsOverrides, BadgeOwnerState>;
/**
* Props forwarded to the label slot.
* By default, the available props are based on the span element.
*/
badge: SlotProps<'span', BadgeBadgeSlotPropsOverrides, BadgeOwnerState>;
}
>;
export type BadgeOwnerState = Simplify<
Omit<BadgeOwnProps, 'slotProps' | 'slots'> & {
badgeContent: React.ReactNode;
invisible: boolean;
max: number;
displayValue: React.ReactNode;
showZero: boolean;
anchorOrigin: BadgeOrigin;
color: OverridableStringUnion<
'primary' | 'secondary' | 'default' | 'error' | 'info' | 'success' | 'warning',
BadgePropsColorOverrides
>;
overlap: 'rectangular' | 'circular';
variant: OverridableStringUnion<'standard' | 'dot', BadgePropsVariantOverrides>;
}
>;
export interface BadgeOrigin {
vertical?: 'top' | 'bottom';
horizontal?: 'left' | 'right';
}
export interface BadgeOwnProps extends BadgeSlotsAndSlotProps {
/**
* The anchor of the badge.
* @default {
* vertical: 'top',
* horizontal: 'right',
* }
*/
anchorOrigin?: BadgeOrigin;
/**
* The content rendered within the badge.
*/
badgeContent?: React.ReactNode;
/**
* The badge will be added relative to this node.
*/
children?: React.ReactNode;
/**
* Override or extend the styles applied to the component.
*/
classes?: Partial<BadgeClasses>;
/**
* @ignore
*/
className?: string;
/**
* The color of the component.
* It supports both default and custom theme colors, which can be added as shown in the
* [palette customization guide](https://mui.com/material-ui/customization/palette/#custom-colors).
* @default 'default'
*/
color?: OverridableStringUnion<
'primary' | 'secondary' | 'default' | 'error' | 'info' | 'success' | 'warning',
BadgePropsColorOverrides
>;
/**
* The extra props for the slot components.
* You can override the existing props or add new ones.
*
* @deprecated use the `slotProps` prop instead. This prop will be removed in a future major release. See [Migrating from deprecated APIs](https://mui.com/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*
* @default {}
*/
componentsProps?: BadgeOwnProps['slotProps'];
/**
* The components used for each slot inside.
*
* @deprecated use the `slots` prop instead. This prop will be removed in a future major release. See [Migrating from deprecated APIs](https://mui.com/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*
* @default {}
*/
components?: {
Root?: React.ElementType;
Badge?: React.ElementType;
};
/**
* If `true`, the badge is invisible.
* @default false
*/
invisible?: boolean;
/**
* Max count to show.
* @default 99
*/
max?: number;
/**
* Wrapped shape the badge should overlap.
* @default 'rectangular'
*/
overlap?: 'rectangular' | 'circular';
/**
* Controls whether the badge is hidden when `badgeContent` is zero.
* @default false
*/
showZero?: boolean;
/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx?: SxProps<Theme>;
/**
* The variant to use.
* @default 'standard'
*/
variant?: OverridableStringUnion<'standard' | 'dot', BadgePropsVariantOverrides>;
}
export interface BadgeTypeMap<
RootComponent extends React.ElementType = 'span',
AdditionalProps = {},
> {
props: AdditionalProps & BadgeOwnProps;
defaultComponent: RootComponent;
}
type BadgeRootProps = NonNullable<BadgeTypeMap['props']['slotProps']>['root'];
type BadgeBadgeProps = NonNullable<BadgeTypeMap['props']['slotProps']>['badge'];
export declare const BadgeRoot: React.FC<BadgeRootProps>;
export declare const BadgeMark: React.FC<BadgeBadgeProps>;
/**
*
* Demos:
*
* - [Avatar](https://mui.com/material-ui/react-avatar/)
* - [Badge](https://mui.com/material-ui/react-badge/)
*
* API:
*
* - [Badge API](https://mui.com/material-ui/api/badge/)
*/
declare const Badge: OverridableComponent<BadgeTypeMap>;
export type BadgeProps<
RootComponent extends React.ElementType = BadgeTypeMap['defaultComponent'],
AdditionalProps = {},
> = OverrideProps<BadgeTypeMap<RootComponent, AdditionalProps>, RootComponent> & {
component?: React.ElementType;
};
export default Badge;

View File

@@ -0,0 +1,485 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import usePreviousProps from '@mui/utils/usePreviousProps';
import composeClasses from '@mui/utils/composeClasses';
import useBadge from './useBadge';
import { styled } from '../zero-styled';
import memoTheme from '../utils/memoTheme';
import createSimplePaletteValueFilter from '../utils/createSimplePaletteValueFilter';
import { useDefaultProps } from '../DefaultPropsProvider';
import capitalize from '../utils/capitalize';
import badgeClasses, { getBadgeUtilityClass } from './badgeClasses';
import useSlot from '../utils/useSlot';
const RADIUS_STANDARD = 10;
const RADIUS_DOT = 4;
const useUtilityClasses = (ownerState) => {
const { color, anchorOrigin, invisible, overlap, variant, classes = {} } = ownerState;
const slots = {
root: ['root'],
badge: [
'badge',
variant,
invisible && 'invisible',
`anchorOrigin${capitalize(anchorOrigin.vertical)}${capitalize(anchorOrigin.horizontal)}`,
`anchorOrigin${capitalize(anchorOrigin.vertical)}${capitalize(
anchorOrigin.horizontal,
)}${capitalize(overlap)}`,
`overlap${capitalize(overlap)}`,
color !== 'default' && `color${capitalize(color)}`,
],
};
return composeClasses(slots, getBadgeUtilityClass, classes);
};
const BadgeRoot = styled('span', {
name: 'MuiBadge',
slot: 'Root',
})({
position: 'relative',
display: 'inline-flex',
// For correct alignment with the text.
verticalAlign: 'middle',
flexShrink: 0,
});
const BadgeBadge = styled('span', {
name: 'MuiBadge',
slot: 'Badge',
overridesResolver: (props, styles) => {
const { ownerState } = props;
return [
styles.badge,
styles[ownerState.variant],
styles[
`anchorOrigin${capitalize(ownerState.anchorOrigin.vertical)}${capitalize(
ownerState.anchorOrigin.horizontal,
)}${capitalize(ownerState.overlap)}`
],
ownerState.color !== 'default' && styles[`color${capitalize(ownerState.color)}`],
ownerState.invisible && styles.invisible,
];
},
})(
memoTheme(({ theme }) => ({
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center',
alignContent: 'center',
alignItems: 'center',
position: 'absolute',
boxSizing: 'border-box',
fontFamily: theme.typography.fontFamily,
fontWeight: theme.typography.fontWeightMedium,
fontSize: theme.typography.pxToRem(12),
minWidth: RADIUS_STANDARD * 2,
lineHeight: 1,
padding: '0 6px',
height: RADIUS_STANDARD * 2,
borderRadius: RADIUS_STANDARD,
zIndex: 1, // Render the badge on top of potential ripples.
transition: theme.transitions.create('transform', {
easing: theme.transitions.easing.easeInOut,
duration: theme.transitions.duration.enteringScreen,
}),
variants: [
...Object.entries(theme.palette)
.filter(createSimplePaletteValueFilter(['contrastText']))
.map(([color]) => ({
props: { color },
style: {
backgroundColor: (theme.vars || theme).palette[color].main,
color: (theme.vars || theme).palette[color].contrastText,
},
})),
{
props: { variant: 'dot' },
style: {
borderRadius: RADIUS_DOT,
height: RADIUS_DOT * 2,
minWidth: RADIUS_DOT * 2,
padding: 0,
},
},
{
props: ({ ownerState }) =>
ownerState.anchorOrigin.vertical === 'top' &&
ownerState.anchorOrigin.horizontal === 'right' &&
ownerState.overlap === 'rectangular',
style: {
top: 0,
right: 0,
transform: 'scale(1) translate(50%, -50%)',
transformOrigin: '100% 0%',
[`&.${badgeClasses.invisible}`]: {
transform: 'scale(0) translate(50%, -50%)',
},
},
},
{
props: ({ ownerState }) =>
ownerState.anchorOrigin.vertical === 'bottom' &&
ownerState.anchorOrigin.horizontal === 'right' &&
ownerState.overlap === 'rectangular',
style: {
bottom: 0,
right: 0,
transform: 'scale(1) translate(50%, 50%)',
transformOrigin: '100% 100%',
[`&.${badgeClasses.invisible}`]: {
transform: 'scale(0) translate(50%, 50%)',
},
},
},
{
props: ({ ownerState }) =>
ownerState.anchorOrigin.vertical === 'top' &&
ownerState.anchorOrigin.horizontal === 'left' &&
ownerState.overlap === 'rectangular',
style: {
top: 0,
left: 0,
transform: 'scale(1) translate(-50%, -50%)',
transformOrigin: '0% 0%',
[`&.${badgeClasses.invisible}`]: {
transform: 'scale(0) translate(-50%, -50%)',
},
},
},
{
props: ({ ownerState }) =>
ownerState.anchorOrigin.vertical === 'bottom' &&
ownerState.anchorOrigin.horizontal === 'left' &&
ownerState.overlap === 'rectangular',
style: {
bottom: 0,
left: 0,
transform: 'scale(1) translate(-50%, 50%)',
transformOrigin: '0% 100%',
[`&.${badgeClasses.invisible}`]: {
transform: 'scale(0) translate(-50%, 50%)',
},
},
},
{
props: ({ ownerState }) =>
ownerState.anchorOrigin.vertical === 'top' &&
ownerState.anchorOrigin.horizontal === 'right' &&
ownerState.overlap === 'circular',
style: {
top: '14%',
right: '14%',
transform: 'scale(1) translate(50%, -50%)',
transformOrigin: '100% 0%',
[`&.${badgeClasses.invisible}`]: {
transform: 'scale(0) translate(50%, -50%)',
},
},
},
{
props: ({ ownerState }) =>
ownerState.anchorOrigin.vertical === 'bottom' &&
ownerState.anchorOrigin.horizontal === 'right' &&
ownerState.overlap === 'circular',
style: {
bottom: '14%',
right: '14%',
transform: 'scale(1) translate(50%, 50%)',
transformOrigin: '100% 100%',
[`&.${badgeClasses.invisible}`]: {
transform: 'scale(0) translate(50%, 50%)',
},
},
},
{
props: ({ ownerState }) =>
ownerState.anchorOrigin.vertical === 'top' &&
ownerState.anchorOrigin.horizontal === 'left' &&
ownerState.overlap === 'circular',
style: {
top: '14%',
left: '14%',
transform: 'scale(1) translate(-50%, -50%)',
transformOrigin: '0% 0%',
[`&.${badgeClasses.invisible}`]: {
transform: 'scale(0) translate(-50%, -50%)',
},
},
},
{
props: ({ ownerState }) =>
ownerState.anchorOrigin.vertical === 'bottom' &&
ownerState.anchorOrigin.horizontal === 'left' &&
ownerState.overlap === 'circular',
style: {
bottom: '14%',
left: '14%',
transform: 'scale(1) translate(-50%, 50%)',
transformOrigin: '0% 100%',
[`&.${badgeClasses.invisible}`]: {
transform: 'scale(0) translate(-50%, 50%)',
},
},
},
{
props: { invisible: true },
style: {
transition: theme.transitions.create('transform', {
easing: theme.transitions.easing.easeInOut,
duration: theme.transitions.duration.leavingScreen,
}),
},
},
],
})),
);
function getAnchorOrigin(anchorOrigin) {
return {
vertical: anchorOrigin?.vertical ?? 'top',
horizontal: anchorOrigin?.horizontal ?? 'right',
};
}
const Badge = React.forwardRef(function Badge(inProps, ref) {
const props = useDefaultProps({ props: inProps, name: 'MuiBadge' });
const {
anchorOrigin: anchorOriginProp,
className,
classes: classesProp,
component,
components = {},
componentsProps = {},
children,
overlap: overlapProp = 'rectangular',
color: colorProp = 'default',
invisible: invisibleProp = false,
max: maxProp = 99,
badgeContent: badgeContentProp,
slots,
slotProps,
showZero = false,
variant: variantProp = 'standard',
...other
} = props;
const {
badgeContent,
invisible: invisibleFromHook,
max,
displayValue: displayValueFromHook,
} = useBadge({
max: maxProp,
invisible: invisibleProp,
badgeContent: badgeContentProp,
showZero,
});
const prevProps = usePreviousProps({
anchorOrigin: getAnchorOrigin(anchorOriginProp),
color: colorProp,
overlap: overlapProp,
variant: variantProp,
badgeContent: badgeContentProp,
});
const invisible = invisibleFromHook || (badgeContent == null && variantProp !== 'dot');
const {
color = colorProp,
overlap = overlapProp,
anchorOrigin: anchorOriginPropProp,
variant = variantProp,
} = invisible ? prevProps : props;
const anchorOrigin = getAnchorOrigin(anchorOriginPropProp);
const displayValue = variant !== 'dot' ? displayValueFromHook : undefined;
const ownerState = {
...props,
badgeContent,
invisible,
max,
displayValue,
showZero,
anchorOrigin,
color,
overlap,
variant,
};
const classes = useUtilityClasses(ownerState);
// support both `slots` and `components` for backward compatibility
const externalForwardedProps = {
slots: {
root: slots?.root ?? components.Root,
badge: slots?.badge ?? components.Badge,
},
slotProps: {
root: slotProps?.root ?? componentsProps.root,
badge: slotProps?.badge ?? componentsProps.badge,
},
};
const [RootSlot, rootProps] = useSlot('root', {
elementType: BadgeRoot,
externalForwardedProps: {
...externalForwardedProps,
...other,
},
ownerState,
className: clsx(classes.root, className),
ref,
additionalProps: {
as: component,
},
});
const [BadgeSlot, badgeProps] = useSlot('badge', {
elementType: BadgeBadge,
externalForwardedProps,
ownerState,
className: classes.badge,
});
return (
<RootSlot {...rootProps}>
{children}
<BadgeSlot {...badgeProps}>{displayValue}</BadgeSlot>
</RootSlot>
);
});
Badge.propTypes /* remove-proptypes */ = {
// ┌────────────────────────────── Warning ──────────────────────────────┐
// │ These PropTypes are generated from the TypeScript type definitions. │
// │ To update them, edit the d.ts file and run `pnpm proptypes`. │
// └─────────────────────────────────────────────────────────────────────┘
/**
* The anchor of the badge.
* @default {
* vertical: 'top',
* horizontal: 'right',
* }
*/
anchorOrigin: PropTypes.shape({
horizontal: PropTypes.oneOf(['left', 'right']),
vertical: PropTypes.oneOf(['bottom', 'top']),
}),
/**
* The content rendered within the badge.
*/
badgeContent: PropTypes.node,
/**
* The badge will be added relative to this node.
*/
children: PropTypes.node,
/**
* Override or extend the styles applied to the component.
*/
classes: PropTypes.object,
/**
* @ignore
*/
className: PropTypes.string,
/**
* The color of the component.
* It supports both default and custom theme colors, which can be added as shown in the
* [palette customization guide](https://mui.com/material-ui/customization/palette/#custom-colors).
* @default 'default'
*/
color: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([
PropTypes.oneOf(['default', 'primary', 'secondary', 'error', 'info', 'success', 'warning']),
PropTypes.string,
]),
/**
* The component used for the root node.
* Either a string to use a HTML element or a component.
*/
component: PropTypes.elementType,
/**
* The components used for each slot inside.
*
* @deprecated use the `slots` prop instead. This prop will be removed in a future major release. See [Migrating from deprecated APIs](https://mui.com/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*
* @default {}
*/
components: PropTypes.shape({
Badge: PropTypes.elementType,
Root: PropTypes.elementType,
}),
/**
* The extra props for the slot components.
* You can override the existing props or add new ones.
*
* @deprecated use the `slotProps` prop instead. This prop will be removed in a future major release. See [Migrating from deprecated APIs](https://mui.com/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*
* @default {}
*/
componentsProps: PropTypes.shape({
badge: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
root: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
}),
/**
* If `true`, the badge is invisible.
* @default false
*/
invisible: PropTypes.bool,
/**
* Max count to show.
* @default 99
*/
max: PropTypes.number,
/**
* Wrapped shape the badge should overlap.
* @default 'rectangular'
*/
overlap: PropTypes.oneOf(['circular', 'rectangular']),
/**
* Controls whether the badge is hidden when `badgeContent` is zero.
* @default false
*/
showZero: PropTypes.bool,
/**
* The props used for each slot inside.
* @default {}
*/
slotProps: PropTypes.shape({
badge: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
root: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
}),
/**
* The components used for each slot inside.
* @default {}
*/
slots: PropTypes.shape({
badge: PropTypes.elementType,
root: 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 'standard'
*/
variant: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([
PropTypes.oneOf(['dot', 'standard']),
PropTypes.string,
]),
};
export default Badge;

View File

@@ -0,0 +1,21 @@
import Badge from '@mui/material/Badge';
function classesTest() {
return (
<Badge badgeContent={4} classes={{ badge: 'testBadgeClassName', colorInfo: 'colorInfoClass' }}>
<div>Hello World</div>
</Badge>
);
}
<Badge anchorOrigin={{ vertical: 'bottom' }} />;
<Badge anchorOrigin={{ horizontal: 'left' }} />;
<Badge
slotProps={{
badge: {
sx: {
color: 'red',
},
},
}}
/>;

View File

@@ -0,0 +1,409 @@
import * as React from 'react';
import { expect } from 'chai';
import { createRenderer, screen } from '@mui/internal-test-utils';
import Badge, { badgeClasses as classes } from '@mui/material/Badge';
import describeConformance from '../../test/describeConformance';
function findBadgeRoot(container) {
return container.firstChild;
}
function findBadge(container) {
return findBadgeRoot(container).querySelector('span');
}
describe('<Badge />', () => {
const { render } = createRenderer();
const defaultProps = {
children: (
<div className="unique" data-testid="children">
Hello World
</div>
),
badgeContent: 10,
};
describeConformance(
<Badge>
<div />
</Badge>,
() => ({
classes,
inheritComponent: 'span',
render,
refInstanceof: window.HTMLSpanElement,
muiName: 'MuiBadge',
testVariantProps: { color: 'secondary', variant: 'dot' },
slots: {
root: {
expectedClassName: classes.root,
},
badge: {
expectedClassName: classes.badge,
},
},
}),
);
it('renders children and badgeContent', () => {
const children = <div id="child" data-testid="child" />;
const badge = <div id="badge" data-testid="badge" />;
const { container } = render(<Badge badgeContent={badge}>{children}</Badge>);
expect(container.firstChild).to.contain(screen.getByTestId('child'));
expect(container.firstChild).to.contain(screen.getByTestId('badge'));
});
it('applies customized classes', () => {
const customClasses = {
root: 'test-root',
anchorOriginTopRight: 'test-anchorOriginTopRight',
anchorOriginTopRightCircular: 'test-anchorOriginTopRightCircular',
badge: 'test-badge',
colorSecondary: 'test-colorSecondary',
dot: 'test-dot',
invisible: 'test-invisible',
overlapCircular: 'test-overlapCircular',
};
const { container } = render(
<Badge
{...defaultProps}
variant="dot"
overlap="circular"
invisible
color="secondary"
classes={customClasses}
/>,
);
expect(findBadgeRoot(container)).to.have.class(customClasses.root);
expect(findBadge(container)).to.have.class(customClasses.anchorOriginTopRight);
expect(findBadge(container)).to.have.class(customClasses.anchorOriginTopRightCircular);
expect(findBadge(container)).to.have.class(customClasses.badge);
expect(findBadge(container)).to.have.class(customClasses.colorSecondary);
expect(findBadge(container)).to.have.class(customClasses.dot);
expect(findBadge(container)).to.have.class(customClasses.invisible);
expect(findBadge(container)).to.have.class(customClasses.overlapCircular);
});
it('renders children', () => {
const { container } = render(<Badge className="testClassName" {...defaultProps} />);
expect(container.firstChild).to.contain(screen.getByTestId('children'));
});
describe('prop: color', () => {
it('should have the colorPrimary class when color="primary"', () => {
const { container } = render(<Badge {...defaultProps} color="primary" />);
expect(findBadge(container)).to.have.class(classes.colorPrimary);
});
it('should have the colorSecondary class when color="secondary"', () => {
const { container } = render(<Badge {...defaultProps} color="secondary" />);
expect(findBadge(container)).to.have.class(classes.colorSecondary);
});
it('should have the colorError class when color="error"', () => {
const { container } = render(<Badge {...defaultProps} color="error" />);
expect(findBadge(container)).to.have.class(classes.colorError);
});
});
describe('prop: invisible', () => {
it('should default to false', () => {
const { container } = render(<Badge {...defaultProps} />);
expect(findBadge(container)).not.to.have.class(classes.invisible);
});
it('should render without the invisible class when set to false', () => {
const { container } = render(<Badge {...defaultProps} invisible={false} />);
expect(findBadge(container)).not.to.have.class(classes.invisible);
});
it('should render with the invisible class when set to true', () => {
const { container } = render(<Badge {...defaultProps} invisible />);
expect(findBadge(container)).to.have.class(classes.invisible);
});
it('should render with the invisible class when empty and not dot', () => {
let container;
container = render(<Badge {...defaultProps} badgeContent={null} />).container;
expect(findBadge(container)).to.have.class(classes.invisible);
container = render(<Badge {...defaultProps} badgeContent={undefined} />).container;
expect(findBadge(container)).to.have.class(classes.invisible);
container = render(
<Badge {...defaultProps} badgeContent={undefined} variant="dot" />,
).container;
expect(findBadge(container)).not.to.have.class(classes.invisible);
});
it('should render with invisible class when invisible and showZero are set to false and content is 0', () => {
const { container } = render(<Badge badgeContent={0} showZero={false} invisible={false} />);
expect(findBadge(container)).to.have.class(classes.invisible);
expect(findBadge(container)).to.have.text('');
});
it('should not render with invisible class when invisible and showZero are set to false and content is not 0', () => {
const { container } = render(<Badge badgeContent={1} showZero={false} invisible={false} />);
expect(findBadge(container)).not.to.have.class(classes.invisible);
expect(findBadge(container)).to.have.text('1');
});
});
describe('prop: showZero', () => {
it('should default to false', () => {
const { container } = render(<Badge {...defaultProps} badgeContent={0} />);
expect(findBadge(container)).to.have.class(classes.invisible);
});
it('should render without the invisible class when false and badgeContent is not 0', () => {
const { container } = render(<Badge {...defaultProps} showZero />);
expect(findBadge(container)).not.to.have.class(classes.invisible);
});
it('should render without the invisible class when true and badgeContent is 0', () => {
const { container } = render(<Badge {...defaultProps} badgeContent={0} showZero />);
expect(findBadge(container)).not.to.have.class(classes.invisible);
});
it('should render with the invisible class when false and badgeContent is 0', () => {
const { container } = render(<Badge {...defaultProps} badgeContent={0} showZero={false} />);
expect(findBadge(container)).to.have.class(classes.invisible);
});
});
describe('prop: variant', () => {
it('should default to standard', () => {
const { container } = render(<Badge {...defaultProps} />);
expect(findBadge(container)).to.have.class(classes.badge);
expect(findBadge(container)).not.to.have.class(classes.dot);
});
it('should render with the standard class when variant="standard"', () => {
const { container } = render(<Badge {...defaultProps} variant="standard" />);
expect(findBadge(container)).to.have.class(classes.badge);
expect(findBadge(container)).not.to.have.class(classes.dot);
});
it('should not render badgeContent when variant="dot"', () => {
const { container } = render(<Badge {...defaultProps} variant="dot" />);
expect(findBadge(container)).to.have.class(classes.badge);
expect(findBadge(container)).to.have.class(classes.dot);
expect(findBadge(container)).to.have.text('');
});
});
describe('prop: max', () => {
it('should default to 99', () => {
const { container } = render(<Badge {...defaultProps} badgeContent={100} />);
expect(findBadge(container)).to.have.text('99+');
});
it('should cap badgeContent', () => {
const { container } = render(<Badge {...defaultProps} badgeContent={1000} max={999} />);
expect(findBadge(container)).to.have.text('999+');
});
it('should not cap if badgeContent and max are equal', () => {
const { container } = render(<Badge {...defaultProps} badgeContent={1000} max={1000} />);
expect(findBadge(container)).to.have.text('1000');
});
it('should not cap if badgeContent is lower than max', () => {
const { container } = render(<Badge {...defaultProps} badgeContent={50} max={1000} />);
expect(findBadge(container)).to.have.text('50');
});
});
describe('prop: anchorOrigin', () => {
it('should apply style for top left rectangular', () => {
const { container } = render(
<Badge {...defaultProps} anchorOrigin={{ horizontal: 'left', vertical: 'top' }} />,
);
expect(findBadge(container)).to.have.class(classes.anchorOriginTopLeftRectangular);
});
it('should apply style for top right rectangular', () => {
const { container } = render(
<Badge {...defaultProps} anchorOrigin={{ horizontal: 'right', vertical: 'top' }} />,
);
expect(findBadge(container)).to.have.class(classes.anchorOriginTopRightRectangular);
});
it('should apply style for bottom left rectangular', () => {
const { container } = render(
<Badge {...defaultProps} anchorOrigin={{ horizontal: 'left', vertical: 'bottom' }} />,
);
expect(findBadge(container)).to.have.class(classes.anchorOriginBottomLeftRectangular);
});
it('should apply style for bottom right rectangular', () => {
const { container } = render(
<Badge {...defaultProps} anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }} />,
);
expect(findBadge(container)).to.have.class(classes.anchorOriginBottomRightRectangular);
});
it('should apply style for bottom right rectangular when only vertical is specified', () => {
const { container } = render(
<Badge {...defaultProps} anchorOrigin={{ vertical: 'bottom' }} />,
);
expect(findBadge(container)).to.have.class(classes.anchorOriginBottomRightRectangular);
});
it('should apply style for top left rectangular when only horizontal is specified', () => {
const { container } = render(
<Badge {...defaultProps} anchorOrigin={{ horizontal: 'left' }} />,
);
expect(findBadge(container)).to.have.class(classes.anchorOriginTopLeftRectangular);
});
it('should apply style for top left circular', () => {
const { container } = render(
<Badge
{...defaultProps}
anchorOrigin={{ horizontal: 'left', vertical: 'top' }}
overlap="circular"
/>,
);
expect(findBadge(container)).to.have.class(classes.anchorOriginTopLeftCircular);
});
it('should apply style for top right circular', () => {
const { container } = render(
<Badge
{...defaultProps}
anchorOrigin={{ horizontal: 'right', vertical: 'top' }}
overlap="circular"
/>,
);
expect(findBadge(container)).to.have.class(classes.anchorOriginTopRightCircular);
});
it('should apply style for bottom left circular', () => {
const { container } = render(
<Badge
{...defaultProps}
anchorOrigin={{ horizontal: 'left', vertical: 'bottom' }}
overlap="circular"
/>,
);
expect(findBadge(container)).to.have.class(classes.anchorOriginBottomLeftCircular);
});
it('should apply style for bottom right circular', () => {
const { container } = render(
<Badge
{...defaultProps}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
overlap="circular"
/>,
);
expect(findBadge(container)).to.have.class(classes.anchorOriginBottomRightCircular);
});
});
describe('prop: components / slots', () => {
it('allows overriding the slots using the components prop', () => {
const CustomRoot = React.forwardRef((props, ref) => {
const { ownerState, ...other } = props;
return <span {...other} ref={ref} data-testid="custom-root" />;
});
const CustomBadge = React.forwardRef((props, ref) => {
const { ownerState, ...other } = props;
return <span {...other} ref={ref} data-testid="custom-badge" />;
});
render(
<Badge
{...defaultProps}
badgeContent={1}
components={{ Root: CustomRoot, Badge: CustomBadge }}
/>,
);
screen.getByTestId('custom-root');
screen.getByTestId('custom-badge');
});
it('allows overriding the slots using the slots prop', () => {
const CustomRoot = React.forwardRef((props, ref) => {
const { ownerState, ...other } = props;
return <span {...other} ref={ref} data-testid="custom-root" />;
});
const CustomBadge = React.forwardRef((props, ref) => {
const { ownerState, ...other } = props;
return <span {...other} ref={ref} data-testid="custom-badge" />;
});
render(
<Badge
{...defaultProps}
badgeContent={1}
slots={{ root: CustomRoot, badge: CustomBadge }}
/>,
);
screen.getByTestId('custom-root');
screen.getByTestId('custom-badge');
});
});
describe('prop: componentsProps / slotProps', () => {
it('allows modifying slots props using the componentsProps prop', () => {
render(
<Badge
{...defaultProps}
badgeContent={1}
componentsProps={{
root: { 'data-testid': 'custom-root' },
badge: { 'data-testid': 'custom-badge' },
}}
/>,
);
screen.getByTestId('custom-root');
screen.getByTestId('custom-badge');
});
it('allows modifying slots props using the slotProps prop', () => {
render(
<Badge
{...defaultProps}
badgeContent={1}
slotProps={{
root: { 'data-testid': 'custom-root' },
badge: { 'data-testid': 'custom-badge' },
}}
/>,
);
screen.getByTestId('custom-root');
screen.getByTestId('custom-badge');
});
});
it('retains anchorOrigin, content, color, max, overlap and variant when invisible is true for consistent disappearing transition', () => {
const { container, setProps } = render(
<Badge {...defaultProps} color="secondary" variant="dot" />,
);
setProps({
badgeContent: 0,
color: 'primary',
variant: 'standard',
overlap: 'circular',
anchorOrigin: {
vertical: 'bottom',
horizontal: 'left',
},
});
expect(findBadge(container)).to.have.text('');
expect(findBadge(container)).to.have.class(classes.colorSecondary);
expect(findBadge(container)).to.have.class(classes.dot);
expect(findBadge(container)).to.have.class(classes.anchorOriginTopRightRectangular);
});
});

View File

@@ -0,0 +1,92 @@
import generateUtilityClasses from '@mui/utils/generateUtilityClasses';
import generateUtilityClass from '@mui/utils/generateUtilityClass';
export interface BadgeClasses {
/** Styles applied to the root element. */
root: string;
/** Styles applied to the badge `span` element. */
badge: string;
/** Styles applied to the badge `span` element if `variant="dot"`. */
dot: string;
/** Styles applied to the badge `span` element if `variant="standard"`. */
standard: string;
/** Styles applied to the badge `span` element if `anchorOrigin={{ 'top', 'right' }}`. */
anchorOriginTopRight: string;
/** Styles applied to the badge `span` element if `anchorOrigin={{ 'bottom', 'right' }}`. */
anchorOriginBottomRight: string;
/** Styles applied to the badge `span` element if `anchorOrigin={{ 'top', 'left' }}`. */
anchorOriginTopLeft: string;
/** Styles applied to the badge `span` element if `anchorOrigin={{ 'bottom', 'left' }}`. */
anchorOriginBottomLeft: string;
/** State class applied to the badge `span` element if `invisible={true}`. */
invisible: string;
/** Styles applied to the badge `span` element if `color="primary"`. */
colorPrimary: string;
/** Styles applied to the badge `span` element if `color="secondary"`. */
colorSecondary: string;
/** Styles applied to the badge `span` element if `color="error"`. */
colorError: string;
/** Styles applied to the badge `span` element if `color="info"`. */
colorInfo: string;
/** Styles applied to the badge `span` element if `color="success"`. */
colorSuccess: string;
/** Styles applied to the badge `span` element if `color="warning"`. */
colorWarning: string;
/** Styles applied to the badge `span` element if `anchorOrigin={{ 'top', 'right' }} overlap="rectangular"`. */
anchorOriginTopRightRectangular: string;
/** Styles applied to the badge `span` element if `anchorOrigin={{ 'bottom', 'right' }} overlap="rectangular"`. */
anchorOriginBottomRightRectangular: string;
/** Styles applied to the badge `span` element if `anchorOrigin={{ 'top', 'left' }} overlap="rectangular"`. */
anchorOriginTopLeftRectangular: string;
/** Styles applied to the badge `span` element if `anchorOrigin={{ 'bottom', 'left' }} overlap="rectangular"`. */
anchorOriginBottomLeftRectangular: string;
/** Styles applied to the badge `span` element if `anchorOrigin={{ 'top', 'right' }} overlap="circular"`. */
anchorOriginTopRightCircular: string;
/** Styles applied to the badge `span` element if `anchorOrigin={{ 'bottom', 'right' }} overlap="circular"`. */
anchorOriginBottomRightCircular: string;
/** Styles applied to the badge `span` element if `anchorOrigin={{ 'top', 'left' }} overlap="circular"`. */
anchorOriginTopLeftCircular: string;
/** Styles applied to the badge `span` element if `anchorOrigin={{ 'bottom', 'left' }} overlap="circular"`. */
anchorOriginBottomLeftCircular: string;
/** Styles applied to the badge `span` element if `overlap="rectangular"`. */
overlapRectangular: string;
/** Styles applied to the badge `span` element if `overlap="circular"`. */
overlapCircular: string;
}
export type BadgeClassKey = keyof BadgeClasses;
export function getBadgeUtilityClass(slot: string): string {
return generateUtilityClass('MuiBadge', slot);
}
const badgeClasses: BadgeClasses = generateUtilityClasses('MuiBadge', [
'root',
'badge',
'dot',
'standard',
'anchorOriginTopRight',
'anchorOriginBottomRight',
'anchorOriginTopLeft',
'anchorOriginBottomLeft',
'invisible',
'colorError',
'colorInfo',
'colorPrimary',
'colorSecondary',
'colorSuccess',
'colorWarning',
'overlapRectangular',
'overlapCircular',
// TODO: v6 remove the overlap value from these class keys
'anchorOriginTopLeftCircular',
'anchorOriginTopLeftRectangular',
'anchorOriginTopRightCircular',
'anchorOriginTopRightRectangular',
'anchorOriginBottomLeftCircular',
'anchorOriginBottomLeftRectangular',
'anchorOriginBottomRightCircular',
'anchorOriginBottomRightRectangular',
]);
export default badgeClasses;

View File

@@ -0,0 +1,5 @@
export { default } from './Badge';
export * from './Badge';
export { default as badgeClasses } from './badgeClasses';
export * from './badgeClasses';

View File

@@ -0,0 +1,4 @@
export { default } from './Badge';
export { default as badgeClasses } from './badgeClasses';
export * from './badgeClasses';

View File

@@ -0,0 +1,38 @@
'use client';
import * as React from 'react';
import usePreviousProps from '@mui/utils/usePreviousProps';
import { UseBadgeParameters, UseBadgeReturnValue } from './useBadge.types';
function useBadge(parameters: UseBadgeParameters): UseBadgeReturnValue {
const {
badgeContent: badgeContentProp,
invisible: invisibleProp = false,
max: maxProp = 99,
showZero = false,
} = parameters;
const prevProps = usePreviousProps({
badgeContent: badgeContentProp,
max: maxProp,
});
let invisible = invisibleProp;
if (invisibleProp === false && badgeContentProp === 0 && !showZero) {
invisible = true;
}
const { badgeContent, max = maxProp } = invisible ? prevProps : parameters;
const displayValue: React.ReactNode =
badgeContent && Number(badgeContent) > max ? `${max}+` : badgeContent;
return {
badgeContent,
invisible,
max,
displayValue,
};
}
export default useBadge;

View File

@@ -0,0 +1,40 @@
export interface UseBadgeParameters {
/**
* The content rendered within the badge.
*/
badgeContent?: React.ReactNode;
/**
* If `true`, the badge is invisible.
* @default false
*/
invisible?: boolean;
/**
* Max count to show.
* @default 99
*/
max?: number;
/**
* Controls whether the badge is hidden when `badgeContent` is zero.
* @default false
*/
showZero?: boolean;
}
export interface UseBadgeReturnValue {
/**
* Defines the content that's displayed inside the badge.
*/
badgeContent: React.ReactNode;
/**
* If `true`, the component will not be visible.
*/
invisible: boolean;
/**
* Maximum number to be displayed in the badge.
*/
max: number;
/**
* Value to be displayed in the badge. If `badgeContent` is greater than `max`, it will return `max+`.
*/
displayValue: React.ReactNode;
}