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,162 @@
import * as React from 'react';
import { SxProps } from '@mui/system';
import { Theme } from '../styles';
import { TransitionProps } from '../transitions/transition';
import { AccordionClasses } from './accordionClasses';
import { OverridableComponent, OverrideProps } from '../OverridableComponent';
import { ExtendPaperTypeMap, PaperProps } from '../Paper/Paper';
import { CreateSlotsAndSlotProps, SlotComponentProps, SlotProps } from '../utils/types';
export interface AccordionSlots {
/**
* The component that renders the root.
* @default Paper
*/
root: React.ElementType;
/**
* The component that renders the heading.
* @default 'h3'
*/
heading: React.ElementType;
/**
* The component that renders the transition.
* [Follow this guide](https://mui.com/material-ui/transitions/#transitioncomponent-prop) to learn more about the requirements for this component.
* @default Collapse
*/
transition: React.ElementType;
/**
* The component that renders the region.
* @default 'div'
*/
region: React.ElementType;
}
export interface AccordionRootSlotPropsOverrides {}
export interface AccordionHeadingSlotPropsOverrides {}
export interface AccordionTransitionSlotPropsOverrides {}
export interface AccordionRegionSlotPropsOverrides {}
export type AccordionSlotsAndSlotProps = CreateSlotsAndSlotProps<
AccordionSlots,
{
/**
* Props forwarded to the root slot.
* By default, the available props are based on the Paper element.
*/
root: SlotProps<
React.ElementType<PaperProps>,
AccordionRootSlotPropsOverrides,
AccordionOwnerState
>;
/**
* Props forwarded to the heading slot.
* By default, the available props are based on the h3 element.
*/
heading: SlotProps<'h3', AccordionHeadingSlotPropsOverrides, AccordionOwnerState>;
/**
* Props forwarded to the transition slot.
* By default, the available props are based on the [Collapse](https://mui.com/material-ui/api/collapse/#props) component.
*/
transition: SlotComponentProps<
React.ElementType,
TransitionProps & AccordionTransitionSlotPropsOverrides,
AccordionOwnerState
>;
/**
* Props forwarded to the region slot.
* By default, the available props are based on the div element.
*/
region: SlotProps<'div', AccordionRegionSlotPropsOverrides, AccordionOwnerState>;
}
>;
export interface AccordionOwnProps {
/**
* The content of the component.
*/
children: NonNullable<React.ReactNode>;
/**
* Override or extend the styles applied to the component.
*/
classes?: Partial<AccordionClasses>;
/**
* If `true`, expands the accordion by default.
* @default false
*/
defaultExpanded?: boolean;
/**
* If `true`, the component is disabled.
* @default false
*/
disabled?: boolean;
/**
* If `true`, it removes the margin between two expanded accordion items and the increase of height.
* @default false
*/
disableGutters?: boolean;
/**
* If `true`, expands the accordion, otherwise collapse it.
* Setting this prop enables control over the accordion.
*/
expanded?: boolean;
/**
* Callback fired when the expand/collapse state is changed.
*
* @param {React.SyntheticEvent} event The event source of the callback. **Warning**: This is a generic event not a change event.
* @param {boolean} expanded The `expanded` state of the accordion.
*/
onChange?: (event: React.SyntheticEvent, expanded: boolean) => void;
/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx?: SxProps<Theme>;
/**
* The component used for the transition.
* [Follow this guide](https://mui.com/material-ui/transitions/#transitioncomponent-prop) to learn more about the requirements for this component.
* @deprecated Use `slots.transition` instead. This prop will be removed in a future major release. See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
TransitionComponent?: React.JSXElementConstructor<
TransitionProps & { children?: React.ReactElement<unknown, any> }
>;
/**
* Props applied to the transition element.
* By default, the element is based on this [`Transition`](https://reactcommunity.org/react-transition-group/transition/) component.
* @deprecated Use `slotProps.transition` instead. This prop will be removed in a future major release. See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
TransitionProps?: TransitionProps;
}
export type AccordionTypeMap<
AdditionalProps = {},
RootComponent extends React.ElementType = 'div',
> = ExtendPaperTypeMap<
{
props: AdditionalProps & AccordionOwnProps & AccordionSlotsAndSlotProps;
defaultComponent: RootComponent;
},
'onChange' | 'classes'
>;
/**
*
* Demos:
*
* - [Accordion](https://mui.com/material-ui/react-accordion/)
*
* API:
*
* - [Accordion API](https://mui.com/material-ui/api/accordion/)
* - inherits [Paper API](https://mui.com/material-ui/api/paper/)
*/
declare const Accordion: OverridableComponent<AccordionTypeMap>;
export type AccordionProps<
RootComponent extends React.ElementType = AccordionTypeMap['defaultComponent'],
AdditionalProps = {},
> = OverrideProps<AccordionTypeMap<AdditionalProps, RootComponent>, RootComponent> & {
component?: React.ElementType;
};
export interface AccordionOwnerState extends AccordionProps {}
export default Accordion;

View File

@@ -0,0 +1,358 @@
'use client';
import * as React from 'react';
import { isFragment } from 'react-is';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import chainPropTypes from '@mui/utils/chainPropTypes';
import composeClasses from '@mui/utils/composeClasses';
import { styled } from '../zero-styled';
import memoTheme from '../utils/memoTheme';
import { useDefaultProps } from '../DefaultPropsProvider';
import Collapse from '../Collapse';
import Paper from '../Paper';
import AccordionContext from './AccordionContext';
import useControlled from '../utils/useControlled';
import useSlot from '../utils/useSlot';
import accordionClasses, { getAccordionUtilityClass } from './accordionClasses';
const useUtilityClasses = (ownerState) => {
const { classes, square, expanded, disabled, disableGutters } = ownerState;
const slots = {
root: [
'root',
!square && 'rounded',
expanded && 'expanded',
disabled && 'disabled',
!disableGutters && 'gutters',
],
heading: ['heading'],
region: ['region'],
};
return composeClasses(slots, getAccordionUtilityClass, classes);
};
const AccordionRoot = styled(Paper, {
name: 'MuiAccordion',
slot: 'Root',
overridesResolver: (props, styles) => {
const { ownerState } = props;
return [
{ [`& .${accordionClasses.region}`]: styles.region },
styles.root,
!ownerState.square && styles.rounded,
!ownerState.disableGutters && styles.gutters,
];
},
})(
memoTheme(({ theme }) => {
const transition = {
duration: theme.transitions.duration.shortest,
};
return {
position: 'relative',
transition: theme.transitions.create(['margin'], transition),
overflowAnchor: 'none', // Keep the same scrolling position
'&::before': {
position: 'absolute',
left: 0,
top: -1,
right: 0,
height: 1,
content: '""',
opacity: 1,
backgroundColor: (theme.vars || theme).palette.divider,
transition: theme.transitions.create(['opacity', 'background-color'], transition),
},
'&:first-of-type': {
'&::before': {
display: 'none',
},
},
[`&.${accordionClasses.expanded}`]: {
'&::before': {
opacity: 0,
},
'&:first-of-type': {
marginTop: 0,
},
'&:last-of-type': {
marginBottom: 0,
},
'& + &': {
'&::before': {
display: 'none',
},
},
},
[`&.${accordionClasses.disabled}`]: {
backgroundColor: (theme.vars || theme).palette.action.disabledBackground,
},
};
}),
memoTheme(({ theme }) => ({
variants: [
{
props: (props) => !props.square,
style: {
borderRadius: 0,
'&:first-of-type': {
borderTopLeftRadius: (theme.vars || theme).shape.borderRadius,
borderTopRightRadius: (theme.vars || theme).shape.borderRadius,
},
'&:last-of-type': {
borderBottomLeftRadius: (theme.vars || theme).shape.borderRadius,
borderBottomRightRadius: (theme.vars || theme).shape.borderRadius,
// Fix a rendering issue on Edge
'@supports (-ms-ime-align: auto)': {
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
},
},
},
},
{
props: (props) => !props.disableGutters,
style: {
[`&.${accordionClasses.expanded}`]: {
margin: '16px 0',
},
},
},
],
})),
);
const AccordionHeading = styled('h3', {
name: 'MuiAccordion',
slot: 'Heading',
})({
all: 'unset',
});
const AccordionRegion = styled('div', {
name: 'MuiAccordion',
slot: 'Region',
})({});
const Accordion = React.forwardRef(function Accordion(inProps, ref) {
const props = useDefaultProps({ props: inProps, name: 'MuiAccordion' });
const {
children: childrenProp,
className,
defaultExpanded = false,
disabled = false,
disableGutters = false,
expanded: expandedProp,
onChange,
square = false,
slots = {},
slotProps = {},
TransitionComponent: TransitionComponentProp,
TransitionProps: TransitionPropsProp,
...other
} = props;
const [expanded, setExpandedState] = useControlled({
controlled: expandedProp,
default: defaultExpanded,
name: 'Accordion',
state: 'expanded',
});
const handleChange = React.useCallback(
(event) => {
setExpandedState(!expanded);
if (onChange) {
onChange(event, !expanded);
}
},
[expanded, onChange, setExpandedState],
);
const [summary, ...children] = React.Children.toArray(childrenProp);
const contextValue = React.useMemo(
() => ({ expanded, disabled, disableGutters, toggle: handleChange }),
[expanded, disabled, disableGutters, handleChange],
);
const ownerState = {
...props,
square,
disabled,
disableGutters,
expanded,
};
const classes = useUtilityClasses(ownerState);
const backwardCompatibleSlots = { transition: TransitionComponentProp, ...slots };
const backwardCompatibleSlotProps = { transition: TransitionPropsProp, ...slotProps };
const externalForwardedProps = {
slots: backwardCompatibleSlots,
slotProps: backwardCompatibleSlotProps,
};
const [RootSlot, rootProps] = useSlot('root', {
elementType: AccordionRoot,
externalForwardedProps: {
...externalForwardedProps,
...other,
},
className: clsx(classes.root, className),
shouldForwardComponentProp: true,
ownerState,
ref,
additionalProps: {
square,
},
});
const [AccordionHeadingSlot, accordionProps] = useSlot('heading', {
elementType: AccordionHeading,
externalForwardedProps,
className: classes.heading,
ownerState,
});
const [TransitionSlot, transitionProps] = useSlot('transition', {
elementType: Collapse,
externalForwardedProps,
ownerState,
});
const [AccordionRegionSlot, accordionRegionProps] = useSlot('region', {
elementType: AccordionRegion,
externalForwardedProps,
ownerState,
className: classes.region,
additionalProps: {
'aria-labelledby': summary.props.id,
id: summary.props['aria-controls'],
role: 'region',
},
});
return (
<RootSlot {...rootProps}>
<AccordionHeadingSlot {...accordionProps}>
<AccordionContext.Provider value={contextValue}>{summary}</AccordionContext.Provider>
</AccordionHeadingSlot>
<TransitionSlot in={expanded} timeout="auto" {...transitionProps}>
<AccordionRegionSlot {...accordionRegionProps}>{children}</AccordionRegionSlot>
</TransitionSlot>
</RootSlot>
);
});
Accordion.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 content of the component.
*/
children: chainPropTypes(PropTypes.node.isRequired, (props) => {
const summary = React.Children.toArray(props.children)[0];
if (isFragment(summary)) {
return new Error(
"MUI: The Accordion doesn't accept a Fragment as a child. " +
'Consider providing an array instead.',
);
}
if (!React.isValidElement(summary)) {
return new Error('MUI: Expected the first child of Accordion to be a valid element.');
}
return null;
}),
/**
* Override or extend the styles applied to the component.
*/
classes: PropTypes.object,
/**
* @ignore
*/
className: PropTypes.string,
/**
* If `true`, expands the accordion by default.
* @default false
*/
defaultExpanded: PropTypes.bool,
/**
* If `true`, the component is disabled.
* @default false
*/
disabled: PropTypes.bool,
/**
* If `true`, it removes the margin between two expanded accordion items and the increase of height.
* @default false
*/
disableGutters: PropTypes.bool,
/**
* If `true`, expands the accordion, otherwise collapse it.
* Setting this prop enables control over the accordion.
*/
expanded: PropTypes.bool,
/**
* Callback fired when the expand/collapse state is changed.
*
* @param {React.SyntheticEvent} event The event source of the callback. **Warning**: This is a generic event not a change event.
* @param {boolean} expanded The `expanded` state of the accordion.
*/
onChange: PropTypes.func,
/**
* The props used for each slot inside.
* @default {}
*/
slotProps: PropTypes.shape({
heading: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
region: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
root: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
transition: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
}),
/**
* The components used for each slot inside.
* @default {}
*/
slots: PropTypes.shape({
heading: PropTypes.elementType,
region: PropTypes.elementType,
root: PropTypes.elementType,
transition: PropTypes.elementType,
}),
/**
* If `true`, rounded corners are disabled.
* @default false
*/
square: PropTypes.bool,
/**
* 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 component used for the transition.
* [Follow this guide](https://mui.com/material-ui/transitions/#transitioncomponent-prop) to learn more about the requirements for this component.
* @deprecated Use `slots.transition` instead. This prop will be removed in a future major release. See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
TransitionComponent: PropTypes.elementType,
/**
* Props applied to the transition element.
* By default, the element is based on this [`Transition`](https://reactcommunity.org/react-transition-group/transition/) component.
* @deprecated Use `slotProps.transition` instead. This prop will be removed in a future major release. See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
TransitionProps: PropTypes.object,
};
export default Accordion;

View File

@@ -0,0 +1,104 @@
import * as React from 'react';
import { expectType } from '@mui/types';
import { mergeSlotProps } from '@mui/material/utils';
import Accordion, { AccordionProps } from '@mui/material/Accordion';
function testOnChange() {
function handleAccordionChange(event: React.SyntheticEvent, tabsValue: unknown) {}
<Accordion onChange={handleAccordionChange}>
<div />
</Accordion>;
function handleElementChange(event: React.ChangeEvent) {}
<Accordion
// @ts-expect-error internally it's whatever even lead to a change in value
onChange={handleElementChange}
>
<div />
</Accordion>;
}
const CustomComponent: React.FC<{ prop1: string; prop2: number }> = function CustomComponent() {
return <div />;
};
const requiredProps = {
children: <div />,
};
const AccordionComponentTest = () => {
return (
<div>
<Accordion {...requiredProps} />
<Accordion {...requiredProps} component="legend" />
<Accordion
{...requiredProps}
component="a"
href="test"
onClick={(event) => {
expectType<React.MouseEvent<HTMLAnchorElement, MouseEvent>, typeof event>(event);
}}
/>
{/* @ts-expect-error */}
<Accordion {...requiredProps} component="a" incorrectAttribute="url" />
{/* @ts-expect-error */}
<Accordion {...requiredProps} component="div" href="url" />
<Accordion {...requiredProps} component={CustomComponent} prop1="1" prop2={12} />
{/* @ts-expect-error */}
<Accordion {...requiredProps} component={CustomComponent} prop1="1" />
{/* @ts-expect-error */}
<Accordion {...requiredProps} component={CustomComponent} prop1="1" prop2="12" />
</div>
);
};
// slotProps type test. Changing heading level.
<Accordion slotProps={{ heading: { component: 'h4' } }}>
<div />
</Accordion>;
function Custom(props: AccordionProps) {
const { slotProps, ...other } = props;
return (
<Accordion
slotProps={{
...slotProps,
transition: (ownerState) => {
const transitionProps =
typeof slotProps?.transition === 'function'
? slotProps.transition(ownerState)
: slotProps?.transition;
return {
...transitionProps,
onExited: (node) => {
transitionProps?.onExited?.(node);
},
};
},
}}
{...other}
>
test
</Accordion>
);
}
function Custom2(props: AccordionProps) {
const { slotProps, ...other } = props;
return (
<Accordion
slotProps={{
...slotProps,
transition: mergeSlotProps(slotProps?.transition, {
onExited: (node) => {
expectType<HTMLElement, typeof node>(node);
},
}),
}}
{...other}
>
test
</Accordion>
);
}

View File

@@ -0,0 +1,334 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { expect } from 'chai';
import { spy } from 'sinon';
import { createRenderer, fireEvent, reactMajor, screen } from '@mui/internal-test-utils';
import Accordion, { accordionClasses as classes } from '@mui/material/Accordion';
import Paper from '@mui/material/Paper';
import Collapse from '@mui/material/Collapse';
import Fade from '@mui/material/Fade';
import Slide from '@mui/material/Slide';
import Grow from '@mui/material/Grow';
import Zoom from '@mui/material/Zoom';
import AccordionSummary from '@mui/material/AccordionSummary';
import describeConformance from '../../test/describeConformance';
function NoTransition(props) {
const { children, in: inProp } = props;
if (!inProp) {
return null;
}
return children;
}
const CustomPaper = React.forwardRef(({ square, ...props }, ref) => <Paper ref={ref} {...props} />);
describe('<Accordion />', () => {
const { render } = createRenderer();
const minimalChildren = [<AccordionSummary key="header">Header</AccordionSummary>];
describeConformance(<Accordion>{minimalChildren}</Accordion>, () => ({
classes,
inheritComponent: Paper,
render,
refInstanceof: window.HTMLDivElement,
muiName: 'MuiAccordion',
testVariantProps: { variant: 'rounded' },
slots: {
transition: {
testWithElement: null,
},
heading: {
testWithElement: 'h4',
expectedClassName: classes.heading,
},
root: {
expectedClassName: classes.root,
testWithElement: CustomPaper,
},
region: {
expectedClassName: classes.region,
testWithElement: 'div',
},
},
skip: ['componentProp', 'componentsProp'],
}));
it('should render and not be controlled', () => {
const { container } = render(<Accordion>{minimalChildren}</Accordion>);
expect(container.firstChild).not.to.have.class(classes.expanded);
});
it('should handle defaultExpanded prop', () => {
const { container } = render(<Accordion defaultExpanded>{minimalChildren}</Accordion>);
expect(container.firstChild).to.have.class(classes.expanded);
});
it('should render the summary and collapse elements', () => {
render(
<Accordion>
<AccordionSummary>Summary</AccordionSummary>
<div id="panel-content">Hello</div>
</Accordion>,
);
expect(screen.getByText('Summary')).toBeVisible();
expect(screen.getByRole('button')).to.have.attribute('aria-expanded', 'false');
});
it('should be controlled', () => {
const { container, setProps } = render(
<Accordion expanded TransitionComponent={NoTransition}>
{minimalChildren}
</Accordion>,
);
const panel = container.firstChild;
expect(panel).to.have.class(classes.expanded);
setProps({ expanded: false });
expect(panel).not.to.have.class(classes.expanded);
});
it('should call onChange when clicking the summary element', () => {
const handleChange = spy();
render(
<Accordion onChange={handleChange} TransitionComponent={NoTransition}>
{minimalChildren}
</Accordion>,
);
fireEvent.click(screen.getByText('Header'));
expect(handleChange.callCount).to.equal(1);
});
it('when controlled should call the onChange', () => {
const handleChange = spy();
render(
<Accordion onChange={handleChange} expanded>
{minimalChildren}
</Accordion>,
);
fireEvent.click(screen.getByText('Header'));
expect(handleChange.callCount).to.equal(1);
expect(handleChange.args[0][1]).to.equal(false);
});
it('when undefined onChange and controlled should not call the onChange', () => {
const handleChange = spy();
const { setProps } = render(
<Accordion onChange={handleChange} expanded>
{minimalChildren}
</Accordion>,
);
setProps({ onChange: undefined });
fireEvent.click(screen.getByText('Header'));
expect(handleChange.callCount).to.equal(0);
});
it('when disabled should have the disabled class', () => {
const { container } = render(<Accordion disabled>{minimalChildren}</Accordion>);
expect(container.firstChild).to.have.class(classes.disabled);
});
it('should handle the TransitionComponent prop', () => {
function NoTransitionCollapse(props) {
return props.in ? <div>{props.children}</div> : null;
}
NoTransitionCollapse.propTypes = {
children: PropTypes.node,
in: PropTypes.bool,
};
function CustomContent() {
return <div>Hello</div>;
}
const { setProps } = render(
<Accordion expanded TransitionComponent={NoTransitionCollapse}>
<AccordionSummary />
<CustomContent />
</Accordion>,
);
// Collapse is initially shown
expect(screen.getByText('Hello')).toBeVisible();
// Hide the collapse
setProps({ expanded: false });
expect(screen.queryByText('Hello')).to.equal(null);
});
it('should handle the `square` prop', () => {
const { container } = render(<Accordion square>{minimalChildren}</Accordion>);
expect(container.firstChild).not.toHaveComputedStyle({
borderBottomLeftRadius: '4px',
borderBottomRightRadius: '4px',
borderTopLeftRadius: '4px',
borderTopRightRadius: '4px',
});
});
it('when `square` prop is passed, it should not have the rounded class', () => {
const { container } = render(<Accordion square>{minimalChildren}</Accordion>);
expect(container.firstChild).not.to.have.class(classes.rounded);
});
describe('prop: children', () => {
describe.skipIf(reactMajor >= 19)('first child', () => {
beforeEach(() => {
PropTypes.resetWarningCache();
});
it('requires at least one child', () => {
expect(() => {
PropTypes.checkPropTypes(
Accordion.propTypes,
{ classes: {}, children: [] },
'prop',
'MockedName',
);
}).toErrorDev(['MUI: Expected the first child']);
});
it('needs a valid element as the first child', () => {
expect(() => {
PropTypes.checkPropTypes(
Accordion.propTypes,
{
classes: {},
// eslint-disable-next-line react/jsx-no-useless-fragment
children: <React.Fragment />,
},
'prop',
'MockedName',
);
}).toErrorDev(["MUI: The Accordion doesn't accept a Fragment"]);
});
});
it('should accept empty content', () => {
render(
<Accordion>
<AccordionSummary />
{null}
</Accordion>,
);
});
});
it('should warn when switching from controlled to uncontrolled', () => {
const { setProps } = render(
<Accordion expanded TransitionComponent={NoTransition}>
{minimalChildren}
</Accordion>,
);
expect(() => setProps({ expanded: undefined })).to.toErrorDev(
'MUI: A component is changing the controlled expanded state of Accordion to be uncontrolled.',
);
});
it('should warn when switching between uncontrolled to controlled', () => {
const { setProps } = render(
<Accordion TransitionComponent={NoTransition}>{minimalChildren}</Accordion>,
);
expect(() => setProps({ expanded: true })).toErrorDev(
'MUI: A component is changing the uncontrolled expanded state of Accordion to be controlled.',
);
});
describe('prop: TransitionProps', () => {
it('should apply properties to the Transition component', () => {
render(
<Accordion TransitionProps={{ 'data-testid': 'transition-testid' }}>
{minimalChildren}
</Accordion>,
);
expect(screen.getByTestId('transition-testid')).not.to.equal(null);
});
});
describe('details unmounting behavior', () => {
it('does not unmount by default', () => {
render(
<Accordion expanded={false}>
<AccordionSummary>Summary</AccordionSummary>
<div data-testid="details">Details</div>
</Accordion>,
);
expect(screen.queryByTestId('details')).not.to.equal(null);
});
it('unmounts if opted in via slotProps.transition', () => {
render(
<Accordion expanded={false} slotProps={{ transition: { unmountOnExit: true } }}>
<AccordionSummary>Summary</AccordionSummary>
<div data-testid="details">Details</div>
</Accordion>,
);
expect(screen.queryByTestId('details')).to.equal(null);
});
});
describe('should not forward ownerState prop to the underlying DOM element when using transition slot', () => {
const transitions = [
{
component: Collapse,
name: 'Collapse',
},
{
component: Fade,
name: 'Fade',
},
{
component: Grow,
name: 'Grow',
},
{
component: Slide,
name: 'Slide',
},
{
component: Zoom,
name: 'Zoom',
},
];
transitions.forEach((transition) => {
it(`${transition.name}`, () => {
render(
<Accordion
defaultExpanded
slots={{
transition: transition.component,
}}
slotProps={{ transition: { timeout: 400 } }}
>
<AccordionSummary>Summary</AccordionSummary>
Details
</Accordion>,
);
expect(screen.getByRole('region')).not.to.have.attribute('ownerstate');
});
});
});
it('should allow custom role for region slot via slotProps', () => {
render(
<Accordion expanded slotProps={{ region: { role: 'list', 'data-testid': 'region-slot' } }}>
<AccordionSummary>Summary</AccordionSummary>
Details
</Accordion>,
);
expect(screen.getByTestId('region-slot')).to.have.attribute('role', 'list');
});
});

View File

@@ -0,0 +1,14 @@
'use client';
import * as React from 'react';
/**
* @ignore - internal component.
* @type {React.Context<{} | {expanded: boolean, disabled: boolean, toggle: () => void}>}
*/
const AccordionContext = React.createContext({});
if (process.env.NODE_ENV !== 'production') {
AccordionContext.displayName = 'AccordionContext';
}
export default AccordionContext;

View File

@@ -0,0 +1,37 @@
import generateUtilityClasses from '@mui/utils/generateUtilityClasses';
import generateUtilityClass from '@mui/utils/generateUtilityClass';
export interface AccordionClasses {
/** Styles applied to the root element. */
root: string;
/** Styles applied to the heading element. */
heading: string;
/** Styles applied to the root element unless `square={true}`. */
rounded: string;
/** State class applied to the root element if `expanded={true}`. */
expanded: string;
/** State class applied to the root element if `disabled={true}`. */
disabled: string;
/** Styles applied to the root element unless `disableGutters={true}`. */
gutters: string;
/** Styles applied to the region element, the container of the children. */
region: string;
}
export type AccordionClassKey = keyof AccordionClasses;
export function getAccordionUtilityClass(slot: string): string {
return generateUtilityClass('MuiAccordion', slot);
}
const accordionClasses: AccordionClasses = generateUtilityClasses('MuiAccordion', [
'root',
'heading',
'rounded',
'expanded',
'disabled',
'gutters',
'region',
]);
export default accordionClasses;

View File

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

View File

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

View File

@@ -0,0 +1,37 @@
import * as React from 'react';
import { SxProps } from '@mui/system';
import { Theme } from '../styles';
import { InternalStandardProps as StandardProps } from '../internal';
import { AccordionActionsClasses } from './accordionActionsClasses';
export interface AccordionActionsProps extends StandardProps<React.HTMLAttributes<HTMLDivElement>> {
/**
* The content of the component.
*/
children?: React.ReactNode;
/**
* Override or extend the styles applied to the component.
*/
classes?: Partial<AccordionActionsClasses>;
/**
* If `true`, the actions do not have additional margin.
* @default false
*/
disableSpacing?: boolean;
/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx?: SxProps<Theme>;
}
/**
*
* Demos:
*
* - [Accordion](https://mui.com/material-ui/react-accordion/)
*
* API:
*
* - [AccordionActions API](https://mui.com/material-ui/api/accordion-actions/)
*/
export default function AccordionActions(props: AccordionActionsProps): React.JSX.Element;

View File

@@ -0,0 +1,94 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import composeClasses from '@mui/utils/composeClasses';
import { styled } from '../zero-styled';
import { useDefaultProps } from '../DefaultPropsProvider';
import { getAccordionActionsUtilityClass } from './accordionActionsClasses';
const useUtilityClasses = (ownerState) => {
const { classes, disableSpacing } = ownerState;
const slots = {
root: ['root', !disableSpacing && 'spacing'],
};
return composeClasses(slots, getAccordionActionsUtilityClass, classes);
};
const AccordionActionsRoot = styled('div', {
name: 'MuiAccordionActions',
slot: 'Root',
overridesResolver: (props, styles) => {
const { ownerState } = props;
return [styles.root, !ownerState.disableSpacing && styles.spacing];
},
})({
display: 'flex',
alignItems: 'center',
padding: 8,
justifyContent: 'flex-end',
variants: [
{
props: (props) => !props.disableSpacing,
style: {
'& > :not(style) ~ :not(style)': {
marginLeft: 8,
},
},
},
],
});
const AccordionActions = React.forwardRef(function AccordionActions(inProps, ref) {
const props = useDefaultProps({ props: inProps, name: 'MuiAccordionActions' });
const { className, disableSpacing = false, ...other } = props;
const ownerState = { ...props, disableSpacing };
const classes = useUtilityClasses(ownerState);
return (
<AccordionActionsRoot
className={clsx(classes.root, className)}
ref={ref}
ownerState={ownerState}
{...other}
/>
);
});
AccordionActions.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 content of the component.
*/
children: PropTypes.node,
/**
* Override or extend the styles applied to the component.
*/
classes: PropTypes.object,
/**
* @ignore
*/
className: PropTypes.string,
/**
* If `true`, the actions do not have additional margin.
* @default false
*/
disableSpacing: PropTypes.bool,
/**
* 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,
]),
};
export default AccordionActions;

View File

@@ -0,0 +1,42 @@
import { createRenderer, isJsdom } from '@mui/internal-test-utils';
import AccordionActions, {
accordionActionsClasses as classes,
} from '@mui/material/AccordionActions';
import Button from '@mui/material/Button';
import { expect } from 'chai';
import describeConformance from '../../test/describeConformance';
describe('<AccordionActions />', () => {
const { render } = createRenderer();
describeConformance(<AccordionActions>Conformance</AccordionActions>, () => ({
classes,
inheritComponent: 'div',
render,
refInstanceof: window.HTMLDivElement,
muiName: 'MuiAccordionActions',
testVariantProps: { disableSpacing: true },
skip: ['componentProp', 'componentsProp'],
}));
it.skipIf(isJsdom())('should apply margin to all children but the first one', function test() {
const { container } = render(
<AccordionActions>
<Button data-testid="child-1">Agree</Button>
<Button data-testid="child-2" href="#">
Agree
</Button>
<Button data-testid="child-3" component="span">
Agree
</Button>
<div data-testid="child-4" />
</AccordionActions>,
);
const children = container.querySelectorAll('[data-testid^="child-"]');
expect(children[0]).toHaveComputedStyle({ marginLeft: '0px' });
expect(children[1]).toHaveComputedStyle({ marginLeft: '8px' });
expect(children[2]).toHaveComputedStyle({ marginLeft: '8px' });
expect(children[3]).toHaveComputedStyle({ marginLeft: '8px' });
});
});

View File

@@ -0,0 +1,22 @@
import generateUtilityClasses from '@mui/utils/generateUtilityClasses';
import generateUtilityClass from '@mui/utils/generateUtilityClass';
export interface AccordionActionsClasses {
/** Styles applied to the root element. */
root: string;
/** Styles applied to the root element unless `disableSpacing={true}`. */
spacing: string;
}
export type AccordionActionsClassKey = keyof AccordionActionsClasses;
export function getAccordionActionsUtilityClass(slot: string): string {
return generateUtilityClass('MuiAccordionActions', slot);
}
const accordionActionsClasses: AccordionActionsClasses = generateUtilityClasses(
'MuiAccordionActions',
['root', 'spacing'],
);
export default accordionActionsClasses;

View File

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

View File

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

View File

@@ -0,0 +1,32 @@
import * as React from 'react';
import { SxProps } from '@mui/system';
import { Theme } from '../styles';
import { InternalStandardProps as StandardProps } from '../internal';
import { AccordionDetailsClasses } from './accordionDetailsClasses';
export interface AccordionDetailsProps extends StandardProps<React.HTMLAttributes<HTMLDivElement>> {
/**
* The content of the component.
*/
children?: React.ReactNode;
/**
* Override or extend the styles applied to the component.
*/
classes?: Partial<AccordionDetailsClasses>;
/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx?: SxProps<Theme>;
}
/**
*
* Demos:
*
* - [Accordion](https://mui.com/material-ui/react-accordion/)
*
* API:
*
* - [AccordionDetails API](https://mui.com/material-ui/api/accordion-details/)
*/
export default function AccordionDetails(props: AccordionDetailsProps): React.JSX.Element;

View File

@@ -0,0 +1,73 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import composeClasses from '@mui/utils/composeClasses';
import { styled } from '../zero-styled';
import memoTheme from '../utils/memoTheme';
import { useDefaultProps } from '../DefaultPropsProvider';
import { getAccordionDetailsUtilityClass } from './accordionDetailsClasses';
const useUtilityClasses = (ownerState) => {
const { classes } = ownerState;
const slots = {
root: ['root'],
};
return composeClasses(slots, getAccordionDetailsUtilityClass, classes);
};
const AccordionDetailsRoot = styled('div', {
name: 'MuiAccordionDetails',
slot: 'Root',
})(
memoTheme(({ theme }) => ({
padding: theme.spacing(1, 2, 2),
})),
);
const AccordionDetails = React.forwardRef(function AccordionDetails(inProps, ref) {
const props = useDefaultProps({ props: inProps, name: 'MuiAccordionDetails' });
const { className, ...other } = props;
const ownerState = props;
const classes = useUtilityClasses(ownerState);
return (
<AccordionDetailsRoot
className={clsx(classes.root, className)}
ref={ref}
ownerState={ownerState}
{...other}
/>
);
});
AccordionDetails.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 content of the component.
*/
children: PropTypes.node,
/**
* Override or extend the styles applied to the component.
*/
classes: PropTypes.object,
/**
* @ignore
*/
className: PropTypes.string,
/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])),
PropTypes.func,
PropTypes.object,
]),
};
export default AccordionDetails;

View File

@@ -0,0 +1,29 @@
import { expect } from 'chai';
import { createRenderer, screen } from '@mui/internal-test-utils';
import AccordionDetails, {
accordionDetailsClasses as classes,
} from '@mui/material/AccordionDetails';
import describeConformance from '../../test/describeConformance';
describe('<AccordionDetails />', () => {
const { render } = createRenderer();
describeConformance(<AccordionDetails>Conformance</AccordionDetails>, () => ({
classes,
inheritComponent: 'div',
render,
refInstanceof: window.HTMLDivElement,
muiName: 'MuiAccordionDetails',
skip: ['componentProp', 'componentsProp', 'themeVariants'],
}));
it('should render a children element', () => {
render(
<AccordionDetails>
<div data-testid="test-children" />
</AccordionDetails>,
);
expect(screen.queryByTestId('test-children')).not.to.equal(null);
});
});

View File

@@ -0,0 +1,20 @@
import generateUtilityClasses from '@mui/utils/generateUtilityClasses';
import generateUtilityClass from '@mui/utils/generateUtilityClass';
export interface AccordionDetailsClasses {
/** Styles applied to the root element. */
root: string;
}
export type AccordionDetailsClassKey = keyof AccordionDetailsClasses;
export function getAccordionDetailsUtilityClass(slot: string): string {
return generateUtilityClass('MuiAccordionDetails', slot);
}
const accordionDetailsClasses: AccordionDetailsClasses = generateUtilityClasses(
'MuiAccordionDetails',
['root'],
);
export default accordionDetailsClasses;

View File

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

View File

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

View File

@@ -0,0 +1,111 @@
import * as React from 'react';
import { SxProps } from '@mui/system';
import { ButtonBaseProps, ExtendButtonBase, ExtendButtonBaseTypeMap } from '../ButtonBase';
import { OverrideProps } from '../OverridableComponent';
import { CreateSlotsAndSlotProps, SlotProps } from '../utils/types';
import { Theme } from '../styles';
import { AccordionSummaryClasses } from './accordionSummaryClasses';
export interface AccordionSummarySlots {
/**
* The component that renders the root slot.
* @default ButtonBase
*/
root: React.ElementType;
/**
* The component that renders the content slot.
* @default div
*/
content: React.ElementType;
/**
* The component that renders the expand icon wrapper slot.
* @default div
*/
expandIconWrapper: React.ElementType;
}
export interface AccordionSummaryRootSlotPropsOverrides {}
export interface AccordionSummaryContentSlotPropsOverrides {}
export interface AccordionSummaryExpandIconWrapperSlotPropsOverrides {}
export type AccordionSummarySlotsAndSlotProps = CreateSlotsAndSlotProps<
AccordionSummarySlots,
{
/**
* Props forwarded to the root slot.
* By default, the available props are based on the [ButtonBase](https://mui.com/material-ui/api/button-base/#props) component.
*/
root: SlotProps<
React.ElementType<ButtonBaseProps>,
AccordionSummaryRootSlotPropsOverrides,
AccordionSummaryOwnerState
>;
/**
* Props forwarded to the content slot.
* By default, the available props are based on a div element.
*/
content: SlotProps<
'div',
AccordionSummaryContentSlotPropsOverrides,
AccordionSummaryOwnerState
>;
/**
* Props forwarded to the expand icon wrapper slot.
* By default, the available props are based on a div element.
*/
expandIconWrapper: SlotProps<
'div',
AccordionSummaryExpandIconWrapperSlotPropsOverrides,
AccordionSummaryOwnerState
>;
}
>;
export interface AccordionSummaryOwnProps extends AccordionSummarySlotsAndSlotProps {
children?: React.ReactNode;
/**
* Override or extend the styles applied to the component.
*/
classes?: Partial<AccordionSummaryClasses>;
/**
* The icon to display as the expand indicator.
*/
expandIcon?: React.ReactNode;
/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx?: SxProps<Theme>;
}
export type AccordionSummaryTypeMap<
AdditionalProps = {},
RootComponent extends React.ElementType = 'div',
> = ExtendButtonBaseTypeMap<{
props: AdditionalProps & AccordionSummaryOwnProps;
defaultComponent: RootComponent;
}>;
export interface AccordionSummaryOwnerState
extends Omit<AccordionSummaryProps, 'slots' | 'slotProps'> {}
/**
*
* Demos:
*
* - [Accordion](https://mui.com/material-ui/react-accordion/)
*
* API:
*
* - [AccordionSummary API](https://mui.com/material-ui/api/accordion-summary/)
* - inherits [ButtonBase API](https://mui.com/material-ui/api/button-base/)
*/
declare const AccordionSummary: ExtendButtonBase<AccordionSummaryTypeMap>;
export type AccordionSummaryProps<
RootComponent extends React.ElementType = AccordionSummaryTypeMap['defaultComponent'],
AdditionalProps = {},
> = OverrideProps<AccordionSummaryTypeMap<AdditionalProps, RootComponent>, RootComponent> & {
component?: React.ElementType;
};
export default AccordionSummary;

View File

@@ -0,0 +1,258 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import composeClasses from '@mui/utils/composeClasses';
import { styled } from '../zero-styled';
import memoTheme from '../utils/memoTheme';
import { useDefaultProps } from '../DefaultPropsProvider';
import ButtonBase from '../ButtonBase';
import AccordionContext from '../Accordion/AccordionContext';
import accordionSummaryClasses, {
getAccordionSummaryUtilityClass,
} from './accordionSummaryClasses';
import useSlot from '../utils/useSlot';
const useUtilityClasses = (ownerState) => {
const { classes, expanded, disabled, disableGutters } = ownerState;
const slots = {
root: ['root', expanded && 'expanded', disabled && 'disabled', !disableGutters && 'gutters'],
focusVisible: ['focusVisible'],
content: ['content', expanded && 'expanded', !disableGutters && 'contentGutters'],
expandIconWrapper: ['expandIconWrapper', expanded && 'expanded'],
};
return composeClasses(slots, getAccordionSummaryUtilityClass, classes);
};
const AccordionSummaryRoot = styled(ButtonBase, {
name: 'MuiAccordionSummary',
slot: 'Root',
})(
memoTheme(({ theme }) => {
const transition = {
duration: theme.transitions.duration.shortest,
};
return {
display: 'flex',
width: '100%',
minHeight: 48,
padding: theme.spacing(0, 2),
transition: theme.transitions.create(['min-height', 'background-color'], transition),
[`&.${accordionSummaryClasses.focusVisible}`]: {
backgroundColor: (theme.vars || theme).palette.action.focus,
},
[`&.${accordionSummaryClasses.disabled}`]: {
opacity: (theme.vars || theme).palette.action.disabledOpacity,
},
[`&:hover:not(.${accordionSummaryClasses.disabled})`]: {
cursor: 'pointer',
},
variants: [
{
props: (props) => !props.disableGutters,
style: {
[`&.${accordionSummaryClasses.expanded}`]: {
minHeight: 64,
},
},
},
],
};
}),
);
const AccordionSummaryContent = styled('span', {
name: 'MuiAccordionSummary',
slot: 'Content',
})(
memoTheme(({ theme }) => ({
display: 'flex',
textAlign: 'start',
flexGrow: 1,
margin: '12px 0',
variants: [
{
props: (props) => !props.disableGutters,
style: {
transition: theme.transitions.create(['margin'], {
duration: theme.transitions.duration.shortest,
}),
[`&.${accordionSummaryClasses.expanded}`]: {
margin: '20px 0',
},
},
},
],
})),
);
const AccordionSummaryExpandIconWrapper = styled('span', {
name: 'MuiAccordionSummary',
slot: 'ExpandIconWrapper',
})(
memoTheme(({ theme }) => ({
display: 'flex',
color: (theme.vars || theme).palette.action.active,
transform: 'rotate(0deg)',
transition: theme.transitions.create('transform', {
duration: theme.transitions.duration.shortest,
}),
[`&.${accordionSummaryClasses.expanded}`]: {
transform: 'rotate(180deg)',
},
})),
);
const AccordionSummary = React.forwardRef(function AccordionSummary(inProps, ref) {
const props = useDefaultProps({ props: inProps, name: 'MuiAccordionSummary' });
const {
children,
className,
expandIcon,
focusVisibleClassName,
onClick,
slots,
slotProps,
...other
} = props;
const { disabled = false, disableGutters, expanded, toggle } = React.useContext(AccordionContext);
const handleChange = (event) => {
if (toggle) {
toggle(event);
}
if (onClick) {
onClick(event);
}
};
const ownerState = {
...props,
expanded,
disabled,
disableGutters,
};
const classes = useUtilityClasses(ownerState);
const externalForwardedProps = {
slots,
slotProps,
};
const [RootSlot, rootSlotProps] = useSlot('root', {
ref,
shouldForwardComponentProp: true,
className: clsx(classes.root, className),
elementType: AccordionSummaryRoot,
externalForwardedProps: {
...externalForwardedProps,
...other,
},
ownerState,
additionalProps: {
focusRipple: false,
disableRipple: true,
disabled,
'aria-expanded': expanded,
focusVisibleClassName: clsx(classes.focusVisible, focusVisibleClassName),
},
getSlotProps: (handlers) => ({
...handlers,
onClick: (event) => {
handlers.onClick?.(event);
handleChange(event);
},
}),
});
const [ContentSlot, contentSlotProps] = useSlot('content', {
className: classes.content,
elementType: AccordionSummaryContent,
externalForwardedProps,
ownerState,
});
const [ExpandIconWrapperSlot, expandIconWrapperSlotProps] = useSlot('expandIconWrapper', {
className: classes.expandIconWrapper,
elementType: AccordionSummaryExpandIconWrapper,
externalForwardedProps,
ownerState,
});
return (
<RootSlot {...rootSlotProps}>
<ContentSlot {...contentSlotProps}>{children}</ContentSlot>
{expandIcon && (
<ExpandIconWrapperSlot {...expandIconWrapperSlotProps}>{expandIcon}</ExpandIconWrapperSlot>
)}
</RootSlot>
);
});
AccordionSummary.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 content of the component.
*/
children: PropTypes.node,
/**
* Override or extend the styles applied to the component.
*/
classes: PropTypes.object,
/**
* @ignore
*/
className: PropTypes.string,
/**
* The icon to display as the expand indicator.
*/
expandIcon: PropTypes.node,
/**
* This prop can help identify which element has keyboard focus.
* The class name will be applied when the element gains the focus through keyboard interaction.
* It's a polyfill for the [CSS :focus-visible selector](https://drafts.csswg.org/selectors-4/#the-focus-visible-pseudo).
* The rationale for using this feature [is explained here](https://github.com/WICG/focus-visible/blob/HEAD/explainer.md).
* A [polyfill can be used](https://github.com/WICG/focus-visible) to apply a `focus-visible` class to other components
* if needed.
*/
focusVisibleClassName: PropTypes.string,
/**
* @ignore
*/
onClick: PropTypes.func,
/**
* The props used for each slot inside.
* @default {}
*/
slotProps: PropTypes.shape({
content: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
expandIconWrapper: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
root: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
}),
/**
* The components used for each slot inside.
* @default {}
*/
slots: PropTypes.shape({
content: PropTypes.elementType,
expandIconWrapper: 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,
]),
};
export default AccordionSummary;

View File

@@ -0,0 +1,133 @@
import * as React from 'react';
import { expect } from 'chai';
import { spy } from 'sinon';
import { act, createRenderer, fireEvent, screen, isJsdom } from '@mui/internal-test-utils';
import AccordionSummary, {
accordionSummaryClasses as classes,
} from '@mui/material/AccordionSummary';
import Accordion from '@mui/material/Accordion';
import ButtonBase from '@mui/material/ButtonBase';
import describeConformance from '../../test/describeConformance';
const CustomButtonBase = React.forwardRef(({ focusVisible, ...props }, ref) => (
<ButtonBase ref={ref} {...props} />
));
describe('<AccordionSummary />', () => {
const { render } = createRenderer();
describeConformance(<AccordionSummary expandIcon="expand" />, () => ({
classes,
inheritComponent: ButtonBase,
render,
refInstanceof: window.HTMLButtonElement,
muiName: 'MuiAccordionSummary',
testVariantProps: { disabled: true },
testDeepOverrides: { slotName: 'content', slotClassName: classes.content },
skip: ['componentProp', 'componentsProp'],
slots: {
root: {
expectedClassName: classes.root,
testWithElement: CustomButtonBase,
},
content: {
expectedClassName: classes.content,
},
expandIconWrapper: {
expectedClassName: classes.expandIconWrapper,
},
},
}));
it('renders the children inside the .content element', () => {
const { container } = render(<AccordionSummary>The Summary</AccordionSummary>);
expect(container.querySelector(`.${classes.content}`)).to.have.text('The Summary');
});
it('when disabled should have disabled class', () => {
render(
<Accordion disabled>
<AccordionSummary />
</Accordion>,
);
expect(screen.getByRole('button')).to.have.class(classes.disabled);
});
it('renders the content given in expandIcon prop inside the div.expandIconWrapper', () => {
const { container } = render(<AccordionSummary expandIcon="iconElementContentExample" />);
const expandIconWrapper = container.querySelector(`.${classes.expandIconWrapper}`);
expect(expandIconWrapper).to.have.text('iconElementContentExample');
});
it('when expanded adds the expanded class to the button and .expandIconWrapper', () => {
const { container } = render(
<Accordion expanded>
<AccordionSummary expandIcon="expand" />
</Accordion>,
);
const button = screen.getByRole('button');
expect(button).to.have.class(classes.expanded);
expect(button).to.have.attribute('aria-expanded', 'true');
expect(container.querySelector(`.${classes.expandIconWrapper}`)).to.have.class(
classes.expanded,
);
});
it('should fire onBlur when the button blurs', () => {
const handleBlur = spy();
render(<AccordionSummary onBlur={handleBlur} />);
const button = screen.getByRole('button');
act(() => {
button.focus();
button.blur();
});
expect(handleBlur.callCount).to.equal(1);
});
it('should fire onClick callbacks', () => {
const handleClick = spy();
render(<AccordionSummary onClick={handleClick} />);
screen.getByRole('button').click();
expect(handleClick.callCount).to.equal(1);
});
it('fires onChange of the Accordion if clicked', () => {
const handleChange = spy();
render(
<Accordion onChange={handleChange} expanded={false}>
<AccordionSummary />
</Accordion>,
);
act(() => {
screen.getByRole('button').click();
});
expect(handleChange.callCount).to.equal(1);
});
// JSDOM doesn't support :focus-visible
it.skipIf(isJsdom())('calls onFocusVisible if focused visibly', function test() {
const handleFocusVisible = spy();
render(<AccordionSummary onFocusVisible={handleFocusVisible} />);
// simulate pointer device
fireEvent.mouseDown(document.body);
// this doesn't actually apply focus like in the browser. we need to move focus manually
fireEvent.keyDown(document.body, { key: 'Tab' });
act(() => {
screen.getByRole('button').focus();
});
expect(handleFocusVisible.callCount).to.equal(1);
});
});

View File

@@ -0,0 +1,46 @@
import generateUtilityClasses from '@mui/utils/generateUtilityClasses';
import generateUtilityClass from '@mui/utils/generateUtilityClass';
export interface AccordionSummaryClasses {
/** Styles applied to the root element. */
root: string;
/** State class applied to the root element, children wrapper element and `IconButton` component if `expanded={true}`. */
expanded: string;
/** State class applied to the ButtonBase root element if the button is keyboard focused. */
focusVisible: string;
/** State class applied to the root element if `disabled={true}`. */
disabled: string;
/** Styles applied to the root element unless `disableGutters={true}`. */
gutters: string;
/**
* Styles applied to the children wrapper element unless `disableGutters={true}`.
* @deprecated Combine the [.MuiAccordionSummary-gutters](/material-ui/api/accordion-summary/#accordion-summary-classes-MuiAccordionSummary-gutters) and [.MuiAccordionSummary-content](/material-ui/api/accordion-summary/#AccordionSummary-css-MuiAccordionSummary-content) classes instead. See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
contentGutters: string;
/** Styles applied to the children wrapper element. */
content: string;
/** Styles applied to the `expandIcon`'s wrapper element. */
expandIconWrapper: string;
}
export type AccordionSummaryClassKey = keyof AccordionSummaryClasses;
export function getAccordionSummaryUtilityClass(slot: string): string {
return generateUtilityClass('MuiAccordionSummary', slot);
}
const accordionSummaryClasses: AccordionSummaryClasses = generateUtilityClasses(
'MuiAccordionSummary',
[
'root',
'expanded',
'focusVisible',
'disabled',
'gutters',
'contentGutters',
'content',
'expandIconWrapper',
],
);
export default accordionSummaryClasses;

View File

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

View File

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

View File

@@ -0,0 +1,205 @@
import * as React from 'react';
import { OverridableStringUnion } from '@mui/types';
import { SxProps } from '@mui/system';
import { SvgIconProps } from '../SvgIcon';
import { Theme } from '../styles';
import { InternalStandardProps as StandardProps } from '../internal';
import { IconButtonProps } from '../IconButton';
import { PaperProps } from '../Paper';
import { AlertClasses } from './alertClasses';
import { CreateSlotsAndSlotProps, SlotProps } from '../utils/types';
export type AlertColor = 'success' | 'info' | 'warning' | 'error';
export interface AlertPropsVariantOverrides {}
export interface AlertPropsColorOverrides {}
export interface AlertRootSlotPropsOverrides {}
export interface AlertIconSlotPropsOverrides {}
export interface AlertMessageSlotPropsOverrides {}
export interface AlertActionSlotPropsOverrides {}
export interface AlertCloseButtonSlotPropsOverrides {}
export interface AlertCloseIconSlotPropsOverrides {}
export interface AlertSlots {
/**
* The component that renders the root slot.
* @default Paper
*/
root: React.ElementType;
/**
* The component that renders the icon slot.
* @default div
*/
icon: React.ElementType;
/**
* The component that renders the message slot.
* @default div
*/
message: React.ElementType;
/**
* The component that renders the action slot.
* @default div
*/
action: React.ElementType;
/**
* The component that renders the close button.
* @default IconButton
*/
closeButton: React.ElementType;
/**
* The component that renders the close icon.
* @default svg
*/
closeIcon: React.ElementType;
}
export type AlertSlotsAndSlotProps = CreateSlotsAndSlotProps<
AlertSlots,
{
/**
* Props forwarded to the root slot.
* By default, the available props are based on the [Paper](https://mui.com/material-ui/api/paper/#props) component.
*/
root: SlotProps<React.ElementType<PaperProps>, AlertRootSlotPropsOverrides, AlertOwnerState>;
/**
* Props forwarded to the icon slot.
* By default, the available props are based on a div element.
*/
icon: SlotProps<'div', AlertIconSlotPropsOverrides, AlertOwnerState>;
/**
* Props forwarded to the message slot.
* By default, the available props are based on a div element.
*/
message: SlotProps<'div', AlertMessageSlotPropsOverrides, AlertOwnerState>;
/**
* Props forwarded to the action slot.
* By default, the available props are based on a div element.
*/
action: SlotProps<'div', AlertActionSlotPropsOverrides, AlertOwnerState>;
/**
* Props forwarded to the closeButton slot.
* By default, the available props are based on the [IconButton](https://mui.com/material-ui/api/icon-button/#props) component.
*/
closeButton: SlotProps<
React.ElementType<IconButtonProps>,
AlertCloseButtonSlotPropsOverrides,
AlertOwnerState
>;
/**
* Props forwarded to the closeIcon slot.
* By default, the available props are based on the [SvgIcon](https://mui.com/material-ui/api/svg-icon/#props) component.
*/
closeIcon: SlotProps<
React.ElementType<SvgIconProps>,
AlertCloseIconSlotPropsOverrides,
AlertOwnerState
>;
}
>;
export interface AlertProps extends StandardProps<PaperProps, 'variant'>, AlertSlotsAndSlotProps {
/**
* The action to display. It renders after the message, at the end of the alert.
*/
action?: React.ReactNode;
/**
* Override or extend the styles applied to the component.
*/
classes?: Partial<AlertClasses>;
/**
* Override the default label for the *close popup* icon button.
*
* For localization purposes, you can use the provided [translations](https://mui.com/material-ui/guides/localization/).
* @default 'Close'
*/
closeText?: string;
/**
* The color of the component. Unless provided, the value is taken from the `severity` prop.
* 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).
*/
color?: OverridableStringUnion<AlertColor, AlertPropsColorOverrides>;
/**
* 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?: {
CloseButton?: React.ElementType;
CloseIcon?: React.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?: {
closeButton?: IconButtonProps;
closeIcon?: SvgIconProps;
};
/**
* The severity of the alert. This defines the color and icon used.
* @default 'success'
*/
severity?: OverridableStringUnion<AlertColor, AlertPropsColorOverrides>;
/**
* Override the icon displayed before the children.
* Unless provided, the icon is mapped to the value of the `severity` prop.
* Set to `false` to remove the `icon`.
*/
icon?: React.ReactNode;
/**
* The ARIA role attribute of the element.
* @default 'alert'
*/
role?: string;
/**
* The component maps the `severity` prop to a range of different icons,
* for instance success to `<SuccessOutlined>`.
* If you wish to change this mapping, you can provide your own.
* Alternatively, you can use the `icon` prop to override the icon displayed.
*/
iconMapping?: Partial<
Record<OverridableStringUnion<AlertColor, AlertPropsColorOverrides>, React.ReactNode>
>;
/**
* Callback fired when the component requests to be closed.
* When provided and no `action` prop is set, a close icon button is displayed that triggers the callback when clicked.
* @param {React.SyntheticEvent} event The event source of the callback.
*/
onClose?: (event: React.SyntheticEvent) => void;
/**
* The variant to use.
* @default 'standard'
*/
variant?: OverridableStringUnion<'standard' | 'filled' | 'outlined', AlertPropsVariantOverrides>;
/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx?: SxProps<Theme>;
}
export interface AlertOwnerState extends AlertProps {}
/**
*
* Demos:
*
* - [Alert](https://mui.com/material-ui/react-alert/)
*
* API:
*
* - [Alert API](https://mui.com/material-ui/api/alert/)
* - inherits [Paper API](https://mui.com/material-ui/api/paper/)
*/
export default function Alert(props: AlertProps): React.JSX.Element;

View File

@@ -0,0 +1,416 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import composeClasses from '@mui/utils/composeClasses';
import { styled } from '../zero-styled';
import memoTheme from '../utils/memoTheme';
import { useDefaultProps } from '../DefaultPropsProvider';
import useSlot from '../utils/useSlot';
import capitalize from '../utils/capitalize';
import createSimplePaletteValueFilter from '../utils/createSimplePaletteValueFilter';
import Paper from '../Paper';
import alertClasses, { getAlertUtilityClass } from './alertClasses';
import IconButton from '../IconButton';
import SuccessOutlinedIcon from '../internal/svg-icons/SuccessOutlined';
import ReportProblemOutlinedIcon from '../internal/svg-icons/ReportProblemOutlined';
import ErrorOutlineIcon from '../internal/svg-icons/ErrorOutline';
import InfoOutlinedIcon from '../internal/svg-icons/InfoOutlined';
import CloseIcon from '../internal/svg-icons/Close';
const useUtilityClasses = (ownerState) => {
const { variant, color, severity, classes } = ownerState;
const slots = {
root: [
'root',
`color${capitalize(color || severity)}`,
`${variant}${capitalize(color || severity)}`,
`${variant}`,
],
icon: ['icon'],
message: ['message'],
action: ['action'],
};
return composeClasses(slots, getAlertUtilityClass, classes);
};
const AlertRoot = styled(Paper, {
name: 'MuiAlert',
slot: 'Root',
overridesResolver: (props, styles) => {
const { ownerState } = props;
return [
styles.root,
styles[ownerState.variant],
styles[`${ownerState.variant}${capitalize(ownerState.color || ownerState.severity)}`],
];
},
})(
memoTheme(({ theme }) => {
const getColor = theme.palette.mode === 'light' ? theme.darken : theme.lighten;
const getBackgroundColor = theme.palette.mode === 'light' ? theme.lighten : theme.darken;
return {
...theme.typography.body2,
backgroundColor: 'transparent',
display: 'flex',
padding: '6px 16px',
variants: [
...Object.entries(theme.palette)
.filter(createSimplePaletteValueFilter(['light']))
.map(([color]) => ({
props: { colorSeverity: color, variant: 'standard' },
style: {
color: theme.vars
? theme.vars.palette.Alert[`${color}Color`]
: getColor(theme.palette[color].light, 0.6),
backgroundColor: theme.vars
? theme.vars.palette.Alert[`${color}StandardBg`]
: getBackgroundColor(theme.palette[color].light, 0.9),
[`& .${alertClasses.icon}`]: theme.vars
? { color: theme.vars.palette.Alert[`${color}IconColor`] }
: {
color: theme.palette[color].main,
},
},
})),
...Object.entries(theme.palette)
.filter(createSimplePaletteValueFilter(['light']))
.map(([color]) => ({
props: { colorSeverity: color, variant: 'outlined' },
style: {
color: theme.vars
? theme.vars.palette.Alert[`${color}Color`]
: getColor(theme.palette[color].light, 0.6),
border: `1px solid ${(theme.vars || theme).palette[color].light}`,
[`& .${alertClasses.icon}`]: theme.vars
? { color: theme.vars.palette.Alert[`${color}IconColor`] }
: {
color: theme.palette[color].main,
},
},
})),
...Object.entries(theme.palette)
.filter(createSimplePaletteValueFilter(['dark']))
.map(([color]) => ({
props: { colorSeverity: color, variant: 'filled' },
style: {
fontWeight: theme.typography.fontWeightMedium,
...(theme.vars
? {
color: theme.vars.palette.Alert[`${color}FilledColor`],
backgroundColor: theme.vars.palette.Alert[`${color}FilledBg`],
}
: {
backgroundColor:
theme.palette.mode === 'dark'
? theme.palette[color].dark
: theme.palette[color].main,
color: theme.palette.getContrastText(theme.palette[color].main),
}),
},
})),
],
};
}),
);
const AlertIcon = styled('div', {
name: 'MuiAlert',
slot: 'Icon',
})({
marginRight: 12,
padding: '7px 0',
display: 'flex',
fontSize: 22,
opacity: 0.9,
});
const AlertMessage = styled('div', {
name: 'MuiAlert',
slot: 'Message',
})({
padding: '8px 0',
minWidth: 0,
overflow: 'auto',
});
const AlertAction = styled('div', {
name: 'MuiAlert',
slot: 'Action',
})({
display: 'flex',
alignItems: 'flex-start',
padding: '4px 0 0 16px',
marginLeft: 'auto',
marginRight: -8,
});
const defaultIconMapping = {
success: <SuccessOutlinedIcon fontSize="inherit" />,
warning: <ReportProblemOutlinedIcon fontSize="inherit" />,
error: <ErrorOutlineIcon fontSize="inherit" />,
info: <InfoOutlinedIcon fontSize="inherit" />,
};
const Alert = React.forwardRef(function Alert(inProps, ref) {
const props = useDefaultProps({ props: inProps, name: 'MuiAlert' });
const {
action,
children,
className,
closeText = 'Close',
color,
components = {},
componentsProps = {},
icon,
iconMapping = defaultIconMapping,
onClose,
role = 'alert',
severity = 'success',
slotProps = {},
slots = {},
variant = 'standard',
...other
} = props;
const ownerState = {
...props,
color,
severity,
variant,
colorSeverity: color || severity,
};
const classes = useUtilityClasses(ownerState);
const externalForwardedProps = {
slots: {
closeButton: components.CloseButton,
closeIcon: components.CloseIcon,
...slots,
},
slotProps: {
...componentsProps,
...slotProps,
},
};
const [RootSlot, rootSlotProps] = useSlot('root', {
ref,
shouldForwardComponentProp: true,
className: clsx(classes.root, className),
elementType: AlertRoot,
externalForwardedProps: {
...externalForwardedProps,
...other,
},
ownerState,
additionalProps: {
role,
elevation: 0,
},
});
const [IconSlot, iconSlotProps] = useSlot('icon', {
className: classes.icon,
elementType: AlertIcon,
externalForwardedProps,
ownerState,
});
const [MessageSlot, messageSlotProps] = useSlot('message', {
className: classes.message,
elementType: AlertMessage,
externalForwardedProps,
ownerState,
});
const [ActionSlot, actionSlotProps] = useSlot('action', {
className: classes.action,
elementType: AlertAction,
externalForwardedProps,
ownerState,
});
const [CloseButtonSlot, closeButtonProps] = useSlot('closeButton', {
elementType: IconButton,
externalForwardedProps,
ownerState,
});
const [CloseIconSlot, closeIconProps] = useSlot('closeIcon', {
elementType: CloseIcon,
externalForwardedProps,
ownerState,
});
return (
<RootSlot {...rootSlotProps}>
{icon !== false ? (
<IconSlot {...iconSlotProps}>
{icon || iconMapping[severity] || defaultIconMapping[severity]}
</IconSlot>
) : null}
<MessageSlot {...messageSlotProps}>{children}</MessageSlot>
{action != null ? <ActionSlot {...actionSlotProps}>{action}</ActionSlot> : null}
{action == null && onClose ? (
<ActionSlot {...actionSlotProps}>
<CloseButtonSlot
size="small"
aria-label={closeText}
title={closeText}
color="inherit"
onClick={onClose}
{...closeButtonProps}
>
<CloseIconSlot fontSize="small" {...closeIconProps} />
</CloseButtonSlot>
</ActionSlot>
) : null}
</RootSlot>
);
});
Alert.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 action to display. It renders after the message, at the end of the alert.
*/
action: PropTypes.node,
/**
* The content of the component.
*/
children: PropTypes.node,
/**
* Override or extend the styles applied to the component.
*/
classes: PropTypes.object,
/**
* @ignore
*/
className: PropTypes.string,
/**
* Override the default label for the *close popup* icon button.
*
* For localization purposes, you can use the provided [translations](https://mui.com/material-ui/guides/localization/).
* @default 'Close'
*/
closeText: PropTypes.string,
/**
* The color of the component. Unless provided, the value is taken from the `severity` prop.
* 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).
*/
color: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([
PropTypes.oneOf(['error', 'info', 'success', 'warning']),
PropTypes.string,
]),
/**
* 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({
CloseButton: PropTypes.elementType,
CloseIcon: 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({
closeButton: PropTypes.object,
closeIcon: PropTypes.object,
}),
/**
* Override the icon displayed before the children.
* Unless provided, the icon is mapped to the value of the `severity` prop.
* Set to `false` to remove the `icon`.
*/
icon: PropTypes.node,
/**
* The component maps the `severity` prop to a range of different icons,
* for instance success to `<SuccessOutlined>`.
* If you wish to change this mapping, you can provide your own.
* Alternatively, you can use the `icon` prop to override the icon displayed.
*/
iconMapping: PropTypes.shape({
error: PropTypes.node,
info: PropTypes.node,
success: PropTypes.node,
warning: PropTypes.node,
}),
/**
* Callback fired when the component requests to be closed.
* When provided and no `action` prop is set, a close icon button is displayed that triggers the callback when clicked.
* @param {React.SyntheticEvent} event The event source of the callback.
*/
onClose: PropTypes.func,
/**
* The ARIA role attribute of the element.
* @default 'alert'
*/
role: PropTypes.string,
/**
* The severity of the alert. This defines the color and icon used.
* @default 'success'
*/
severity: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([
PropTypes.oneOf(['error', 'info', 'success', 'warning']),
PropTypes.string,
]),
/**
* The props used for each slot inside.
* @default {}
*/
slotProps: PropTypes.shape({
action: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
closeButton: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
closeIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
icon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
message: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
root: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
}),
/**
* The components used for each slot inside.
* @default {}
*/
slots: PropTypes.shape({
action: PropTypes.elementType,
closeButton: PropTypes.elementType,
closeIcon: PropTypes.elementType,
icon: PropTypes.elementType,
message: 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(['filled', 'outlined', 'standard']),
PropTypes.string,
]),
};
export default Alert;

View File

@@ -0,0 +1,38 @@
import CloseRounded from '@mui/icons-material/CloseRounded';
import { createTheme } from '@mui/material';
import Alert from '@mui/material/Alert';
createTheme({
components: {
MuiAlert: {
defaultProps: {
slots: {
closeIcon: CloseRounded,
},
},
},
},
});
<Alert
slotProps={{
root: {
className: 'px-4 py-3',
},
icon: {
className: 'mr-2',
},
message: {
className: 'flex-1',
},
action: {
className: 'ml-4',
},
closeButton: {
className: 'p-1',
},
closeIcon: {
className: 'w-5 h-5',
},
}}
/>;

View File

@@ -0,0 +1,229 @@
import { expect } from 'chai';
import { createRenderer, screen } from '@mui/internal-test-utils';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import Alert, { alertClasses as classes } from '@mui/material/Alert';
import Paper, { paperClasses } from '@mui/material/Paper';
import { iconButtonClasses } from '@mui/material/IconButton';
import { svgIconClasses } from '@mui/material/SvgIcon';
import describeConformance from '../../test/describeConformance';
import capitalize from '../utils/capitalize';
describe('<Alert />', () => {
const { render } = createRenderer();
describeConformance(<Alert onClose={() => {}} />, () => ({
classes,
inheritComponent: Paper,
render,
refInstanceof: window.HTMLDivElement,
muiName: 'MuiAlert',
testVariantProps: { variant: 'standard', color: 'success' },
testDeepOverrides: { slotName: 'message', slotClassName: classes.message },
testLegacyComponentsProp: ['closeButton', 'closeIcon'],
slots: {
root: {
expectedClassName: classes.root,
},
icon: {
expectedClassName: classes.icon,
},
message: {
expectedClassName: classes.message,
},
action: {
expectedClassName: classes.action,
},
closeButton: {
expectedClassName: classes.closeButton,
},
closeIcon: {
expectedClassName: classes.closeIcon,
},
},
skip: ['componentsProp'],
}));
describe('prop: square', () => {
it('should have rounded corners by default', () => {
render(<Alert data-testid="root">Hello World</Alert>);
expect(screen.getByTestId('root')).to.have.class(paperClasses.rounded);
});
it('should disable rounded corners with square prop', () => {
render(
<Alert data-testid="root" square>
Hello World
</Alert>,
);
expect(screen.getByTestId('root')).not.to.have.class(paperClasses.rounded);
});
});
describe('prop: action', () => {
it('using ownerState in styleOverrides should not throw', () => {
const theme = createTheme({
components: {
MuiAlert: {
styleOverrides: {
root: (props) => {
return {
...(props.ownerState.variant === 'filled' && {
border: '1px red solid',
}),
};
},
},
},
},
});
expect(() =>
render(
<ThemeProvider theme={theme}>
<Alert action={<button>Action</button>}>Alert</Alert>
</ThemeProvider>,
),
).not.to.throw();
});
it('should render the action provided into the Alert', () => {
render(<Alert action={<button data-testid="action">Action</button>}>Hello World</Alert>);
expect(screen.getByTestId('action')).toBeVisible();
});
});
describe('prop: components', () => {
it('should override the default icon used in the close action', () => {
function MyCloseIcon() {
return <div data-testid="closeIcon">X</div>;
}
render(
<Alert onClose={() => {}} components={{ CloseIcon: MyCloseIcon }}>
Hello World
</Alert>,
);
expect(screen.getByTestId('closeIcon')).toBeVisible();
});
it('should override the default button used in the close action', () => {
function MyCloseButton() {
return <button data-testid="closeButton">X</button>;
}
render(
<Alert onClose={() => {}} components={{ CloseButton: MyCloseButton }}>
Hello World
</Alert>,
);
expect(screen.getByTestId('closeButton')).toBeVisible();
});
});
describe('prop: componentsProps', () => {
it('should apply the props on the close IconButton component', () => {
render(
<Alert
onClose={() => {}}
componentsProps={{
closeButton: {
'data-testid': 'closeButton',
size: 'large',
className: 'my-class',
},
}}
>
Hello World
</Alert>,
);
const closeIcon = screen.getByTestId('closeButton');
expect(closeIcon).to.have.class(iconButtonClasses.sizeLarge);
expect(closeIcon).to.have.class('my-class');
});
it('should apply the props on the close SvgIcon component', () => {
render(
<Alert
onClose={() => {}}
componentsProps={{
closeIcon: {
'data-testid': 'closeIcon',
fontSize: 'large',
className: 'my-class',
},
}}
>
Hello World
</Alert>,
);
const closeIcon = screen.getByTestId('closeIcon');
expect(closeIcon).to.have.class(svgIconClasses.fontSizeLarge);
expect(closeIcon).to.have.class('my-class');
});
});
describe('prop: icon', () => {
it('should render the icon provided into the Alert', () => {
render(<Alert icon={<div data-testid="icon" />}>Hello World</Alert>);
expect(screen.getByTestId('icon')).toBeVisible();
});
it('should not render any icon if false is provided', () => {
render(
<Alert
icon={false}
severity="success"
iconMapping={{ success: <div data-testid="success-icon" /> }}
>
Hello World
</Alert>,
);
expect(screen.queryByTestId('success-icon')).to.eq(null);
});
});
describe('prop: iconMapping', () => {
const severities = ['success', 'info', 'warning', 'error'];
const iconMapping = severities.reduce((acc, severity) => {
acc[severity] = <div data-testid={`${severity}-icon`} />;
return acc;
}, {});
severities.forEach((severity) => {
it(`should render the icon provided into the Alert for severity ${severity}`, () => {
render(
<Alert severity={severity} iconMapping={iconMapping}>
Hello World
</Alert>,
);
expect(screen.getByTestId(`${severity}-icon`)).toBeVisible();
});
});
});
describe('classes', () => {
it('should apply default color class to the root', () => {
render(<Alert data-testid="alert" />);
expect(screen.getByTestId('alert')).to.have.class(classes.colorSuccess);
});
['success', 'info', 'warning', 'error'].forEach((color) => {
it('should apply color classes to the root', () => {
render(<Alert data-testid="alert" color={color} />);
expect(screen.getByTestId('alert')).to.have.class(classes[`color${capitalize(color)}`]);
});
});
});
});

View File

@@ -0,0 +1,133 @@
import generateUtilityClasses from '@mui/utils/generateUtilityClasses';
import generateUtilityClass from '@mui/utils/generateUtilityClass';
export interface AlertClasses {
/** Styles applied to the root element. */
root: string;
/** Styles applied to the root element if `variant="filled"`. */
filled: string;
/** Styles applied to the root element if `variant="outlined"`. */
outlined: string;
/** Styles applied to the root element if `variant="standard"`. */
standard: string;
/** Styles applied to the root element if `color="success"`. */
colorSuccess: string;
/** Styles applied to the root element if `color="info"`. */
colorInfo: string;
/** Styles applied to the root element if `color="warning"`. */
colorWarning: string;
/** Styles applied to the root element if `color="error"`. */
colorError: string;
/** Styles applied to the root element if `variant="standard"` and `color="success"`.
* @deprecated Combine the [.MuiAlert-standard](/material-ui/api/alert/#alert-classes-MuiAlert-standard)
* and [.MuiAlert-colorSuccess](/material-ui/api/alert/#alert-classes-MuiAlert-colorSuccess) classes instead.
* See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
standardSuccess: string;
/** Styles applied to the root element if `variant="standard"` and `color="info"`.
* @deprecated Combine the [.MuiAlert-standard](/material-ui/api/alert/#alert-classes-MuiAlert-standard)
* and [.MuiAlert-colorInfo](/material-ui/api/alert/#alert-classes-MuiAlert-colorInfo) classes instead.
* See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
standardInfo: string;
/** Styles applied to the root element if `variant="standard"` and `color="warning"`.
* @deprecated Combine the [.MuiAlert-standard](/material-ui/api/alert/#alert-classes-MuiAlert-standard)
* and [.MuiAlert-colorWarning](/material-ui/api/alert/#alert-classes-MuiAlert-colorWarning) classes instead.
* See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
standardWarning: string;
/** Styles applied to the root element if `variant="standard"` and `color="error"`.
* @deprecated Combine the [.MuiAlert-standard](/material-ui/api/alert/#alert-classes-MuiAlert-standard)
* and [.MuiAlert-colorError](/material-ui/api/alert/#alert-classes-MuiAlert-colorError) classes instead.
* See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
standardError: string;
/** Styles applied to the root element if `variant="outlined"` and `color="success"`.
* @deprecated Combine the [.MuiAlert-outlined](/material-ui/api/alert/#alert-classes-MuiAlert-outlined)
* and [.MuiAlert-colorSuccess](/material-ui/api/alert/#alert-classes-MuiAlert-colorSuccess) classes instead.
* See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
outlinedSuccess: string;
/** Styles applied to the root element if `variant="outlined"` and `color="info"`.
* @deprecated Combine the [.MuiAlert-outlined](/material-ui/api/alert/#alert-classes-MuiAlert-outlined)
* and [.MuiAlert-colorInfo](/material-ui/api/alert/#alert-classes-MuiAlert-colorInfo) classes instead.
* See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
outlinedInfo: string;
/** Styles applied to the root element if `variant="outlined"` and `color="warning"`.
* @deprecated Combine the [.MuiAlert-outlined](/material-ui/api/alert/#alert-classes-MuiAlert-outlined)
* and [.MuiAlert-colorWarning](/material-ui/api/alert/#alert-classes-MuiAlert-colorWarning) classes instead.
* See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
outlinedWarning: string;
/** Styles applied to the root element if `variant="outlined"` and `color="error"`.
* @deprecated Combine the [.MuiAlert-outlined](/material-ui/api/alert/#alert-classes-MuiAlert-outlined)
* and [.MuiAlert-colorError](/material-ui/api/alert/#alert-classes-MuiAlert-colorError) classes instead.
* See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
outlinedError: string;
/** Styles applied to the root element if `variant="filled"` and `color="success"`.
* @deprecated Combine the [.MuiAlert-filled](/material-ui/api/alert/#alert-classes-MuiAlert-filled)
* and [.MuiAlert-colorSuccess](/material-ui/api/alert/#alert-classes-MuiAlert-colorSuccess) classes instead.
* See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
filledSuccess: string;
/** Styles applied to the root element if `variant="filled"` and `color="info"`.
* @deprecated Combine the [.MuiAlert-filled](/material-ui/api/alert/#alert-classes-MuiAlert-filled)
* and [.MuiAlert-colorInfo](/material-ui/api/alert/#alert-classes-MuiAlert-colorInfo) classes instead.
* See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
filledInfo: string;
/** Styles applied to the root element if `variant="filled"` and `color="warning"`
* @deprecated Combine the [.MuiAlert-filled](/material-ui/api/alert/#alert-classes-MuiAlert-filled)
* and [.MuiAlert-colorWarning](/material-ui/api/alert/#alert-classes-MuiAlert-colorWarning) classes instead.
* See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
filledWarning: string;
/** Styles applied to the root element if `variant="filled"` and `color="error"`.
* @deprecated Combine the [.MuiAlert-filled](/material-ui/api/alert/#alert-classes-MuiAlert-filled)
* and [.MuiAlert-colorError](/material-ui/api/alert/#alert-classes-MuiAlert-colorError) classes instead.
* See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
filledError: string;
/** Styles applied to the icon wrapper element. */
icon: string;
/** Styles applied to the message wrapper element. */
message: string;
/** Styles applied to the action wrapper element if `action` is provided. */
action: string;
}
export type AlertClassKey = keyof AlertClasses;
export function getAlertUtilityClass(slot: string): string {
return generateUtilityClass('MuiAlert', slot);
}
const alertClasses: AlertClasses = generateUtilityClasses('MuiAlert', [
'root',
'action',
'icon',
'message',
'filled',
'colorSuccess',
'colorInfo',
'colorWarning',
'colorError',
'filledSuccess',
'filledInfo',
'filledWarning',
'filledError',
'outlined',
'outlinedSuccess',
'outlinedInfo',
'outlinedWarning',
'outlinedError',
'standard',
'standardSuccess',
'standardInfo',
'standardWarning',
'standardError',
]);
export default alertClasses;

View File

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

View File

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

View File

@@ -0,0 +1,33 @@
import * as React from 'react';
import { SxProps } from '@mui/system';
import { Theme } from '../styles';
import { TypographyProps } from '../Typography';
import { AlertTitleClasses } from './alertTitleClasses';
export interface AlertTitleProps extends TypographyProps<'div'> {
/**
* The content of the component.
*/
children?: React.ReactNode;
/**
* Override or extend the styles applied to the component.
*/
classes?: Partial<AlertTitleClasses>;
/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx?: SxProps<Theme>;
}
/**
*
* Demos:
*
* - [Alert](https://mui.com/material-ui/react-alert/)
*
* API:
*
* - [AlertTitle API](https://mui.com/material-ui/api/alert-title/)
* - inherits [Typography API](https://mui.com/material-ui/api/typography/)
*/
export default function AlertTitle(props: AlertTitleProps): React.JSX.Element;

View File

@@ -0,0 +1,84 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import composeClasses from '@mui/utils/composeClasses';
import { styled } from '../zero-styled';
import memoTheme from '../utils/memoTheme';
import { useDefaultProps } from '../DefaultPropsProvider';
import Typography from '../Typography';
import { getAlertTitleUtilityClass } from './alertTitleClasses';
const useUtilityClasses = (ownerState) => {
const { classes } = ownerState;
const slots = {
root: ['root'],
};
return composeClasses(slots, getAlertTitleUtilityClass, classes);
};
const AlertTitleRoot = styled(Typography, {
name: 'MuiAlertTitle',
slot: 'Root',
})(
memoTheme(({ theme }) => {
return {
fontWeight: theme.typography.fontWeightMedium,
marginTop: -2,
};
}),
);
const AlertTitle = React.forwardRef(function AlertTitle(inProps, ref) {
const props = useDefaultProps({
props: inProps,
name: 'MuiAlertTitle',
});
const { className, ...other } = props;
const ownerState = props;
const classes = useUtilityClasses(ownerState);
return (
<AlertTitleRoot
gutterBottom
component="div"
ownerState={ownerState}
ref={ref}
className={clsx(classes.root, className)}
{...other}
/>
);
});
AlertTitle.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 content of the component.
*/
children: PropTypes.node,
/**
* Override or extend the styles applied to the component.
*/
classes: PropTypes.object,
/**
* @ignore
*/
className: PropTypes.string,
/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])),
PropTypes.func,
PropTypes.object,
]),
};
export default AlertTitle;

View File

@@ -0,0 +1,3 @@
import AlertTitle from '@mui/material/AlertTitle';
<AlertTitle variant="h4" />;

View File

@@ -0,0 +1,18 @@
import { createRenderer } from '@mui/internal-test-utils';
import AlertTitle, { alertTitleClasses as classes } from '@mui/material/AlertTitle';
import Typography from '@mui/material/Typography';
import describeConformance from '../../test/describeConformance';
describe('<AlertTitle />', () => {
const { render } = createRenderer();
describeConformance(<AlertTitle />, () => ({
classes,
inheritComponent: Typography,
render,
muiName: 'MuiAlertTitle',
refInstanceof: window.HTMLDivElement,
testStateOverrides: { styleKey: 'root' },
skip: ['componentsProp', 'themeVariants', 'themeDefaultProps'],
}));
});

View File

@@ -0,0 +1,17 @@
import generateUtilityClasses from '@mui/utils/generateUtilityClasses';
import generateUtilityClass from '@mui/utils/generateUtilityClass';
export interface AlertTitleClasses {
/** Styles applied to the root element. */
root: string;
}
export type AlertTitleClassKey = keyof AlertTitleClasses;
export function getAlertTitleUtilityClass(slot: string): string {
return generateUtilityClass('MuiAlertTitle', slot);
}
const alertTitleClasses: AlertTitleClasses = generateUtilityClasses('MuiAlertTitle', ['root']);
export default alertTitleClasses;

View File

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

View File

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

View File

@@ -0,0 +1,87 @@
import * as React from 'react';
import { SxProps } from '@mui/system';
import { OverridableStringUnion } from '@mui/types';
import { OverridableComponent, OverrideProps } from '../OverridableComponent';
import { PropTypes, Theme } from '../styles';
import { AppBarClasses } from './appBarClasses';
import { ExtendPaperTypeMap } from '../Paper/Paper';
export interface AppBarPropsColorOverrides {}
export interface AppBarOwnProps {
/**
* Override or extend the styles applied to the component.
*/
classes?: Partial<AppBarClasses>;
/**
* 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 'primary'
*/
color?: OverridableStringUnion<
PropTypes.Color | 'transparent' | 'error' | 'info' | 'success' | 'warning',
AppBarPropsColorOverrides
>;
/**
* Shadow depth, corresponds to `dp` in the spec.
* It accepts values between 0 and 24 inclusive.
* @default 4
*/
elevation?: number;
/**
* If true, the `color` prop is applied in dark mode.
* @default false
*/
enableColorOnDark?: boolean;
/**
* The positioning type. The behavior of the different options is described
* [in the MDN web docs](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/position).
* Note: `sticky` is not universally supported and will fall back to `static` when unavailable.
* @default 'fixed'
*/
position?: 'fixed' | 'absolute' | 'sticky' | 'static' | 'relative';
/**
* If `false`, rounded corners are enabled.
* @default true
*/
square?: boolean;
/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx?: SxProps<Theme>;
}
export type AppBarTypeMap<
AdditionalProps = {},
RootComponent extends React.ElementType = 'header',
> = ExtendPaperTypeMap<
{
props: AdditionalProps & AppBarOwnProps;
defaultComponent: RootComponent;
},
'position' | 'color' | 'classes' | 'elevation' | 'square'
>;
/**
*
* Demos:
*
* - [App Bar](https://mui.com/material-ui/react-app-bar/)
*
* API:
*
* - [AppBar API](https://mui.com/material-ui/api/app-bar/)
* - inherits [Paper API](https://mui.com/material-ui/api/paper/)
*/
declare const AppBar: OverridableComponent<AppBarTypeMap>;
export type AppBarProps<
RootComponent extends React.ElementType = AppBarTypeMap['defaultComponent'],
AdditionalProps = {},
> = OverrideProps<AppBarTypeMap<AdditionalProps, RootComponent>, RootComponent> & {
component?: React.ElementType;
};
export default AppBar;

View File

@@ -0,0 +1,276 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import composeClasses from '@mui/utils/composeClasses';
import { styled } from '../zero-styled';
import memoTheme from '../utils/memoTheme';
import { useDefaultProps } from '../DefaultPropsProvider';
import capitalize from '../utils/capitalize';
import createSimplePaletteValueFilter from '../utils/createSimplePaletteValueFilter';
import Paper from '../Paper';
import { getAppBarUtilityClass } from './appBarClasses';
const useUtilityClasses = (ownerState) => {
const { color, position, classes } = ownerState;
const slots = {
root: ['root', `color${capitalize(color)}`, `position${capitalize(position)}`],
};
return composeClasses(slots, getAppBarUtilityClass, classes);
};
// var2 is the fallback.
// Ex. var1: 'var(--a)', var2: 'var(--b)'; return: 'var(--a, var(--b))'
const joinVars = (var1, var2) => (var1 ? `${var1?.replace(')', '')}, ${var2})` : var2);
const AppBarRoot = styled(Paper, {
name: 'MuiAppBar',
slot: 'Root',
overridesResolver: (props, styles) => {
const { ownerState } = props;
return [
styles.root,
styles[`position${capitalize(ownerState.position)}`],
styles[`color${capitalize(ownerState.color)}`],
];
},
})(
memoTheme(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
width: '100%',
boxSizing: 'border-box', // Prevent padding issue with the Modal and fixed positioned AppBar.
flexShrink: 0,
variants: [
{
props: { position: 'fixed' },
style: {
position: 'fixed',
zIndex: (theme.vars || theme).zIndex.appBar,
top: 0,
left: 'auto',
right: 0,
'@media print': {
// Prevent the app bar to be visible on each printed page.
position: 'absolute',
},
},
},
{
props: { position: 'absolute' },
style: {
position: 'absolute',
zIndex: (theme.vars || theme).zIndex.appBar,
top: 0,
left: 'auto',
right: 0,
},
},
{
props: { position: 'sticky' },
style: {
position: 'sticky',
zIndex: (theme.vars || theme).zIndex.appBar,
top: 0,
left: 'auto',
right: 0,
},
},
{
props: { position: 'static' },
style: {
position: 'static',
},
},
{
props: { position: 'relative' },
style: {
position: 'relative',
},
},
{
props: { color: 'inherit' },
style: {
'--AppBar-color': 'inherit',
},
},
{
props: { color: 'default' },
style: {
'--AppBar-background': theme.vars
? theme.vars.palette.AppBar.defaultBg
: theme.palette.grey[100],
'--AppBar-color': theme.vars
? theme.vars.palette.text.primary
: theme.palette.getContrastText(theme.palette.grey[100]),
...theme.applyStyles('dark', {
'--AppBar-background': theme.vars
? theme.vars.palette.AppBar.defaultBg
: theme.palette.grey[900],
'--AppBar-color': theme.vars
? theme.vars.palette.text.primary
: theme.palette.getContrastText(theme.palette.grey[900]),
}),
},
},
...Object.entries(theme.palette)
.filter(createSimplePaletteValueFilter(['contrastText']))
.map(([color]) => ({
props: { color },
style: {
'--AppBar-background': (theme.vars ?? theme).palette[color].main,
'--AppBar-color': (theme.vars ?? theme).palette[color].contrastText,
},
})),
{
props: (props) =>
props.enableColorOnDark === true && !['inherit', 'transparent'].includes(props.color),
style: {
backgroundColor: 'var(--AppBar-background)',
color: 'var(--AppBar-color)',
},
},
{
props: (props) =>
props.enableColorOnDark === false && !['inherit', 'transparent'].includes(props.color),
style: {
backgroundColor: 'var(--AppBar-background)',
color: 'var(--AppBar-color)',
...theme.applyStyles('dark', {
backgroundColor: theme.vars
? joinVars(theme.vars.palette.AppBar.darkBg, 'var(--AppBar-background)')
: null,
color: theme.vars
? joinVars(theme.vars.palette.AppBar.darkColor, 'var(--AppBar-color)')
: null,
}),
},
},
{
props: { color: 'transparent' },
style: {
'--AppBar-background': 'transparent',
'--AppBar-color': 'inherit',
backgroundColor: 'var(--AppBar-background)',
color: 'var(--AppBar-color)',
...theme.applyStyles('dark', {
backgroundImage: 'none',
}),
},
},
],
})),
);
const AppBar = React.forwardRef(function AppBar(inProps, ref) {
const props = useDefaultProps({ props: inProps, name: 'MuiAppBar' });
const {
className,
color = 'primary',
enableColorOnDark = false,
position = 'fixed',
...other
} = props;
const ownerState = {
...props,
color,
position,
enableColorOnDark,
};
const classes = useUtilityClasses(ownerState);
return (
<AppBarRoot
square
component="header"
ownerState={ownerState}
elevation={4}
className={clsx(
classes.root,
{
'mui-fixed': position === 'fixed', // Useful for the Dialog
},
className,
)}
ref={ref}
{...other}
/>
);
});
AppBar.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 content of the component.
*/
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 'primary'
*/
color: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([
PropTypes.oneOf([
'default',
'inherit',
'primary',
'secondary',
'transparent',
'error',
'info',
'success',
'warning',
]),
PropTypes.string,
]),
/**
* Shadow depth, corresponds to `dp` in the spec.
* It accepts values between 0 and 24 inclusive.
* @default 4
*/
elevation: PropTypes.number,
/**
* If true, the `color` prop is applied in dark mode.
* @default false
*/
enableColorOnDark: PropTypes.bool,
/**
* The positioning type. The behavior of the different options is described
* [in the MDN web docs](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/position).
* Note: `sticky` is not universally supported and will fall back to `static` when unavailable.
* @default 'fixed'
*/
position: PropTypes.oneOf(['absolute', 'fixed', 'relative', 'static', 'sticky']),
/**
* If `false`, rounded corners are enabled.
* @default true
*/
square: PropTypes.bool,
/**
* 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,
]),
};
export default AppBar;

View File

@@ -0,0 +1,39 @@
import * as React from 'react';
import { expectType } from '@mui/types';
import AppBar from '@mui/material/AppBar';
const CustomComponent: React.FC<{ stringProp: string; numberProp: number }> =
function CustomComponent() {
return <div />;
};
function AppBarTest() {
return (
<div>
<AppBar />
<AppBar elevation={4} />
<AppBar
component="a"
href="test"
onClick={(event) => {
expectType<React.MouseEvent<HTMLAnchorElement, MouseEvent>, typeof event>(event);
}}
/>
<AppBar component={CustomComponent} stringProp="test" numberProp={0} />
{/* @ts-expect-error missing stringProp and numberProp */}
<AppBar component={CustomComponent} />
</div>
);
}
// `color`
<AppBar color="inherit" />;
<AppBar color="primary" />;
<AppBar color="secondary" />;
<AppBar color="default" />;
<AppBar color="transparent" />;
<AppBar color="error" />;
<AppBar color="success" />;
<AppBar color="info" />;
<AppBar color="warning" />;

View File

@@ -0,0 +1,99 @@
import { expect } from 'chai';
import { createRenderer, screen, isJsdom } from '@mui/internal-test-utils';
import AppBar, { appBarClasses as classes } from '@mui/material/AppBar';
import Paper from '@mui/material/Paper';
import { ThemeProvider, CssVarsProvider, hexToRgb } from '@mui/material/styles';
import defaultTheme from '../styles/defaultTheme';
import describeConformance from '../../test/describeConformance';
describe('<AppBar />', () => {
const { render } = createRenderer();
describeConformance(<AppBar>Conformance?</AppBar>, () => ({
classes,
inheritComponent: Paper,
render,
muiName: 'MuiAppBar',
refInstanceof: window.HTMLElement,
testVariantProps: { position: 'relative' },
testStateOverrides: { prop: 'color', value: 'secondary', styleKey: 'colorSecondary' },
skip: ['componentsProp'],
}));
it('should render with the root class and primary', () => {
const { container } = render(<AppBar>Hello World</AppBar>);
const appBar = container.firstChild;
expect(appBar).to.have.class(classes.root);
expect(appBar).to.have.class(classes.colorPrimary);
expect(appBar).not.to.have.class(classes.colorSecondary);
});
it('should render a primary app bar', () => {
const { container } = render(<AppBar color="primary">Hello World</AppBar>);
const appBar = container.firstChild;
expect(appBar).to.have.class(classes.root);
expect(appBar).to.have.class(classes.colorPrimary);
expect(appBar).not.to.have.class(classes.colorSecondary);
});
it('should render an secondary app bar', () => {
const { container } = render(<AppBar color="secondary">Hello World</AppBar>);
const appBar = container.firstChild;
expect(appBar).to.have.class(classes.root);
expect(appBar).not.to.have.class(classes.colorPrimary);
expect(appBar).to.have.class(classes.colorSecondary);
});
it('should change elevation', () => {
render(
<AppBar data-testid="root" elevation={5} classes={{ elevation5: 'app-bar-elevation-5' }}>
Hello World
</AppBar>,
);
const appBar = screen.getByTestId('root');
expect(appBar).not.to.have.class(classes.elevation5);
expect(appBar).not.to.have.class('app-bar-elevation-5');
});
describe('Dialog', () => {
it('should add a .mui-fixed class', () => {
const { container } = render(<AppBar position="fixed">Hello World</AppBar>);
const appBar = container.firstChild;
expect(appBar).to.have.class('mui-fixed');
});
});
it.skipIf(isJsdom())('should inherit Paper background color with ThemeProvider', function test() {
render(
<ThemeProvider theme={defaultTheme}>
<AppBar data-testid="root" color="inherit">
Hello World
</AppBar>
</ThemeProvider>,
);
const appBar = screen.getByTestId('root');
expect(appBar).toHaveComputedStyle({
backgroundColor: hexToRgb(defaultTheme.palette.background.paper),
});
});
it.skipIf(isJsdom())(
'should inherit Paper background color with CssVarsProvider',
function test() {
render(
<CssVarsProvider>
<AppBar data-testid="root" color="inherit">
Hello World
</AppBar>
</CssVarsProvider>,
);
const appBar = screen.getByTestId('root');
expect(appBar).toHaveComputedStyle({
backgroundColor: hexToRgb(defaultTheme.palette.background.paper),
});
},
);
});

View File

@@ -0,0 +1,61 @@
import generateUtilityClasses from '@mui/utils/generateUtilityClasses';
import generateUtilityClass from '@mui/utils/generateUtilityClass';
export interface AppBarClasses {
/** Styles applied to the root element. */
root: string;
/** Styles applied to the root element if `position="fixed"`. */
positionFixed: string;
/** Styles applied to the root element if `position="absolute"`. */
positionAbsolute: string;
/** Styles applied to the root element if `position="sticky"`. */
positionSticky: string;
/** Styles applied to the root element if `position="static"`. */
positionStatic: string;
/** Styles applied to the root element if `position="relative"`. */
positionRelative: string;
/** Styles applied to the root element if `color="default"`. */
colorDefault: string;
/** Styles applied to the root element if `color="primary"`. */
colorPrimary: string;
/** Styles applied to the root element if `color="secondary"`. */
colorSecondary: string;
/** Styles applied to the root element if `color="inherit"`. */
colorInherit: string;
/** Styles applied to the root element if `color="transparent"`. */
colorTransparent: string;
/** Styles applied to the root element if `color="error"`. */
colorError: 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;
}
export type AppBarClassKey = keyof AppBarClasses;
export function getAppBarUtilityClass(slot: string): string {
return generateUtilityClass('MuiAppBar', slot);
}
const appBarClasses: AppBarClasses = generateUtilityClasses('MuiAppBar', [
'root',
'positionFixed',
'positionAbsolute',
'positionSticky',
'positionStatic',
'positionRelative',
'colorDefault',
'colorPrimary',
'colorSecondary',
'colorInherit',
'colorTransparent',
'colorError',
'colorInfo',
'colorSuccess',
'colorWarning',
]);
export default appBarClasses;

View File

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

View File

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

View File

@@ -0,0 +1,423 @@
import * as React from 'react';
import { SxProps } from '@mui/system';
import { OverridableStringUnion } from '@mui/types';
import { Theme } from '../styles';
import { InternalStandardProps as StandardProps } from '../internal';
import { IconButtonProps } from '../IconButton';
import { ChipProps, ChipTypeMap } from '../Chip';
import { PaperProps } from '../Paper';
import { PopperProps } from '../Popper';
import useAutocomplete, {
AutocompleteChangeDetails,
AutocompleteChangeReason,
AutocompleteCloseReason,
AutocompleteInputChangeReason,
AutocompleteValue,
createFilterOptions,
UseAutocompleteProps,
AutocompleteFreeSoloValueMapping,
} from '../useAutocomplete';
import { AutocompleteClasses } from './autocompleteClasses';
import { CreateSlotsAndSlotProps, SlotProps } from '../utils/types';
export interface AutocompletePaperSlotPropsOverrides {}
export interface AutocompletePopperSlotPropsOverrides {}
export {
AutocompleteChangeDetails,
AutocompleteChangeReason,
AutocompleteCloseReason,
AutocompleteInputChangeReason,
AutocompleteValue,
createFilterOptions,
};
export type AutocompleteOwnerState<
Value,
Multiple extends boolean | undefined,
DisableClearable extends boolean | undefined,
FreeSolo extends boolean | undefined,
ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent'],
> = AutocompleteProps<Value, Multiple, DisableClearable, FreeSolo, ChipComponent> & {
disablePortal: boolean;
expanded: boolean;
focused: boolean;
fullWidth: boolean;
getOptionLabel: (option: Value | AutocompleteFreeSoloValueMapping<FreeSolo>) => string;
hasClearIcon: boolean;
hasPopupIcon: boolean;
inputFocused: boolean;
popupOpen: boolean;
size: OverridableStringUnion<'small' | 'medium', AutocompletePropsSizeOverrides>;
};
export type AutocompleteRenderGetTagProps = ({ index }: { index: number }) => {
key: number;
className: string;
disabled: boolean;
'data-tag-index': number;
tabIndex: -1;
onDelete: (event: any) => void;
};
export type AutocompleteRenderValueGetItemProps<Multiple extends boolean | undefined> =
Multiple extends true
? (args: { index: number }) => {
key: number;
className: string;
disabled: boolean;
'data-item-index': number;
tabIndex: -1;
onDelete: (event: any) => void;
}
: (args?: { index?: number }) => {
className: string;
disabled: boolean;
'data-item-index': number;
tabIndex: -1;
onDelete: (event: any) => void;
};
export type AutocompleteRenderValue<Value, Multiple, FreeSolo> = Multiple extends true
? Array<Value | AutocompleteFreeSoloValueMapping<FreeSolo>>
: NonNullable<Value | AutocompleteFreeSoloValueMapping<FreeSolo>>;
export interface AutocompleteRenderOptionState {
inputValue: string;
index: number;
selected: boolean;
}
export interface AutocompleteRenderGroupParams {
key: number;
group: string;
children?: React.ReactNode;
}
export interface AutocompleteRenderInputParams {
id: string;
disabled: boolean;
fullWidth: boolean;
size: 'small' | undefined;
InputLabelProps: ReturnType<ReturnType<typeof useAutocomplete>['getInputLabelProps']>;
InputProps: {
ref: React.Ref<any>;
className: string;
startAdornment: React.ReactNode;
endAdornment: React.ReactNode;
onMouseDown: React.MouseEventHandler;
};
inputProps: ReturnType<ReturnType<typeof useAutocomplete>['getInputProps']>;
}
export interface AutocompletePropsSizeOverrides {}
export interface AutocompleteSlots {
/**
* The component used to render the listbox.
* @default 'ul'
*/
listbox: React.JSXElementConstructor<React.HTMLAttributes<HTMLElement>>;
/**
* The component used to render the body of the popup.
* @default Paper
*/
paper: React.JSXElementConstructor<PaperProps & AutocompletePaperSlotPropsOverrides>;
/**
* The component used to position the popup.
* @default Popper
*/
popper: React.JSXElementConstructor<PopperProps & AutocompletePopperSlotPropsOverrides>;
}
export type AutocompleteSlotsAndSlotProps<
Value,
Multiple extends boolean | undefined,
DisableClearable extends boolean | undefined,
FreeSolo extends boolean | undefined,
ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent'],
> = CreateSlotsAndSlotProps<
AutocompleteSlots,
{
chip: SlotProps<
React.ElementType<Partial<ChipProps<ChipComponent>>>,
{},
AutocompleteOwnerState<Value, Multiple, DisableClearable, FreeSolo, ChipComponent>
>;
clearIndicator: SlotProps<
React.ElementType<Partial<IconButtonProps>>,
{},
AutocompleteOwnerState<Value, Multiple, DisableClearable, FreeSolo, ChipComponent>
>;
/**
* Props applied to the Listbox element.
*/
listbox: SlotProps<
React.ElementType<
ReturnType<ReturnType<typeof useAutocomplete>['getListboxProps']> & {
sx?: SxProps<Theme>;
ref?: React.Ref<Element>;
}
>,
{},
AutocompleteOwnerState<Value, Multiple, DisableClearable, FreeSolo, ChipComponent>
>;
paper: SlotProps<
React.ElementType<Partial<PaperProps>>,
AutocompletePaperSlotPropsOverrides,
AutocompleteOwnerState<Value, Multiple, DisableClearable, FreeSolo, ChipComponent>
>;
popper: SlotProps<
React.ElementType<Partial<PopperProps>>,
AutocompletePopperSlotPropsOverrides,
AutocompleteOwnerState<Value, Multiple, DisableClearable, FreeSolo, ChipComponent>
>;
popupIndicator: SlotProps<
React.ElementType<Partial<IconButtonProps>>,
{},
AutocompleteOwnerState<Value, Multiple, DisableClearable, FreeSolo, ChipComponent>
>;
}
>;
export interface AutocompleteProps<
Value,
Multiple extends boolean | undefined,
DisableClearable extends boolean | undefined,
FreeSolo extends boolean | undefined,
ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent'],
> extends UseAutocompleteProps<Value, Multiple, DisableClearable, FreeSolo>,
StandardProps<React.HTMLAttributes<HTMLDivElement>, 'defaultValue' | 'onChange' | 'children'>,
AutocompleteSlotsAndSlotProps<Value, Multiple, DisableClearable, FreeSolo, ChipComponent> {
/**
* Props applied to the [`Chip`](https://mui.com/material-ui/api/chip/) element.
* @deprecated Use `slotProps.chip` instead. This prop will be removed in a future major release. See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
ChipProps?: ChipProps<ChipComponent>;
/**
* Override or extend the styles applied to the component.
*/
classes?: Partial<AutocompleteClasses>;
/**
* The icon to display in place of the default clear icon.
* @default <ClearIcon fontSize="small" />
*/
clearIcon?: React.ReactNode;
/**
* Override the default text for the *clear* icon button.
*
* For localization purposes, you can use the provided [translations](https://mui.com/material-ui/guides/localization/).
* @default 'Clear'
*/
clearText?: string;
/**
* Override the default text for the *close popup* icon button.
*
* For localization purposes, you can use the provided [translations](https://mui.com/material-ui/guides/localization/).
* @default 'Close'
*/
closeText?: string;
/**
* The props used for each slot inside.
* @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.
*/
componentsProps?: {
clearIndicator?: Partial<IconButtonProps>;
paper?: PaperProps;
popper?: Partial<PopperProps>;
popupIndicator?: Partial<IconButtonProps>;
};
/**
* If `true`, the component is disabled.
* @default false
*/
disabled?: boolean;
/**
* If `true`, the `Popper` content will be under the DOM hierarchy of the parent component.
* @default false
*/
disablePortal?: boolean;
/**
* Force the visibility display of the popup icon.
* @default 'auto'
*/
forcePopupIcon?: true | false | 'auto';
/**
* If `true`, the input will take up the full width of its container.
* @default false
*/
fullWidth?: boolean;
/**
* The label to display when the tags are truncated (`limitTags`).
*
* @param {number} more The number of truncated tags.
* @returns {ReactNode}
* @default (more) => `+${more}`
*/
getLimitTagsText?: (more: number) => React.ReactNode;
/**
* The component used to render the listbox.
* @default 'ul'
* @deprecated Use `slotProps.listbox.component` instead. This prop will be removed in a future major release. See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
ListboxComponent?: React.JSXElementConstructor<React.HTMLAttributes<HTMLElement>>;
/**
* Props applied to the Listbox element.
* @deprecated Use `slotProps.listbox` instead. This prop will be removed in a future major release. See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
ListboxProps?: ReturnType<ReturnType<typeof useAutocomplete>['getListboxProps']> & {
sx?: SxProps<Theme>;
ref?: React.Ref<Element>;
};
/**
* If `true`, the component is in a loading state.
* This shows the `loadingText` in place of suggestions (only if there are no suggestions to show, for example `options` are empty).
* @default false
*/
loading?: boolean;
/**
* Text to display when in a loading state.
*
* For localization purposes, you can use the provided [translations](https://mui.com/material-ui/guides/localization/).
* @default 'Loading…'
*/
loadingText?: React.ReactNode;
/**
* The maximum number of tags that will be visible when not focused.
* Set `-1` to disable the limit.
* @default -1
*/
limitTags?: number;
/**
* Text to display when there are no options.
*
* For localization purposes, you can use the provided [translations](https://mui.com/material-ui/guides/localization/).
* @default 'No options'
*/
noOptionsText?: React.ReactNode;
onKeyDown?: (
event: React.KeyboardEvent<HTMLDivElement> & { defaultMuiPrevented?: boolean },
) => void;
/**
* Override the default text for the *open popup* icon button.
*
* For localization purposes, you can use the provided [translations](https://mui.com/material-ui/guides/localization/).
* @default 'Open'
*/
openText?: string;
/**
* The component used to render the body of the popup.
* @default Paper
* @deprecated Use `slots.paper` instead. This prop will be removed in a future major release. See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
PaperComponent?: React.JSXElementConstructor<React.HTMLAttributes<HTMLElement>>;
/**
* The component used to position the popup.
* @default Popper
* @deprecated Use `slots.popper` instead. This prop will be removed in a future major release. See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
PopperComponent?: React.JSXElementConstructor<PopperProps>;
/**
* The icon to display in place of the default popup icon.
* @default <ArrowDropDownIcon />
*/
popupIcon?: React.ReactNode;
/**
* If `true`, the component becomes readonly. It is also supported for multiple tags where the tag cannot be deleted.
* @default false
*/
readOnly?: boolean;
/**
* Render the group.
*
* @param {AutocompleteRenderGroupParams} params The group to render.
* @returns {ReactNode}
*/
renderGroup?: (params: AutocompleteRenderGroupParams) => React.ReactNode;
/**
* Render the input.
*
* **Note:** The `renderInput` prop must return a `TextField` component or a compatible custom component
* that correctly forwards `InputProps.ref` and spreads `inputProps`. This ensures proper integration
* with the Autocomplete's internal logic (e.g., focus management and keyboard navigation).
*
* Avoid using components like `DatePicker` or `Select` directly, as they may not forward the required props,
* leading to runtime errors or unexpected behavior.
*
* @param {object} params
* @returns {ReactNode}
*/
renderInput: (params: AutocompleteRenderInputParams) => React.ReactNode;
/**
* Render the option, use `getOptionLabel` by default.
*
* @param {object} props The props to apply on the li element.
* @param {Value} option The option to render.
* @param {object} state The state of each option.
* @param {object} ownerState The state of the Autocomplete component.
* @returns {ReactNode}
*/
renderOption?: (
props: React.HTMLAttributes<HTMLLIElement> & { key: any },
option: Value,
state: AutocompleteRenderOptionState,
ownerState: AutocompleteOwnerState<Value, Multiple, DisableClearable, FreeSolo, ChipComponent>,
) => React.ReactNode;
/**
* Render the selected value when doing multiple selections.
*
* @deprecated Use `renderValue` prop instead
*
* @param {Value[]} value The `value` provided to the component.
* @param {function} getTagProps A tag props getter.
* @param {object} ownerState The state of the Autocomplete component.
* @returns {ReactNode}
*/
renderTags?: (
value: Value[],
getTagProps: AutocompleteRenderGetTagProps,
ownerState: AutocompleteOwnerState<Value, Multiple, DisableClearable, FreeSolo, ChipComponent>,
) => React.ReactNode;
/**
* Renders the selected value(s) as rich content in the input for both single and multiple selections.
*
* @param {AutocompleteRenderValue<Value, Multiple, FreeSolo>} value The `value` provided to the component.
* @param {function} getItemProps The value item props.
* @param {object} ownerState The state of the Autocomplete component.
* @returns {ReactNode}
*/
renderValue?: (
value: AutocompleteRenderValue<Value, Multiple, FreeSolo>,
getItemProps: AutocompleteRenderValueGetItemProps<Multiple>,
ownerState: AutocompleteOwnerState<Value, Multiple, DisableClearable, FreeSolo, ChipComponent>,
) => React.ReactNode;
/**
* The size of the component.
* @default 'medium'
*/
size?: OverridableStringUnion<'small' | 'medium', AutocompletePropsSizeOverrides>;
/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx?: SxProps<Theme>;
}
/**
*
* Demos:
*
* - [Autocomplete](https://mui.com/material-ui/react-autocomplete/)
*
* API:
*
* - [Autocomplete API](https://mui.com/material-ui/api/autocomplete/)
*/
export default function Autocomplete<
Value,
Multiple extends boolean | undefined = false,
DisableClearable extends boolean | undefined = false,
FreeSolo extends boolean | undefined = false,
ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent'],
>(
props: AutocompleteProps<Value, Multiple, DisableClearable, FreeSolo, ChipComponent>,
): React.JSX.Element;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,184 @@
import * as React from 'react';
import { expectType } from '@mui/types';
import Autocomplete, {
AutocompleteOwnerState,
AutocompleteProps,
AutocompleteRenderGetTagProps,
} from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
import { ChipTypeMap } from '@mui/material/Chip';
interface MyAutocompleteProps<
T,
Multiple extends boolean | undefined,
DisableClearable extends boolean | undefined,
FreeSolo extends boolean | undefined,
ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent'],
> extends AutocompleteProps<T, Multiple, DisableClearable, FreeSolo, ChipComponent> {
myProp?: string;
}
function MyAutocomplete<
T,
Multiple extends boolean | undefined = false,
DisableClearable extends boolean | undefined = false,
FreeSolo extends boolean | undefined = false,
ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent'],
>(props: MyAutocompleteProps<T, Multiple, DisableClearable, FreeSolo, ChipComponent>) {
return <Autocomplete {...props} />;
}
// Test for ChipComponent generic type
<MyAutocomplete<string, false, false, false, 'span'>
options={['1', '2', '3']}
renderTags={(value, getTagProps, ownerState) => {
expectType<AutocompleteOwnerState<string, false, false, false, 'span'>, typeof ownerState>(
ownerState,
);
return '';
}}
renderInput={() => null}
/>;
// multiple prop can be assigned for components that extend AutocompleteProps
<MyAutocomplete
options={['1', '2', '3']}
onChange={(event, value) => {
expectType<string[], typeof value>(value);
}}
renderInput={() => null}
multiple
/>;
<MyAutocomplete
options={['1', '2', '3']}
onChange={(event, value) => {
expectType<string | null, typeof value>(value);
}}
renderInput={() => null}
/>;
// Tests presence of sx prop in ListboxProps
<Autocomplete
options={['1', '2', '3']}
ListboxProps={{ sx: { height: '10px' } }}
renderInput={() => null}
/>;
// Tests presence of onMouseDown prop in InputProps
<Autocomplete
options={['1', '2', '3']}
renderInput={(params) => {
expectType<React.MouseEventHandler, typeof params.InputProps.onMouseDown>(
params.InputProps.onMouseDown,
);
return <TextField {...params} />;
}}
/>;
<MyAutocomplete
options={['1', '2', '3']}
onChange={(event, value) => {
expectType<string, typeof value>(value);
}}
renderInput={() => null}
disableClearable
/>;
<MyAutocomplete
options={[{ label: '1' }, { label: '2' }]}
onChange={(event, value) => {
expectType<string | { label: string } | null, typeof value>(value);
}}
renderInput={() => null}
freeSolo
/>;
// Test for getInputProps return type
<MyAutocomplete
options={[{ label: '1' }, { label: '2' }]}
renderInput={(params) => <TextField {...params} value={params.inputProps.value} />}
/>;
// Test for focusVisible class
<Autocomplete
classes={{ focusVisible: 'test' }}
options={[{ label: '1' }, { label: '2' }]}
renderInput={(params) => <TextField {...params} />}
/>;
interface Option {
label: string;
value: string;
}
const options: Option[] = [
{ label: '1', value: '1' },
{ label: '2', value: '2' },
];
const defaultOptions = [options[0], options[1]];
<MyAutocomplete
multiple
options={options}
defaultValue={defaultOptions}
isOptionEqualToValue={(o, v) => o.label === v.label}
getOptionLabel={(o) => o.label}
renderInput={() => null}
/>;
interface Tag {
color: string;
label: string;
}
type TagComponentProps = Tag & React.HTMLAttributes<HTMLDivElement>;
function TagComponent({ color, label, ...other }: TagComponentProps) {
return <div {...other}>{label}</div>;
}
function renderTags(value: Tag[], getTagProps: AutocompleteRenderGetTagProps) {
return value.map((tag: Tag, index) => {
const { key, onDelete, ...tagProps } = getTagProps({ index });
return <TagComponent key={key} {...tagProps} {...tag} />;
});
}
function AutocompleteComponentsProps() {
return (
<Autocomplete
options={['one', 'two', 'three']}
renderInput={(params) => <TextField {...params} />}
componentsProps={{
clearIndicator: { size: 'large' },
paper: { elevation: 2 },
popper: { placement: 'bottom-end' },
popupIndicator: { size: 'large' },
}}
/>
);
}
function CustomListboxRef() {
const ref = React.useRef(null);
return (
<Autocomplete
renderInput={(params) => <TextField {...params} />}
options={['one', 'two', 'three']}
ListboxProps={{ ref }}
/>
);
}
// Tests presence of defaultMuiPrevented in event
<Autocomplete
renderInput={(params) => <TextField {...params} />}
options={['one', 'two', 'three']}
onKeyDown={(event) => {
expectType<
React.KeyboardEvent<HTMLDivElement> & {
defaultMuiPrevented?: boolean;
},
typeof event
>(event);
}}
/>;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,94 @@
import generateUtilityClasses from '@mui/utils/generateUtilityClasses';
import generateUtilityClass from '@mui/utils/generateUtilityClass';
export interface AutocompleteClasses {
/** Styles applied to the root element. */
root: string;
/** Styles applied to the root element if `fullWidth={true}`. */
fullWidth: string;
/** State class applied to the root element if the listbox is displayed. */
expanded: string;
/** State class applied to the root element if focused. */
focused: string;
/** Styles applied to the option elements if they are keyboard focused. */
focusVisible: string;
/** Styles applied to the tag elements, for example the chips. */
tag: string;
/** Styles applied to the tag elements, for example the chips if `size="small"`. */
tagSizeSmall: string;
/** Styles applied to the tag elements, for example the chips if `size="medium"`. */
tagSizeMedium: string;
/** Styles applied when the popup icon is rendered. */
hasPopupIcon: string;
/** Styles applied when the clear icon is rendered. */
hasClearIcon: string;
/** Styles applied to the Input element. */
inputRoot: string;
/** Styles applied to the input element. */
input: string;
/** Styles applied to the input element if the input is focused. */
inputFocused: string;
/** Styles applied to the endAdornment element. */
endAdornment: string;
/** Styles applied to the clear indicator. */
clearIndicator: string;
/** Styles applied to the popup indicator. */
popupIndicator: string;
/** Styles applied to the popup indicator if the popup is open. */
popupIndicatorOpen: string;
/** Styles applied to the popper element. */
popper: string;
/** Styles applied to the popper element if `disablePortal={true}`. */
popperDisablePortal: string;
/** Styles applied to the Paper component. */
paper: string;
/** Styles applied to the listbox component. */
listbox: string;
/** Styles applied to the loading wrapper. */
loading: string;
/** Styles applied to the no option wrapper. */
noOptions: string;
/** Styles applied to the option elements. */
option: string;
/** Styles applied to the group's label elements. */
groupLabel: string;
/** Styles applied to the group's ul elements. */
groupUl: string;
}
export type AutocompleteClassKey = keyof AutocompleteClasses;
export function getAutocompleteUtilityClass(slot: string): string {
return generateUtilityClass('MuiAutocomplete', slot);
}
const autocompleteClasses: AutocompleteClasses = generateUtilityClasses('MuiAutocomplete', [
'root',
'expanded',
'fullWidth',
'focused',
'focusVisible',
'tag',
'tagSizeSmall',
'tagSizeMedium',
'hasPopupIcon',
'hasClearIcon',
'inputRoot',
'input',
'inputFocused',
'endAdornment',
'clearIndicator',
'popupIndicator',
'popupIndicatorOpen',
'popper',
'popperDisablePortal',
'paper',
'listbox',
'loading',
'noOptions',
'option',
'groupLabel',
'groupUl',
]);
export default autocompleteClasses;

View File

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

View File

@@ -0,0 +1,4 @@
export { default, createFilterOptions } from './Autocomplete';
export { default as autocompleteClasses } from './autocompleteClasses';
export * from './autocompleteClasses';

View File

@@ -0,0 +1,133 @@
import * as React from 'react';
import { SxProps } from '@mui/system';
import { OverridableStringUnion } from '@mui/types';
import { Theme } from '../styles';
import { OverridableComponent, OverrideProps } from '../OverridableComponent';
import { AvatarClasses } from './avatarClasses';
import { CreateSlotsAndSlotProps, SlotProps } from '../utils/types';
import { SvgIconProps } from '../SvgIcon';
export interface AvatarSlots {
/**
* The component that renders the root slot.
* @default 'div'
*/
root: React.ElementType;
/**
* The component that renders the img slot.
* @default 'img'
*/
img: React.ElementType;
/**
* The component that renders the fallback slot.
* @default Person icon
*/
fallback: React.ElementType;
}
export interface AvatarPropsVariantOverrides {}
export interface AvatarRootSlotPropsOverrides {}
export interface AvatarImgSlotPropsOverrides {}
export interface AvatarFallbackSlotPropsOverrides {}
export type AvatarSlotsAndSlotProps = CreateSlotsAndSlotProps<
AvatarSlots,
{
/**
* Props forwarded to the root slot.
* By default, the available props are based on the div element.
*/
root: SlotProps<'div', AvatarRootSlotPropsOverrides, AvatarOwnProps>;
/**
* Props forwarded to the img slot.
* By default, the available props are based on the img element.
*/
img: SlotProps<'img', AvatarImgSlotPropsOverrides, AvatarOwnProps>;
/**
* Props forwarded to the fallback slot.
* By default, the available props are based on the [SvgIcon](https://mui.com/material-ui/api/svg-icon/#props) component.
*/
fallback: SlotProps<
React.ElementType<SvgIconProps>,
AvatarFallbackSlotPropsOverrides,
AvatarOwnProps
>;
}
>;
export interface AvatarOwnProps {
/**
* Used in combination with `src` or `srcSet` to
* provide an alt attribute for the rendered `img` element.
*/
alt?: string;
/**
* Used to render icon or text elements inside the Avatar if `src` is not set.
* This can be an element, or just a string.
*/
children?: React.ReactNode;
/**
* Override or extend the styles applied to the component.
*/
classes?: Partial<AvatarClasses>;
/**
* [Attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img#attributes) applied to the `img` element if the component is used to display an image.
* It can be used to listen for the loading error event.
* @deprecated Use `slotProps.img` instead. This prop will be removed in a future major release. See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
imgProps?: React.ImgHTMLAttributes<HTMLImageElement> & {
sx?: SxProps<Theme>;
};
/**
* The `sizes` attribute for the `img` element.
*/
sizes?: string;
/**
* The `src` attribute for the `img` element.
*/
src?: string;
/**
* The `srcSet` attribute for the `img` element.
* Use this attribute for responsive image display.
*/
srcSet?: string;
/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx?: SxProps<Theme>;
/**
* The shape of the avatar.
* @default 'circular'
*/
variant?: OverridableStringUnion<'circular' | 'rounded' | 'square', AvatarPropsVariantOverrides>;
}
export interface AvatarTypeMap<
AdditionalProps = {},
RootComponent extends React.ElementType = 'div',
> {
props: AdditionalProps & AvatarOwnProps & AvatarSlotsAndSlotProps;
defaultComponent: RootComponent;
}
/**
*
* Demos:
*
* - [Avatar](https://mui.com/material-ui/react-avatar/)
*
* API:
*
* - [Avatar API](https://mui.com/material-ui/api/avatar/)
*/
declare const Avatar: OverridableComponent<AvatarTypeMap>;
export type AvatarProps<
RootComponent extends React.ElementType = AvatarTypeMap['defaultComponent'],
AdditionalProps = {},
> = OverrideProps<AvatarTypeMap<AdditionalProps, RootComponent>, RootComponent> & {
component?: React.ElementType;
};
export default Avatar;

View File

@@ -0,0 +1,319 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import composeClasses from '@mui/utils/composeClasses';
import { styled } from '../zero-styled';
import memoTheme from '../utils/memoTheme';
import { useDefaultProps } from '../DefaultPropsProvider';
import Person from '../internal/svg-icons/Person';
import { getAvatarUtilityClass } from './avatarClasses';
import useSlot from '../utils/useSlot';
const useUtilityClasses = (ownerState) => {
const { classes, variant, colorDefault } = ownerState;
const slots = {
root: ['root', variant, colorDefault && 'colorDefault'],
img: ['img'],
fallback: ['fallback'],
};
return composeClasses(slots, getAvatarUtilityClass, classes);
};
const AvatarRoot = styled('div', {
name: 'MuiAvatar',
slot: 'Root',
overridesResolver: (props, styles) => {
const { ownerState } = props;
return [
styles.root,
styles[ownerState.variant],
ownerState.colorDefault && styles.colorDefault,
];
},
})(
memoTheme(({ theme }) => ({
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
width: 40,
height: 40,
fontFamily: theme.typography.fontFamily,
fontSize: theme.typography.pxToRem(20),
lineHeight: 1,
borderRadius: '50%',
overflow: 'hidden',
userSelect: 'none',
variants: [
{
props: { variant: 'rounded' },
style: {
borderRadius: (theme.vars || theme).shape.borderRadius,
},
},
{
props: { variant: 'square' },
style: {
borderRadius: 0,
},
},
{
props: { colorDefault: true },
style: {
color: (theme.vars || theme).palette.background.default,
...(theme.vars
? {
backgroundColor: theme.vars.palette.Avatar.defaultBg,
}
: {
backgroundColor: theme.palette.grey[400],
...theme.applyStyles('dark', { backgroundColor: theme.palette.grey[600] }),
}),
},
},
],
})),
);
const AvatarImg = styled('img', {
name: 'MuiAvatar',
slot: 'Img',
})({
width: '100%',
height: '100%',
textAlign: 'center',
// Handle non-square image.
objectFit: 'cover',
// Hide alt text.
color: 'transparent',
// Hide the image broken icon, only works on Chrome.
textIndent: 10000,
});
const AvatarFallback = styled(Person, {
name: 'MuiAvatar',
slot: 'Fallback',
})({
width: '75%',
height: '75%',
});
function useLoaded({ crossOrigin, referrerPolicy, src, srcSet }) {
const [loaded, setLoaded] = React.useState(false);
React.useEffect(() => {
if (!src && !srcSet) {
return undefined;
}
setLoaded(false);
let active = true;
const image = new Image();
image.onload = () => {
if (!active) {
return;
}
setLoaded('loaded');
};
image.onerror = () => {
if (!active) {
return;
}
setLoaded('error');
};
image.crossOrigin = crossOrigin;
image.referrerPolicy = referrerPolicy;
image.src = src;
if (srcSet) {
image.srcset = srcSet;
}
return () => {
active = false;
};
}, [crossOrigin, referrerPolicy, src, srcSet]);
return loaded;
}
const Avatar = React.forwardRef(function Avatar(inProps, ref) {
const props = useDefaultProps({ props: inProps, name: 'MuiAvatar' });
const {
alt,
children: childrenProp,
className,
component = 'div',
slots = {},
slotProps = {},
imgProps,
sizes,
src,
srcSet,
variant = 'circular',
...other
} = props;
let children = null;
const ownerState = {
...props,
component,
variant,
};
// Use a hook instead of onError on the img element to support server-side rendering.
const loaded = useLoaded({
...imgProps,
...(typeof slotProps.img === 'function' ? slotProps.img(ownerState) : slotProps.img),
src,
srcSet,
});
const hasImg = src || srcSet;
const hasImgNotFailing = hasImg && loaded !== 'error';
ownerState.colorDefault = !hasImgNotFailing;
// This issue explains why this is required: https://github.com/mui/material-ui/issues/42184
delete ownerState.ownerState;
const classes = useUtilityClasses(ownerState);
const [RootSlot, rootSlotProps] = useSlot('root', {
ref,
className: clsx(classes.root, className),
elementType: AvatarRoot,
externalForwardedProps: {
slots,
slotProps,
component,
...other,
},
ownerState,
});
const [ImgSlot, imgSlotProps] = useSlot('img', {
className: classes.img,
elementType: AvatarImg,
externalForwardedProps: {
slots,
slotProps: { img: { ...imgProps, ...slotProps.img } },
},
additionalProps: { alt, src, srcSet, sizes },
ownerState,
});
const [FallbackSlot, fallbackSlotProps] = useSlot('fallback', {
className: classes.fallback,
elementType: AvatarFallback,
externalForwardedProps: {
slots,
slotProps,
},
shouldForwardComponentProp: true,
ownerState,
});
if (hasImgNotFailing) {
children = <ImgSlot {...imgSlotProps} />;
// We only render valid children, non valid children are rendered with a fallback
// We consider that invalid children are all falsy values, except 0, which is valid.
} else if (!!childrenProp || childrenProp === 0) {
children = childrenProp;
} else if (hasImg && alt) {
children = alt[0];
} else {
children = <FallbackSlot {...fallbackSlotProps} />;
}
return <RootSlot {...rootSlotProps}>{children}</RootSlot>;
});
Avatar.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`. │
// └─────────────────────────────────────────────────────────────────────┘
/**
* Used in combination with `src` or `srcSet` to
* provide an alt attribute for the rendered `img` element.
*/
alt: PropTypes.string,
/**
* Used to render icon or text elements inside the Avatar if `src` is not set.
* This can be an element, or just a string.
*/
children: PropTypes.node,
/**
* Override or extend the styles applied to the component.
*/
classes: PropTypes.object,
/**
* @ignore
*/
className: PropTypes.string,
/**
* The component used for the root node.
* Either a string to use a HTML element or a component.
*/
component: PropTypes.elementType,
/**
* [Attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img#attributes) applied to the `img` element if the component is used to display an image.
* It can be used to listen for the loading error event.
* @deprecated Use `slotProps.img` instead. This prop will be removed in a future major release. See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
imgProps: PropTypes.object,
/**
* The `sizes` attribute for the `img` element.
*/
sizes: PropTypes.string,
/**
* The props used for each slot inside.
* @default {}
*/
slotProps: PropTypes.shape({
fallback: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
img: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
root: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
}),
/**
* The components used for each slot inside.
* @default {}
*/
slots: PropTypes.shape({
fallback: PropTypes.elementType,
img: PropTypes.elementType,
root: PropTypes.elementType,
}),
/**
* The `src` attribute for the `img` element.
*/
src: PropTypes.string,
/**
* The `srcSet` attribute for the `img` element.
* Use this attribute for responsive image display.
*/
srcSet: PropTypes.string,
/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])),
PropTypes.func,
PropTypes.object,
]),
/**
* The shape of the avatar.
* @default 'circular'
*/
variant: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([
PropTypes.oneOf(['circular', 'rounded', 'square']),
PropTypes.string,
]),
};
export default Avatar;

View File

@@ -0,0 +1,67 @@
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
function ImgPropsShouldSupportSx() {
<Avatar imgProps={{ sx: { objectFit: 'contain' } }} />;
}
function CustomImg() {
return <img alt="" />;
}
<Avatar slotProps={{ img: { alt: '' } }} />;
<Avatar slots={{ img: CustomImg }} />;
// Next.js Image component
interface StaticImageData {
src: string;
height: number;
width: number;
blurDataURL?: string;
blurWidth?: number;
blurHeight?: number;
}
interface StaticRequire {
default: StaticImageData;
}
type StaticImport = StaticRequire | StaticImageData;
type ImageLoaderProps = {
src: string;
width: number;
quality?: number;
};
type ImageLoader = (p: ImageLoaderProps) => string;
type PlaceholderValue = 'blur' | 'empty' | `data:image/${string}`;
type OnLoadingComplete = (img: HTMLImageElement) => void;
declare const Image: React.ForwardRefExoticComponent<
Omit<
React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>,
'height' | 'width' | 'loading' | 'ref' | 'alt' | 'src' | 'srcSet'
> & {
src: string | StaticImport;
alt: string;
width?: number | `${number}`;
height?: number | `${number}`;
fill?: boolean;
loader?: ImageLoader;
quality?: number | `${number}`;
priority?: boolean;
loading?: 'eager' | 'lazy' | undefined;
placeholder?: PlaceholderValue;
blurDataURL?: string;
unoptimized?: boolean;
overrideSrc?: string;
onLoadingComplete?: OnLoadingComplete;
layout?: string;
objectFit?: string;
objectPosition?: string;
lazyBoundary?: string;
lazyRoot?: string;
} & React.RefAttributes<HTMLImageElement | null>
>;
<Avatar slots={{ img: Image }} />;

View File

@@ -0,0 +1,294 @@
import { expect } from 'chai';
import { createRenderer, fireEvent } from '@mui/internal-test-utils';
import { spy } from 'sinon';
import Avatar, { avatarClasses as classes } from '@mui/material/Avatar';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import CancelIcon from '../internal/svg-icons/Cancel';
import describeConformance from '../../test/describeConformance';
describe('<Avatar />', () => {
const { render } = createRenderer();
describeConformance(<Avatar />, () => ({
classes,
inheritComponent: 'div',
render,
refInstanceof: window.HTMLDivElement,
testComponentPropWith: 'span',
muiName: 'MuiAvatar',
testDeepOverrides: { slotName: 'fallback', slotClassName: classes.fallback },
testVariantProps: { variant: 'foo' },
testStateOverrides: { prop: 'variant', value: 'rounded', styleKey: 'rounded' },
slots: {
root: {
expectedClassName: classes.root,
},
fallback: {
expectedClassName: classes.fallback,
},
},
skip: ['componentsProp'],
}));
describe('image avatar', () => {
it('should render a div containing an img', () => {
const { container } = render(
<Avatar
className="my-avatar"
src="/fake.png"
alt="Hello World!"
data-my-prop="woofAvatar"
/>,
);
const avatar = container.firstChild;
const img = avatar.firstChild;
expect(avatar).to.have.tagName('div');
expect(img).to.have.tagName('img');
expect(avatar).to.have.class(classes.root);
expect(avatar).to.have.class('my-avatar');
expect(avatar).to.have.attribute('data-my-prop', 'woofAvatar');
expect(avatar).not.to.have.class(classes.colorDefault);
expect(img).to.have.class(classes.img);
expect(img).to.have.attribute('alt', 'Hello World!');
expect(img).to.have.attribute('src', '/fake.png');
});
it('should be able to add more props to the image', () => {
// TODO: remove this test in v7
const onError = spy();
const { container } = render(<Avatar src="/fake.png" imgProps={{ onError }} />);
const img = container.querySelector('img');
fireEvent.error(img);
expect(onError.callCount).to.equal(1);
});
it('should be able to add more props to the img slot', () => {
const onError = spy();
const { container } = render(<Avatar src="/fake.png" slotProps={{ img: { onError } }} />);
const img = container.querySelector('img');
fireEvent.error(img);
expect(onError.callCount).to.equal(1);
});
it('should pass slots.img to `useLoaded` hook', () => {
const originalImage = globalThis.Image;
const image = {};
globalThis.Image = function Image() {
return image;
};
render(<Avatar src="/fake.png" slotProps={{ img: { crossOrigin: 'anonymous' } }} />);
expect(image.crossOrigin).to.equal('anonymous');
globalThis.Image = originalImage;
});
});
describe('image avatar with unrendered children', () => {
it('should render a div containing an img, not children', () => {
const { container } = render(<Avatar src="/fake.png">MB</Avatar>);
const avatar = container.firstChild;
const imgs = container.querySelectorAll('img');
expect(imgs.length).to.equal(1);
expect(avatar).to.have.text('');
});
it('should be able to add more props to the image', () => {
// TODO: remove this test in v7
const onError = spy();
const { container } = render(<Avatar src="/fake.png" imgProps={{ onError }} />);
const img = container.querySelector('img');
fireEvent.error(img);
expect(onError.callCount).to.equal(1);
});
it('should be able to add more props to the img slot', () => {
const onError = spy();
const { container } = render(<Avatar src="/fake.png" slotProps={{ img: { onError } }} />);
const img = container.querySelector('img');
fireEvent.error(img);
expect(onError.callCount).to.equal(1);
});
});
describe('font icon avatar', () => {
it('should render a div containing an font icon', () => {
const { container } = render(
<Avatar>
<span className="my-icon-font" data-testid="icon">
icon
</span>
</Avatar>,
);
const avatar = container.firstChild;
const icon = avatar.firstChild;
expect(avatar).to.have.tagName('div');
expect(icon).to.have.tagName('span');
expect(icon).to.have.class('my-icon-font');
expect(icon).to.have.text('icon');
});
it('should merge user classes & spread custom props to the root node', () => {
const { container } = render(
<Avatar className="my-avatar" data-my-prop="woofAvatar">
<span>icon</span>
</Avatar>,
);
const avatar = container.firstChild;
expect(avatar).to.have.class(classes.root);
expect(avatar).to.have.class('my-avatar');
expect(avatar).to.have.attribute('data-my-prop', 'woofAvatar');
});
it('should apply the colorDefault class', () => {
const { container } = render(
<Avatar data-testid="avatar">
<span>icon</span>
</Avatar>,
);
const avatar = container.firstChild;
expect(avatar).to.have.class(classes.colorDefault);
});
});
describe('svg icon avatar', () => {
it('should render a div containing an svg icon', () => {
const container = render(
<Avatar>
<CancelIcon />
</Avatar>,
).container;
const avatar = container.firstChild;
expect(avatar).to.have.tagName('div');
const cancelIcon = avatar.firstChild;
expect(cancelIcon).to.have.attribute('data-testid', 'CancelIcon');
});
it('should merge user classes & spread custom props to the root node', () => {
const container = render(
<Avatar className="my-avatar" data-my-prop="woofAvatar">
<CancelIcon />
</Avatar>,
).container;
const avatar = container.firstChild;
expect(avatar).to.have.class(classes.root);
expect(avatar).to.have.class('my-avatar');
expect(avatar).to.have.attribute('data-my-prop', 'woofAvatar');
});
it('should apply the colorDefault class', () => {
const container = render(
<Avatar>
<CancelIcon />
</Avatar>,
).container;
const avatar = container.firstChild;
expect(avatar).to.have.class(classes.colorDefault);
});
});
describe('text avatar', () => {
it('should render a div containing a string', () => {
const container = render(<Avatar>OT</Avatar>).container;
const avatar = container.firstChild;
expect(avatar).to.have.tagName('div');
expect(avatar.firstChild).to.text('OT');
});
it('should merge user classes & spread custom props to the root node', () => {
const container = render(
<Avatar className="my-avatar" data-my-prop="woofAvatar">
OT
</Avatar>,
).container;
const avatar = container.firstChild;
expect(avatar).to.have.class(classes.root);
expect(avatar).to.have.class('my-avatar');
expect(avatar).to.have.attribute('data-my-prop', 'woofAvatar');
});
it('should apply the colorDefault class', () => {
const container = render(<Avatar>OT</Avatar>).container;
const avatar = container.firstChild;
expect(avatar).to.have.class(classes.colorDefault);
});
});
describe('falsey avatar', () => {
it('should render with defaultColor class when supplied with a child with falsey value', () => {
const container = render(<Avatar>{0}</Avatar>).container;
const avatar = container.firstChild;
expect(avatar).to.have.tagName('div');
expect(avatar.firstChild).to.text('0');
});
it('should merge user classes & spread custom props to the root node', () => {
const container = render(
<Avatar className="my-avatar" data-my-prop="woofAvatar">
{0}
</Avatar>,
).container;
const avatar = container.firstChild;
expect(avatar).to.have.class(classes.root);
expect(avatar).to.have.class('my-avatar');
expect(avatar).to.have.attribute('data-my-prop', 'woofAvatar');
});
it('should apply the colorDefault class', () => {
const container = render(<Avatar>{0}</Avatar>).container;
const avatar = container.firstChild;
expect(avatar).to.have.class(classes.colorDefault);
});
it('should fallback if children is empty string', () => {
const container = render(<Avatar>{''}</Avatar>).container;
const avatar = container.firstChild;
expect(avatar.firstChild).to.have.attribute('data-testid', 'PersonIcon');
});
it('should fallback if children is false', () => {
const container = render(<Avatar>{false}</Avatar>).container;
const avatar = container.firstChild;
expect(avatar.firstChild).to.have.attribute('data-testid', 'PersonIcon');
});
});
it('should not throw error when ownerState is used in styleOverrides', () => {
const theme = createTheme({
components: {
MuiAvatar: {
styleOverrides: {
root: ({ ownerState }) => ({
...(ownerState.variant === 'rounded' && {
color: 'red',
}),
}),
},
},
},
});
expect(() =>
render(
<ThemeProvider theme={theme}>
<Avatar variant="rounded" />
</ThemeProvider>,
),
).not.to.throw();
});
});

View File

@@ -0,0 +1,37 @@
import generateUtilityClasses from '@mui/utils/generateUtilityClasses';
import generateUtilityClass from '@mui/utils/generateUtilityClass';
export interface AvatarClasses {
/** Styles applied to the root element. */
root: string;
/** Styles applied to the root element if not `src` or `srcSet`. */
colorDefault: string;
/** Styles applied to the root element if `variant="circular"`. */
circular: string;
/** Styles applied to the root element if `variant="rounded"`. */
rounded: string;
/** Styles applied to the root element if `variant="square"`. */
square: string;
/** Styles applied to the img element if either `src` or `srcSet` is defined. */
img: string;
/** Styles applied to the fallback icon */
fallback: string;
}
export type AvatarClassKey = keyof AvatarClasses;
export function getAvatarUtilityClass(slot: string): string {
return generateUtilityClass('MuiAvatar', slot);
}
const avatarClasses: AvatarClasses = generateUtilityClasses('MuiAvatar', [
'root',
'colorDefault',
'circular',
'rounded',
'square',
'img',
'fallback',
]);
export default avatarClasses;

View File

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

View File

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

View File

@@ -0,0 +1,128 @@
import * as React from 'react';
import {
OverridableComponent,
OverridableStringUnion,
OverrideProps,
PartiallyRequired,
} from '@mui/types';
import { SxProps } from '@mui/system';
import { Theme } from '../styles';
import { AvatarGroupClasses } from './avatarGroupClasses';
import Avatar from '../Avatar';
import { CreateSlotsAndSlotProps, SlotProps } from '../utils/types';
export interface AvatarGroupPropsVariantOverrides {}
export interface AvatarGroupComponentsPropsOverrides {}
export interface AvatarGroupSlots {
surplus: React.ElementType;
}
export type AvatarGroupSlotsAndSlotProps = CreateSlotsAndSlotProps<
AvatarGroupSlots,
{
/**
* @deprecated use `slotProps.surplus` 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.
* */
additionalAvatar: React.ComponentPropsWithRef<typeof Avatar> &
AvatarGroupComponentsPropsOverrides;
surplus: SlotProps<
React.ElementType<React.ComponentPropsWithRef<typeof Avatar>>,
AvatarGroupComponentsPropsOverrides,
AvatarGroupOwnerState
>;
}
>;
export interface AvatarGroupOwnProps extends AvatarGroupSlotsAndSlotProps {
/**
* The avatars to stack.
*/
children?: React.ReactNode;
/**
* Override or extend the styles applied to the component.
*/
classes?: Partial<AvatarGroupClasses>;
/**
* The component used for the root node.
* Either a string to use a HTML element or a component.
*/
component?: React.ElementType;
/**
* The extra props for the slot components.
* You can override the existing props or add new ones.
*
* This prop is an alias for the `slotProps` prop.
*
* @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.
*/
componentsProps?: {
additionalAvatar?: React.ComponentPropsWithRef<typeof Avatar> &
AvatarGroupComponentsPropsOverrides;
};
/**
* Max avatars to show before +x.
* @default 5
*/
max?: number;
/**
* custom renderer of extraAvatars
* @param {number} surplus number of extra avatars
* @returns {React.ReactNode} custom element to display
*/
renderSurplus?: (surplus: number) => React.ReactNode;
/**
* Spacing between avatars.
* @default 'medium'
*/
spacing?: 'small' | 'medium' | number;
/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx?: SxProps<Theme>;
/**
* The total number of avatars. Used for calculating the number of extra avatars.
* @default children.length
*/
total?: number;
/**
* The variant to use.
* @default 'circular'
*/
variant?: OverridableStringUnion<
'circular' | 'rounded' | 'square',
AvatarGroupPropsVariantOverrides
>;
}
export interface AvatarGroupTypeMap<
AdditionalProps = {},
RootComponent extends React.ElementType = 'div',
> {
props: AdditionalProps & AvatarGroupOwnProps;
defaultComponent: RootComponent;
}
/**
*
* Demos:
*
* - [Avatar](https://mui.com/material-ui/react-avatar/)
*
* API:
*
* - [AvatarGroup API](https://mui.com/material-ui/api/avatar-group/)
*/
declare const AvatarGroup: OverridableComponent<AvatarGroupTypeMap>;
export type AvatarGroupProps<
RootComponent extends React.ElementType = AvatarGroupTypeMap['defaultComponent'],
AdditionalProps = {},
> = OverrideProps<AvatarGroupTypeMap<AdditionalProps, RootComponent>, RootComponent> & {
component?: React.ElementType;
};
export interface AvatarGroupOwnerState
extends PartiallyRequired<AvatarGroupProps, 'max' | 'spacing' | 'component' | 'variant'> {}
export default AvatarGroup;

View File

@@ -0,0 +1,268 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
import { isFragment } from 'react-is';
import clsx from 'clsx';
import chainPropTypes from '@mui/utils/chainPropTypes';
import composeClasses from '@mui/utils/composeClasses';
import { styled } from '../zero-styled';
import memoTheme from '../utils/memoTheme';
import { useDefaultProps } from '../DefaultPropsProvider';
import Avatar, { avatarClasses } from '../Avatar';
import avatarGroupClasses, { getAvatarGroupUtilityClass } from './avatarGroupClasses';
import useSlot from '../utils/useSlot';
const SPACINGS = {
small: -16,
medium: -8,
};
const useUtilityClasses = (ownerState) => {
const { classes } = ownerState;
const slots = {
root: ['root'],
avatar: ['avatar'],
};
return composeClasses(slots, getAvatarGroupUtilityClass, classes);
};
const AvatarGroupRoot = styled('div', {
name: 'MuiAvatarGroup',
slot: 'Root',
overridesResolver: (props, styles) => {
return [{ [`& .${avatarGroupClasses.avatar}`]: styles.avatar }, styles.root];
},
})(
memoTheme(({ theme }) => ({
display: 'flex',
flexDirection: 'row-reverse',
[`& .${avatarClasses.root}`]: {
border: `2px solid ${(theme.vars || theme).palette.background.default}`,
boxSizing: 'content-box',
marginLeft: 'var(--AvatarGroup-spacing, -8px)',
'&:last-child': {
marginLeft: 0,
},
},
})),
);
const AvatarGroup = React.forwardRef(function AvatarGroup(inProps, ref) {
const props = useDefaultProps({
props: inProps,
name: 'MuiAvatarGroup',
});
const {
children: childrenProp,
className,
component = 'div',
componentsProps,
max = 5,
renderSurplus,
slotProps = {},
slots = {},
spacing = 'medium',
total,
variant = 'circular',
...other
} = props;
let clampedMax = max < 2 ? 2 : max;
const ownerState = {
...props,
max,
spacing,
component,
variant,
};
const classes = useUtilityClasses(ownerState);
const children = React.Children.toArray(childrenProp).filter((child) => {
if (process.env.NODE_ENV !== 'production') {
if (isFragment(child)) {
console.error(
[
"MUI: The AvatarGroup component doesn't accept a Fragment as a child.",
'Consider providing an array instead.',
].join('\n'),
);
}
}
return React.isValidElement(child);
});
const totalAvatars = total || children.length;
if (totalAvatars === clampedMax) {
clampedMax += 1;
}
clampedMax = Math.min(totalAvatars + 1, clampedMax);
const maxAvatars = Math.min(children.length, clampedMax - 1);
const extraAvatars = Math.max(totalAvatars - clampedMax, totalAvatars - maxAvatars, 0);
const extraAvatarsElement = renderSurplus ? renderSurplus(extraAvatars) : `+${extraAvatars}`;
let marginValue;
if (ownerState.spacing && SPACINGS[ownerState.spacing] !== undefined) {
marginValue = SPACINGS[ownerState.spacing];
} else if (ownerState.spacing === 0) {
marginValue = 0;
} else {
marginValue = -ownerState.spacing || SPACINGS.medium;
}
const externalForwardedProps = {
slots,
slotProps: {
surplus: slotProps.additionalAvatar ?? componentsProps?.additionalAvatar,
...componentsProps,
...slotProps,
},
};
const [SurplusSlot, surplusProps] = useSlot('surplus', {
elementType: Avatar,
externalForwardedProps,
className: classes.avatar,
ownerState,
additionalProps: {
variant,
},
});
return (
<AvatarGroupRoot
as={component}
ownerState={ownerState}
className={clsx(classes.root, className)}
ref={ref}
{...other}
style={{
'--AvatarGroup-spacing': `${marginValue}px`, // marginValue is always defined
...other.style,
}}
>
{extraAvatars ? <SurplusSlot {...surplusProps}>{extraAvatarsElement}</SurplusSlot> : null}
{children
.slice(0, maxAvatars)
.reverse()
.map((child) => {
return React.cloneElement(child, {
className: clsx(child.props.className, classes.avatar),
variant: child.props.variant || variant,
});
})}
</AvatarGroupRoot>
);
});
AvatarGroup.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 avatars to stack.
*/
children: PropTypes.node,
/**
* Override or extend the styles applied to the component.
*/
classes: PropTypes.object,
/**
* @ignore
*/
className: PropTypes.string,
/**
* The component used for the root node.
* Either a string to use a HTML element or a component.
*/
component: PropTypes.elementType,
/**
* The extra props for the slot components.
* You can override the existing props or add new ones.
*
* This prop is an alias for the `slotProps` prop.
*
* @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.
*/
componentsProps: PropTypes.shape({
additionalAvatar: PropTypes.object,
}),
/**
* Max avatars to show before +x.
* @default 5
*/
max: chainPropTypes(PropTypes.number, (props) => {
if (props.max < 2) {
return new Error(
[
'MUI: The prop `max` should be equal to 2 or above.',
'A value below is clamped to 2.',
].join('\n'),
);
}
return null;
}),
/**
* custom renderer of extraAvatars
* @param {number} surplus number of extra avatars
* @returns {React.ReactNode} custom element to display
*/
renderSurplus: PropTypes.func,
/**
* The props used for each slot inside.
* @default {}
*/
slotProps: PropTypes.shape({
additionalAvatar: PropTypes.object,
surplus: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
}),
/**
* The components used for each slot inside.
* @default {}
*/
slots: PropTypes.shape({
surplus: PropTypes.elementType,
}),
/**
* Spacing between avatars.
* @default 'medium'
*/
spacing: PropTypes.oneOfType([PropTypes.oneOf(['medium', 'small']), PropTypes.number]),
/**
* @ignore
*/
style: PropTypes.object,
/**
* 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 total number of avatars. Used for calculating the number of extra avatars.
* @default children.length
*/
total: PropTypes.number,
/**
* The variant to use.
* @default 'circular'
*/
variant: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([
PropTypes.oneOf(['circular', 'rounded', 'square']),
PropTypes.string,
]),
};
export default AvatarGroup;

View File

@@ -0,0 +1,17 @@
import { expectType } from '@mui/types';
import AvatarGroup from '@mui/material/AvatarGroup';
<AvatarGroup component="ul" />;
<AvatarGroup variant="circular" />;
<AvatarGroup variant="rounded" />;
<AvatarGroup variant="square" />;
// @ts-expect-error
<AvatarGroup variant="unknown" />;
<AvatarGroup
renderSurplus={(surplus) => {
expectType<number, number>(surplus);
return <div>{surplus}</div>;
}}
/>;

View File

@@ -0,0 +1,225 @@
import { expect } from 'chai';
import { createRenderer } from '@mui/internal-test-utils';
import Avatar from '@mui/material/Avatar';
import AvatarGroup, { avatarGroupClasses as classes } from '@mui/material/AvatarGroup';
import describeConformance from '../../test/describeConformance';
describe('<AvatarGroup />', () => {
const { render } = createRenderer();
describeConformance(
<AvatarGroup max={2}>
<Avatar src="/fake.png" />
<Avatar src="/fake.png" />
<Avatar src="/fake.png" />
</AvatarGroup>,
() => ({
classes,
inheritComponent: 'div',
render,
muiName: 'MuiAvatarGroup',
refInstanceof: window.HTMLDivElement,
testVariantProps: { max: 10, spacing: 'small', variant: 'square' },
slots: {
surplus: { expectedClassName: classes.avatar },
},
skip: ['componentsProp'],
}),
);
// test additionalAvatar slot separately
describeConformance(
<AvatarGroup max={2}>
<Avatar src="/fake.png" />
<Avatar src="/fake.png" />
<Avatar src="/fake.png" />
</AvatarGroup>,
() => ({
classes,
render,
muiName: 'MuiAvatarGroup',
slots: {
additionalAvatar: { expectedClassName: classes.avatar },
},
only: ['slotPropsProp'],
}),
);
it('should render avatars with spacing of 0px when spacing is 0', () => {
const { container } = render(
<AvatarGroup spacing={0}>
<Avatar src="/fake.png" />
<Avatar src="/fake.png" />
<Avatar src="/fake.png" />
</AvatarGroup>,
);
const avatarGroupRoot = container.firstChild;
const avatarGroupStyle = avatarGroupRoot.style.getPropertyValue('--AvatarGroup-spacing');
expect(avatarGroupStyle).to.equal('0px');
});
it('should display all the avatars', () => {
const { container } = render(
<AvatarGroup max={3}>
<Avatar src="/fake.png" />
<Avatar src="/fake.png" />
<Avatar src="/fake.png" />
</AvatarGroup>,
);
expect(container.querySelectorAll('.MuiAvatar-root').length).to.equal(3);
expect(container.querySelectorAll('img').length).to.equal(3);
expect(container.textContent).to.equal('');
});
it('should display 2 avatars and "+2"', () => {
const { container } = render(
<AvatarGroup max={3}>
<Avatar src="/fake.png" />
<Avatar src="/fake.png" />
<Avatar src="/fake.png" />
<Avatar src="/fake.png" />
</AvatarGroup>,
);
expect(container.querySelectorAll('.MuiAvatar-root').length).to.equal(3);
expect(container.querySelectorAll('img').length).to.equal(2);
expect(container.textContent).to.equal('+2');
});
it('should display custom surplus element if renderSurplus prop is passed', () => {
const { container } = render(
<AvatarGroup renderSurplus={(num) => <span>%{num}</span>} max={3}>
<Avatar src="/fake.png" />
<Avatar src="/fake.png" />
<Avatar src="/fake.png" />
<Avatar src="/fake.png" />
</AvatarGroup>,
);
expect(container.textContent).to.equal('%2');
});
it('should pass props from componentsProps.additionalAvatar to the slot component', () => {
const componentsProps = { additionalAvatar: { className: 'additional-avatar-test' } };
const { container } = render(
<AvatarGroup max={3} componentsProps={componentsProps}>
<Avatar src="/fake.png" />
<Avatar src="/fake.png" />
<Avatar src="/fake.png" />
<Avatar src="/fake.png" />
</AvatarGroup>,
);
const additionalAvatar = container.querySelector('.additional-avatar-test');
expect(additionalAvatar.classList.contains('additional-avatar-test')).to.equal(true);
});
it('should respect total', () => {
const { container } = render(
<AvatarGroup total={10}>
<Avatar src="/fake.png" />
<Avatar src="/fake.png" />
<Avatar src="/fake.png" />
</AvatarGroup>,
);
expect(container.querySelectorAll('.MuiAvatar-root').length).to.equal(4);
expect(container.querySelectorAll('img').length).to.equal(3);
expect(container.textContent).to.equal('+7');
});
it('should respect both total and max', () => {
const { container } = render(
<AvatarGroup max={2} total={3}>
<Avatar src="/fake.png" />
<Avatar src="/fake.png" />
</AvatarGroup>,
);
expect(container.querySelectorAll('.MuiAvatar-root').length).to.equal(2);
expect(container.querySelectorAll('img').length).to.equal(1);
expect(container.textContent).to.equal('+2');
});
it('should respect total and clamp down shown avatars', () => {
const { container } = render(
<AvatarGroup total={1}>
<Avatar src="/fake.png" />
<Avatar src="/fake.png" />
</AvatarGroup>,
);
expect(container.querySelectorAll('.MuiAvatar-root').length).to.equal(1);
expect(container.querySelectorAll('img').length).to.equal(1);
expect(container.textContent).to.equal('');
});
it('should display extra if clamp max is >= total', () => {
const { container } = render(
<AvatarGroup total={10} max={10}>
<Avatar src="/fake.png" />
<Avatar src="/fake.png" />
</AvatarGroup>,
);
expect(container.querySelectorAll('.MuiAvatar-root').length).to.equal(3);
expect(container.querySelectorAll('img').length).to.equal(2);
expect(container.textContent).to.equal('+8');
});
it('should display all avatars if total === max === children.length', () => {
const { container } = render(
<AvatarGroup total={4} max={4}>
<Avatar src="/fake.png" />
<Avatar src="/fake.png" />
<Avatar src="/fake.png" />
<Avatar src="/fake.png" />
</AvatarGroup>,
);
expect(container.querySelectorAll('.MuiAvatar-root').length).to.equal(4);
expect(container.querySelectorAll('img').length).to.equal(4);
expect(container.textContent).to.equal('');
});
it('should display all avatars with default (circular) variant', () => {
const { container } = render(
<AvatarGroup>
<Avatar src="/fake.png" />
</AvatarGroup>,
);
const avatarGroup = container.firstChild;
const avatar = avatarGroup.firstChild;
expect(container.querySelectorAll('.MuiAvatar-root').length).to.equal(
avatarGroup.childNodes.length,
);
expect(avatar).to.have.class('MuiAvatar-circular');
expect(avatar).not.to.have.class('MuiAvatar-rounded');
expect(avatar).not.to.have.class('MuiAvatar-square');
});
it('should display all avatars with the specified variant', () => {
const { container } = render(
<AvatarGroup variant="square">
<Avatar src="/fake.png" />
</AvatarGroup>,
);
const avatarGroup = container.firstChild;
const avatar = avatarGroup.firstChild;
expect(container.querySelectorAll('.MuiAvatar-root').length).to.equal(
avatarGroup.childNodes.length,
);
expect(avatar).to.have.class('MuiAvatar-square');
expect(avatar).not.to.have.class('MuiAvatar-circular');
expect(avatar).not.to.have.class('MuiAvatar-rounded');
});
it("should respect child's avatar variant prop if specified", () => {
const { container } = render(
<AvatarGroup variant="square">
<Avatar src="/fake.png" variant="rounded" />
</AvatarGroup>,
);
const avatarGroup = container.firstChild;
const roundedAvatar = avatarGroup.firstChild;
expect(roundedAvatar).to.have.class('MuiAvatar-rounded');
expect(roundedAvatar).not.to.have.class('MuiAvatar-circular');
expect(roundedAvatar).not.to.have.class('MuiAvatar-square');
});
});

View File

@@ -0,0 +1,22 @@
import generateUtilityClasses from '@mui/utils/generateUtilityClasses';
import generateUtilityClass from '@mui/utils/generateUtilityClass';
export interface AvatarGroupClasses {
/** Styles applied to the root element. */
root: string;
/** Styles applied to the avatar elements. */
avatar: string;
}
export type AvatarGroupClassKey = keyof AvatarGroupClasses;
export function getAvatarGroupUtilityClass(slot: string): string {
return generateUtilityClass('MuiAvatarGroup', slot);
}
const avatarGroupClasses: AvatarGroupClasses = generateUtilityClasses('MuiAvatarGroup', [
'root',
'avatar',
]);
export default avatarGroupClasses;

View File

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

View File

@@ -0,0 +1,3 @@
export { default } from './AvatarGroup';
export { default as avatarGroupClasses } from './avatarGroupClasses';
export * from './avatarGroupClasses';

View File

@@ -0,0 +1,145 @@
import * as React from 'react';
import { SxProps } from '@mui/system';
import { FadeProps } from '../Fade';
import { TransitionProps } from '../transitions/transition';
import { Theme } from '../styles';
import { BackdropClasses } from './backdropClasses';
import { OverridableComponent, OverrideProps } from '../OverridableComponent';
import { CreateSlotsAndSlotProps, SlotComponentProps, SlotProps } from '../utils/types';
export interface BackdropSlots {
/**
* The component that renders the root.
* @default 'div'
*/
root: React.ElementType;
/**
* The component that renders the transition.
* [Follow this guide](https://mui.com/material-ui/transitions/#transitioncomponent-prop) to learn more about the requirements for this component.
* @default Fade
*/
transition: React.ElementType;
}
export interface BackdropComponentsPropsOverrides {}
export interface BackdropTransitionSlotPropsOverrides {}
export type BackdropSlotsAndSlotProps = CreateSlotsAndSlotProps<
BackdropSlots,
{
/**
* Props forwarded to the transition slot.
* By default, the available props are based on the div element.
*/
root: SlotProps<'div', BackdropComponentsPropsOverrides, BackdropOwnerState>;
/**
* Props forwarded to the transition slot.
* By default, the available props are based on the [Fade](https://mui.com/material-ui/api/fade/#props) component.
*/
transition: SlotComponentProps<
React.ElementType,
TransitionProps & BackdropTransitionSlotPropsOverrides,
BackdropOwnerState
>;
}
>;
export interface BackdropOwnProps
extends Partial<Omit<FadeProps, 'children'>>,
BackdropSlotsAndSlotProps {
/**
* The content of the component.
*/
children?: React.ReactNode;
/**
* 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;
};
/**
* 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?: {
root?: React.HTMLAttributes<HTMLDivElement> & BackdropComponentsPropsOverrides;
};
/**
* Override or extend the styles applied to the component.
*/
classes?: Partial<BackdropClasses>;
/**
* If `true`, the backdrop is invisible.
* It can be used when rendering a popover or a custom select component.
* @default false
*/
invisible?: boolean;
/**
* If `true`, the component is shown.
*/
open: boolean;
/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx?: SxProps<Theme>;
/**
* The duration for the transition, in milliseconds.
* You may specify a single timeout for all transitions, or individually with an object.
*/
transitionDuration?: TransitionProps['timeout'];
/**
* The component used for the transition.
* [Follow this guide](https://mui.com/material-ui/transitions/#transitioncomponent-prop) to learn more about the requirements for this component.
* @default Fade
* @deprecated Use `slots.transition` instead. This prop will be removed in a future major release. See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
TransitionComponent?: React.JSXElementConstructor<
TransitionProps & {
children: React.ReactElement<unknown, any>;
}
>;
}
export interface BackdropTypeMap<
AdditionalProps = {},
RootComponent extends React.ElementType = 'div',
> {
props: AdditionalProps & BackdropOwnProps;
defaultComponent: RootComponent;
}
type BackdropRootProps = NonNullable<BackdropTypeMap['props']['componentsProps']>['root'];
export declare const BackdropRoot: React.FC<BackdropRootProps>;
/**
*
* Demos:
*
* - [Backdrop](https://mui.com/material-ui/react-backdrop/)
*
* API:
*
* - [Backdrop API](https://mui.com/material-ui/api/backdrop/)
* - inherits [Fade API](https://mui.com/material-ui/api/fade/)
*/
declare const Backdrop: OverridableComponent<BackdropTypeMap>;
export type BackdropProps<
RootComponent extends React.ElementType = BackdropTypeMap['defaultComponent'],
AdditionalProps = {},
> = OverrideProps<BackdropTypeMap<AdditionalProps, RootComponent>, RootComponent> & {
component?: React.ElementType;
};
export interface BackdropOwnerState extends BackdropProps {}
export default Backdrop;

View File

@@ -0,0 +1,208 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import composeClasses from '@mui/utils/composeClasses';
import { styled } from '../zero-styled';
import { useDefaultProps } from '../DefaultPropsProvider';
import useSlot from '../utils/useSlot';
import Fade from '../Fade';
import { getBackdropUtilityClass } from './backdropClasses';
const useUtilityClasses = (ownerState) => {
const { classes, invisible } = ownerState;
const slots = {
root: ['root', invisible && 'invisible'],
};
return composeClasses(slots, getBackdropUtilityClass, classes);
};
const BackdropRoot = styled('div', {
name: 'MuiBackdrop',
slot: 'Root',
overridesResolver: (props, styles) => {
const { ownerState } = props;
return [styles.root, ownerState.invisible && styles.invisible];
},
})({
position: 'fixed',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
right: 0,
bottom: 0,
top: 0,
left: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
WebkitTapHighlightColor: 'transparent',
variants: [
{
props: { invisible: true },
style: {
backgroundColor: 'transparent',
},
},
],
});
const Backdrop = React.forwardRef(function Backdrop(inProps, ref) {
const props = useDefaultProps({ props: inProps, name: 'MuiBackdrop' });
const {
children,
className,
component = 'div',
invisible = false,
open,
components = {},
componentsProps = {},
slotProps = {},
slots = {},
TransitionComponent: TransitionComponentProp,
transitionDuration,
...other
} = props;
const ownerState = {
...props,
component,
invisible,
};
const classes = useUtilityClasses(ownerState);
const backwardCompatibleSlots = {
transition: TransitionComponentProp,
root: components.Root,
...slots,
};
const backwardCompatibleSlotProps = { ...componentsProps, ...slotProps };
const externalForwardedProps = {
component,
slots: backwardCompatibleSlots,
slotProps: backwardCompatibleSlotProps,
};
const [RootSlot, rootProps] = useSlot('root', {
elementType: BackdropRoot,
externalForwardedProps,
className: clsx(classes.root, className),
ownerState,
});
const [TransitionSlot, transitionProps] = useSlot('transition', {
elementType: Fade,
externalForwardedProps,
ownerState,
});
return (
<TransitionSlot in={open} timeout={transitionDuration} {...other} {...transitionProps}>
<RootSlot aria-hidden {...rootProps} classes={classes} ref={ref}>
{children}
</RootSlot>
</TransitionSlot>
);
});
Backdrop.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 content of the component.
*/
children: PropTypes.node,
/**
* Override or extend the styles applied to the component.
*/
classes: PropTypes.object,
/**
* @ignore
*/
className: 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({
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({
root: PropTypes.object,
}),
/**
* If `true`, the backdrop is invisible.
* It can be used when rendering a popover or a custom select component.
* @default false
*/
invisible: PropTypes.bool,
/**
* If `true`, the component is shown.
*/
open: PropTypes.bool.isRequired,
/**
* The props used for each slot inside.
* @default {}
*/
slotProps: PropTypes.shape({
root: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
transition: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
}),
/**
* The components used for each slot inside.
* @default {}
*/
slots: PropTypes.shape({
root: PropTypes.elementType,
transition: 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 component used for the transition.
* [Follow this guide](https://mui.com/material-ui/transitions/#transitioncomponent-prop) to learn more about the requirements for this component.
* @default Fade
* @deprecated Use `slots.transition` instead. This prop will be removed in a future major release. See [Migrating from deprecated APIs](/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
TransitionComponent: PropTypes.elementType,
/**
* The duration for the transition, in milliseconds.
* You may specify a single timeout for all transitions, or individually with an object.
*/
transitionDuration: PropTypes.oneOfType([
PropTypes.number,
PropTypes.shape({
appear: PropTypes.number,
enter: PropTypes.number,
exit: PropTypes.number,
}),
]),
};
export default Backdrop;

View File

@@ -0,0 +1,56 @@
import { expect } from 'chai';
import { spy } from 'sinon';
import { createRenderer } from '@mui/internal-test-utils';
import Backdrop, { backdropClasses as classes } from '@mui/material/Backdrop';
import Fade from '@mui/material/Fade';
import describeConformance from '../../test/describeConformance';
describe('<Backdrop />', () => {
const { clock, render } = createRenderer();
describeConformance(<Backdrop open />, () => ({
classes,
inheritComponent: Fade,
render,
refInstanceof: window.HTMLDivElement,
muiName: 'MuiBackdrop',
testVariantProps: { invisible: true },
slots: {
root: {
expectedClassName: classes.root,
},
transition: {
testWithElement: null,
},
},
skip: ['componentsProp'],
}));
it('should render a backdrop div with content of nested children', () => {
const { container } = render(
<Backdrop open>
<h1>Hello World</h1>
</Backdrop>,
);
expect(container.querySelector('h1')).to.have.text('Hello World');
});
describe('prop: transitionDuration', () => {
clock.withFakeTimers();
it('delays appearance of its children', () => {
const handleEntered = spy();
render(
<Backdrop open transitionDuration={1954} onEntered={handleEntered}>
<div />
</Backdrop>,
);
expect(handleEntered.callCount).to.equal(0);
clock.tick(1954);
expect(handleEntered.callCount).to.equal(1);
});
});
});

View File

@@ -0,0 +1,22 @@
import generateUtilityClasses from '@mui/utils/generateUtilityClasses';
import generateUtilityClass from '@mui/utils/generateUtilityClass';
export interface BackdropClasses {
/** Styles applied to the root element. */
root: string;
/** Styles applied to the root element if `invisible={true}`. */
invisible: string;
}
export type BackdropClassKey = keyof BackdropClasses;
export function getBackdropUtilityClass(slot: string): string {
return generateUtilityClass('MuiBackdrop', slot);
}
const backdropClasses: BackdropClasses = generateUtilityClasses('MuiBackdrop', [
'root',
'invisible',
]);
export default backdropClasses;

View File

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

View File

@@ -0,0 +1,3 @@
export { default } from './Backdrop';
export { default as backdropClasses } from './backdropClasses';
export * from './backdropClasses';

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;
}

View File

@@ -0,0 +1,65 @@
import * as React from 'react';
import { SxProps } from '@mui/system';
import { Theme } from '../styles';
import { OverridableComponent, OverrideProps } from '../OverridableComponent';
import { BottomNavigationClasses } from './bottomNavigationClasses';
export interface BottomNavigationOwnProps {
/**
* The content of the component.
*/
children?: React.ReactNode;
/**
* Override or extend the styles applied to the component.
*/
classes?: Partial<BottomNavigationClasses>;
/**
* Callback fired when the value changes.
*
* @param {React.SyntheticEvent} event The event source of the callback. **Warning**: This is a generic event not a change event.
* @param {any} value We default to the index of the child.
*/
onChange?: (event: React.SyntheticEvent, value: any) => void;
/**
* If `true`, all `BottomNavigationAction`s will show their labels.
* By default, only the selected `BottomNavigationAction` will show its label.
* @default false
*/
showLabels?: boolean;
/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx?: SxProps<Theme>;
/**
* The value of the currently selected `BottomNavigationAction`.
*/
value?: any;
}
export interface BottomNavigationTypeMap<
AdditionalProps = {},
RootComponent extends React.ElementType = 'div',
> {
props: AdditionalProps & BottomNavigationOwnProps;
defaultComponent: RootComponent;
}
/**
*
* Demos:
*
* - [Bottom Navigation](https://mui.com/material-ui/react-bottom-navigation/)
*
* API:
*
* - [BottomNavigation API](https://mui.com/material-ui/api/bottom-navigation/)
*/
declare const BottomNavigation: OverridableComponent<BottomNavigationTypeMap>;
export type BottomNavigationProps<
RootComponent extends React.ElementType = BottomNavigationTypeMap['defaultComponent'],
AdditionalProps = {},
> = OverrideProps<BottomNavigationTypeMap<AdditionalProps, RootComponent>, RootComponent> & {
component?: React.ElementType;
};
export default BottomNavigation;

View File

@@ -0,0 +1,140 @@
'use client';
import * as React from 'react';
import { isFragment } from 'react-is';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import composeClasses from '@mui/utils/composeClasses';
import { styled } from '../zero-styled';
import memoTheme from '../utils/memoTheme';
import { useDefaultProps } from '../DefaultPropsProvider';
import { getBottomNavigationUtilityClass } from './bottomNavigationClasses';
const useUtilityClasses = (ownerState) => {
const { classes } = ownerState;
const slots = {
root: ['root'],
};
return composeClasses(slots, getBottomNavigationUtilityClass, classes);
};
const BottomNavigationRoot = styled('div', {
name: 'MuiBottomNavigation',
slot: 'Root',
})(
memoTheme(({ theme }) => ({
display: 'flex',
justifyContent: 'center',
height: 56,
backgroundColor: (theme.vars || theme).palette.background.paper,
})),
);
const BottomNavigation = React.forwardRef(function BottomNavigation(inProps, ref) {
const props = useDefaultProps({ props: inProps, name: 'MuiBottomNavigation' });
const {
children,
className,
component = 'div',
onChange,
showLabels = false,
value,
...other
} = props;
const ownerState = {
...props,
component,
showLabels,
};
const classes = useUtilityClasses(ownerState);
return (
<BottomNavigationRoot
as={component}
className={clsx(classes.root, className)}
ref={ref}
ownerState={ownerState}
{...other}
>
{React.Children.map(children, (child, childIndex) => {
if (!React.isValidElement(child)) {
return null;
}
if (process.env.NODE_ENV !== 'production') {
if (isFragment(child)) {
console.error(
[
"MUI: The BottomNavigation component doesn't accept a Fragment as a child.",
'Consider providing an array instead.',
].join('\n'),
);
}
}
const childValue = child.props.value === undefined ? childIndex : child.props.value;
return React.cloneElement(child, {
selected: childValue === value,
showLabel: child.props.showLabel !== undefined ? child.props.showLabel : showLabels,
value: childValue,
onChange,
});
})}
</BottomNavigationRoot>
);
});
BottomNavigation.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 content of the component.
*/
children: PropTypes.node,
/**
* Override or extend the styles applied to the component.
*/
classes: PropTypes.object,
/**
* @ignore
*/
className: PropTypes.string,
/**
* The component used for the root node.
* Either a string to use a HTML element or a component.
*/
component: PropTypes.elementType,
/**
* Callback fired when the value changes.
*
* @param {React.SyntheticEvent} event The event source of the callback. **Warning**: This is a generic event not a change event.
* @param {any} value We default to the index of the child.
*/
onChange: PropTypes.func,
/**
* If `true`, all `BottomNavigationAction`s will show their labels.
* By default, only the selected `BottomNavigationAction` will show its label.
* @default false
*/
showLabels: PropTypes.bool,
/**
* 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 value of the currently selected `BottomNavigationAction`.
*/
value: PropTypes.any,
};
export default BottomNavigation;

View File

@@ -0,0 +1,14 @@
import * as React from 'react';
import BottomNavigation from '@mui/material/BottomNavigation';
function testOnChange() {
function handleBottomNavigationChange(event: React.SyntheticEvent, tabsValue: unknown) {}
<BottomNavigation onChange={handleBottomNavigationChange} />;
function handleElementChange(event: React.ChangeEvent) {}
<BottomNavigation
// @ts-expect-error internally it's whatever even lead to a change in value
onChange={handleElementChange}
/>;
}

View File

@@ -0,0 +1,109 @@
import { expect } from 'chai';
import { spy } from 'sinon';
import { createRenderer, fireEvent, screen } from '@mui/internal-test-utils';
import BottomNavigation, {
bottomNavigationClasses as classes,
} from '@mui/material/BottomNavigation';
import BottomNavigationAction, {
bottomNavigationActionClasses as actionClasses,
} from '@mui/material/BottomNavigationAction';
import Icon from '@mui/material/Icon';
import describeConformance from '../../test/describeConformance';
describe('<BottomNavigation />', () => {
const { render } = createRenderer();
const icon = <Icon>restore</Icon>;
const getBottomNavigation = (container) => container.firstChild;
describeConformance(
<BottomNavigation>
<BottomNavigationAction label="One" />
</BottomNavigation>,
() => ({
classes,
inheritComponent: 'div',
render,
muiName: 'MuiBottomNavigation',
refInstanceof: window.HTMLDivElement,
testComponentPropWith: 'span',
skip: ['componentsProp', 'themeVariants'],
}),
);
it('renders with a null child', () => {
const { container } = render(
<BottomNavigation showLabels value={0}>
<BottomNavigationAction label="One" />
{null}
<BottomNavigationAction label="Three" />
</BottomNavigation>,
);
expect(getBottomNavigation(container).childNodes.length).to.equal(2);
});
it('should pass selected prop to children', () => {
const { container } = render(
<BottomNavigation showLabels value={1}>
<BottomNavigationAction icon={icon} />
<BottomNavigationAction icon={icon} />
</BottomNavigation>,
);
expect(getBottomNavigation(container).childNodes[0]).not.to.have.class(actionClasses.selected);
expect(getBottomNavigation(container).childNodes[1]).to.have.class(actionClasses.selected);
});
it('should overwrite parent showLabel prop adding class iconOnly', () => {
render(
<BottomNavigation showLabels>
<BottomNavigationAction icon={icon} data-testid="withLabel" />
<BottomNavigationAction icon={icon} showLabel={false} data-testid="withoutLabel" />
</BottomNavigation>,
);
expect(screen.getByTestId('withLabel')).not.to.have.class(actionClasses.iconOnly);
expect(screen.getByTestId('withoutLabel')).to.have.class(actionClasses.iconOnly);
});
it('should forward the click', () => {
const handleChange = spy();
const { container } = render(
<BottomNavigation showLabels value={0} onChange={handleChange}>
<BottomNavigationAction icon={icon} />
<BottomNavigationAction icon={icon} />
</BottomNavigation>,
);
fireEvent.click(getBottomNavigation(container).childNodes[1]);
expect(handleChange.callCount).to.equal(1);
expect(handleChange.args[0][1]).to.equal(1);
});
it('should use custom action values', () => {
const handleChange = spy();
const { container } = render(
<BottomNavigation showLabels value={'first'} onChange={handleChange}>
<BottomNavigationAction value="first" icon={icon} />
<BottomNavigationAction value="second" icon={icon} />
</BottomNavigation>,
);
fireEvent.click(getBottomNavigation(container).childNodes[1]);
expect(handleChange.args[0][1]).to.equal('second', 'should have been called with value second');
});
it('should handle also empty action value', () => {
const handleChange = spy();
const { container } = render(
<BottomNavigation showLabels value="val" onChange={handleChange}>
<BottomNavigationAction value="" icon={icon} />
<BottomNavigationAction icon={icon} />
<BottomNavigationAction value={null} icon={icon} />
</BottomNavigation>,
);
fireEvent.click(getBottomNavigation(container).childNodes[0]);
expect(handleChange.args[0][1], '');
fireEvent.click(getBottomNavigation(container).childNodes[1]);
expect(handleChange.args[1][1], 1);
fireEvent.click(getBottomNavigation(container).childNodes[2]);
expect(handleChange.args[2][1], '');
});
});

View File

@@ -0,0 +1,20 @@
import generateUtilityClasses from '@mui/utils/generateUtilityClasses';
import generateUtilityClass from '@mui/utils/generateUtilityClass';
export interface BottomNavigationClasses {
/** Styles applied to the root element. */
root: string;
}
export type BottomNavigationClassKey = keyof BottomNavigationClasses;
export function getBottomNavigationUtilityClass(slot: string): string {
return generateUtilityClass('MuiBottomNavigation', slot);
}
const bottomNavigationClasses: BottomNavigationClasses = generateUtilityClasses(
'MuiBottomNavigation',
['root'],
);
export default bottomNavigationClasses;

View File

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

View File

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

View File

@@ -0,0 +1,112 @@
import * as React from 'react';
import { SxProps } from '@mui/system';
import { CreateSlotsAndSlotProps, SlotProps } from '../utils/types';
import { Theme } from '../styles';
import {
ButtonBaseProps,
ButtonBaseTypeMap,
ExtendButtonBase,
ExtendButtonBaseTypeMap,
} from '../ButtonBase';
import { OverrideProps } from '../OverridableComponent';
import { BottomNavigationActionClasses } from './bottomNavigationActionClasses';
export interface BottomNavigationActionSlots {
/**
* The component that renders the root.
* @default ButtonBase
*/
root: React.ElementType;
/**
* The component that renders the label.
* @default span
*/
label: React.ElementType;
}
export type BottomNavigationActionSlotsAndSlotProps = CreateSlotsAndSlotProps<
BottomNavigationActionSlots,
{
/**
* Props forwarded to the root slot.
* By default, the available props are based on the ButtonBase element.
*/
root: SlotProps<React.ElementType<ButtonBaseProps>, {}, BottomNavigationActionOwnerState>;
/**
* Props forwarded to the label slot.
* By default, the available props are based on the span element.
*/
label: SlotProps<'span', {}, BottomNavigationActionOwnerState>;
}
>;
export interface BottomNavigationActionOwnProps extends BottomNavigationActionSlotsAndSlotProps {
/**
* This prop isn't supported.
* Use the `component` prop if you need to change the children structure.
*/
children?: React.ReactNode;
/**
* Override or extend the styles applied to the component.
*/
classes?: Partial<BottomNavigationActionClasses>;
/**
* The icon to display.
*/
icon?: React.ReactNode;
/**
* The label element.
*/
label?: React.ReactNode;
/**
* If `true`, the `BottomNavigationAction` will show its label.
* By default, only the selected `BottomNavigationAction`
* inside `BottomNavigation` will show its label.
*
* The prop defaults to the value (`false`) inherited from the parent BottomNavigation component.
*/
showLabel?: boolean;
/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx?: SxProps<Theme>;
/**
* You can provide your own value. Otherwise, we fallback to the child position index.
*/
value?: any;
}
export type BottomNavigationActionTypeMap<
AdditionalProps,
RootComponent extends React.ElementType,
> = ExtendButtonBaseTypeMap<{
props: AdditionalProps & BottomNavigationActionOwnProps;
defaultComponent: RootComponent;
}>;
/**
*
* Demos:
*
* - [Bottom Navigation](https://mui.com/material-ui/react-bottom-navigation/)
*
* API:
*
* - [BottomNavigationAction API](https://mui.com/material-ui/api/bottom-navigation-action/)
* - inherits [ButtonBase API](https://mui.com/material-ui/api/button-base/)
*/
declare const BottomNavigationAction: ExtendButtonBase<
BottomNavigationActionTypeMap<{}, ButtonBaseTypeMap['defaultComponent']>
>;
export type BottomNavigationActionProps<
RootComponent extends React.ElementType = ButtonBaseTypeMap['defaultComponent'],
AdditionalProps = {},
> = OverrideProps<BottomNavigationActionTypeMap<AdditionalProps, RootComponent>, RootComponent> & {
component?: React.ElementType;
};
export interface BottomNavigationActionOwnerState
extends Omit<BottomNavigationActionProps, 'slots' | 'slotProps'> {}
export default BottomNavigationAction;

View File

@@ -0,0 +1,235 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import composeClasses from '@mui/utils/composeClasses';
import { styled } from '../zero-styled';
import memoTheme from '../utils/memoTheme';
import { useDefaultProps } from '../DefaultPropsProvider';
import ButtonBase from '../ButtonBase';
import unsupportedProp from '../utils/unsupportedProp';
import bottomNavigationActionClasses, {
getBottomNavigationActionUtilityClass,
} from './bottomNavigationActionClasses';
import useSlot from '../utils/useSlot';
const useUtilityClasses = (ownerState) => {
const { classes, showLabel, selected } = ownerState;
const slots = {
root: ['root', !showLabel && !selected && 'iconOnly', selected && 'selected'],
label: ['label', !showLabel && !selected && 'iconOnly', selected && 'selected'],
};
return composeClasses(slots, getBottomNavigationActionUtilityClass, classes);
};
const BottomNavigationActionRoot = styled(ButtonBase, {
name: 'MuiBottomNavigationAction',
slot: 'Root',
overridesResolver: (props, styles) => {
const { ownerState } = props;
return [styles.root, !ownerState.showLabel && !ownerState.selected && styles.iconOnly];
},
})(
memoTheme(({ theme }) => ({
transition: theme.transitions.create(['color', 'padding-top'], {
duration: theme.transitions.duration.short,
}),
padding: '0px 12px',
minWidth: 80,
maxWidth: 168,
color: (theme.vars || theme).palette.text.secondary,
flexDirection: 'column',
flex: '1',
[`&.${bottomNavigationActionClasses.selected}`]: {
color: (theme.vars || theme).palette.primary.main,
},
variants: [
{
props: ({ showLabel, selected }) => !showLabel && !selected,
style: {
paddingTop: 14,
},
},
{
props: ({ showLabel, selected, label }) => !showLabel && !selected && !label,
style: {
paddingTop: 0,
},
},
],
})),
);
const BottomNavigationActionLabel = styled('span', {
name: 'MuiBottomNavigationAction',
slot: 'Label',
})(
memoTheme(({ theme }) => ({
fontFamily: theme.typography.fontFamily,
fontSize: theme.typography.pxToRem(12),
opacity: 1,
transition: 'font-size 0.2s, opacity 0.2s',
transitionDelay: '0.1s',
[`&.${bottomNavigationActionClasses.selected}`]: {
fontSize: theme.typography.pxToRem(14),
},
variants: [
{
props: ({ showLabel, selected }) => !showLabel && !selected,
style: {
opacity: 0,
transitionDelay: '0s',
},
},
],
})),
);
const BottomNavigationAction = React.forwardRef(function BottomNavigationAction(inProps, ref) {
const props = useDefaultProps({ props: inProps, name: 'MuiBottomNavigationAction' });
const {
className,
icon,
label,
onChange,
onClick,
// eslint-disable-next-line react/prop-types -- private, always overridden by BottomNavigation
selected,
showLabel,
value,
slots = {},
slotProps = {},
...other
} = props;
const ownerState = props;
const classes = useUtilityClasses(ownerState);
const handleChange = (event) => {
if (onChange) {
onChange(event, value);
}
if (onClick) {
onClick(event);
}
};
const externalForwardedProps = {
slots,
slotProps,
};
const [RootSlot, rootProps] = useSlot('root', {
elementType: BottomNavigationActionRoot,
externalForwardedProps: {
...externalForwardedProps,
...other,
},
shouldForwardComponentProp: true,
ownerState,
ref,
className: clsx(classes.root, className),
additionalProps: {
focusRipple: true,
},
getSlotProps: (handlers) => ({
...handlers,
onClick: (event) => {
handlers.onClick?.(event);
handleChange(event);
},
}),
});
const [LabelSlot, labelProps] = useSlot('label', {
elementType: BottomNavigationActionLabel,
externalForwardedProps,
ownerState,
className: classes.label,
});
return (
<RootSlot {...rootProps}>
{icon}
<LabelSlot {...labelProps}>{label}</LabelSlot>
</RootSlot>
);
});
BottomNavigationAction.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`. │
// └─────────────────────────────────────────────────────────────────────┘
/**
* This prop isn't supported.
* Use the `component` prop if you need to change the children structure.
*/
children: unsupportedProp,
/**
* Override or extend the styles applied to the component.
*/
classes: PropTypes.object,
/**
* @ignore
*/
className: PropTypes.string,
/**
* The icon to display.
*/
icon: PropTypes.node,
/**
* The label element.
*/
label: PropTypes.node,
/**
* @ignore
*/
onChange: PropTypes.func,
/**
* @ignore
*/
onClick: PropTypes.func,
/**
* If `true`, the `BottomNavigationAction` will show its label.
* By default, only the selected `BottomNavigationAction`
* inside `BottomNavigation` will show its label.
*
* The prop defaults to the value (`false`) inherited from the parent BottomNavigation component.
*/
showLabel: 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.
*/
sx: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])),
PropTypes.func,
PropTypes.object,
]),
/**
* You can provide your own value. Otherwise, we fallback to the child position index.
*/
value: PropTypes.any,
};
export default BottomNavigationAction;

View File

@@ -0,0 +1,90 @@
import * as React from 'react';
import { expect } from 'chai';
import { spy } from 'sinon';
import { createRenderer, within, screen } from '@mui/internal-test-utils';
import BottomNavigationAction, {
bottomNavigationActionClasses as classes,
} from '@mui/material/BottomNavigationAction';
import ButtonBase from '@mui/material/ButtonBase';
import describeConformance from '../../test/describeConformance';
const CustomButtonBase = React.forwardRef(({ focusRipple, ...props }, ref) => (
<ButtonBase ref={ref} {...props} />
));
describe('<BottomNavigationAction />', () => {
const { render } = createRenderer();
describeConformance(<BottomNavigationAction />, () => ({
classes,
inheritComponent: ButtonBase,
render,
muiName: 'MuiBottomNavigationAction',
refInstanceof: window.HTMLButtonElement,
testVariantProps: { showLabel: true },
testDeepOverrides: { slotName: 'label', slotClassName: classes.label },
skip: ['componentProp', 'componentsProp'],
slots: {
root: {
expectedClassName: classes.root,
testWithElement: CustomButtonBase,
},
label: {
expectedClassName: classes.label,
},
},
}));
it('adds a `selected` class when selected', () => {
render(<BottomNavigationAction selected />);
expect(screen.getByRole('button')).to.have.class(classes.selected);
});
it('should render label with the selected class when selected', () => {
const { container } = render(<BottomNavigationAction selected />);
expect(container.querySelector(`.${classes.label}`)).to.have.class(classes.selected);
});
it('adds a `iconOnly` class by default', () => {
render(<BottomNavigationAction />);
expect(screen.getByRole('button')).to.have.class(classes.iconOnly);
});
it('should render label with the `iconOnly` class', () => {
const { container } = render(<BottomNavigationAction />);
expect(container.querySelector(`.${classes.label}`)).to.have.class(classes.iconOnly);
});
it('removes the `iconOnly` class when `selected`', () => {
render(<BottomNavigationAction selected />);
expect(screen.getByRole('button')).not.to.have.class(classes.iconOnly);
});
it('removes the `iconOnly` class when `showLabel`', () => {
render(<BottomNavigationAction showLabel />);
expect(screen.getByRole('button')).not.to.have.class(classes.iconOnly);
});
it('should render the passed `icon`', () => {
render(<BottomNavigationAction icon={<div data-testid="icon" />} />);
expect(within(screen.getByRole('button')).getByTestId('icon')).not.to.equal(null);
});
describe('prop: onClick', () => {
it('should be called when a click is triggered', () => {
const handleClick = spy();
render(<BottomNavigationAction onClick={handleClick} />);
screen.getByRole('button').click();
expect(handleClick.callCount).to.equal(1);
});
});
});

View File

@@ -0,0 +1,26 @@
import generateUtilityClasses from '@mui/utils/generateUtilityClasses';
import generateUtilityClass from '@mui/utils/generateUtilityClass';
export interface BottomNavigationActionClasses {
/** Styles applied to the root element. */
root: string;
/** State class applied to the root element if selected. */
selected: string;
/** State class applied to the root element if `showLabel={false}` and not selected. */
iconOnly: string;
/** Styles applied to the label's span element. */
label: string;
}
export type BottomNavigationActionClassKey = keyof BottomNavigationActionClasses;
export function getBottomNavigationActionUtilityClass(slot: string): string {
return generateUtilityClass('MuiBottomNavigationAction', slot);
}
const bottomNavigationActionClasses: BottomNavigationActionClasses = generateUtilityClasses(
'MuiBottomNavigationAction',
['root', 'iconOnly', 'selected', 'label'],
);
export default bottomNavigationActionClasses;

View File

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

View File

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

View File

@@ -0,0 +1,25 @@
import { BoxTypeMap } from '@mui/system';
import { OverridableComponent } from '@mui/types';
import { OverrideProps } from '../OverridableComponent';
import { Theme as MaterialTheme } from '../styles';
/**
*
* Demos:
*
* - [Box](https://mui.com/material-ui/react-box/)
*
* API:
*
* - [Box API](https://mui.com/material-ui/api/box/)
*/
declare const Box: OverridableComponent<BoxTypeMap<{}, 'div', MaterialTheme>>;
export type BoxProps<
RootComponent extends React.ElementType = BoxTypeMap['defaultComponent'],
AdditionalProps = {},
> = OverrideProps<BoxTypeMap<AdditionalProps, RootComponent, MaterialTheme>, RootComponent> & {
component?: React.ElementType;
};
export default Box;

View File

@@ -0,0 +1,42 @@
'use client';
import { createBox } from '@mui/system';
import PropTypes from 'prop-types';
import { unstable_ClassNameGenerator as ClassNameGenerator } from '../className';
import { createTheme } from '../styles';
import THEME_ID from '../styles/identifier';
import boxClasses from './boxClasses';
const defaultTheme = createTheme();
const Box = createBox({
themeId: THEME_ID,
defaultTheme,
defaultClassName: boxClasses.root,
generateClassName: ClassNameGenerator.generate,
});
Box.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`. │
// └─────────────────────────────────────────────────────────────────────┘
/**
* @ignore
*/
children: PropTypes.node,
/**
* 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,
]),
};
export default Box;

View File

@@ -0,0 +1,47 @@
import { Box as SystemBox, BoxProps as SystemBoxProps, createBox } from '@mui/system';
import { expectType } from '@mui/types';
import Box, { BoxProps as MaterialBoxProps } from '@mui/material/Box';
import { createTheme } from '@mui/material/styles';
function ThemeValuesCanBeSpread() {
<Box
sx={(theme) => ({
...theme.typography.body1,
color: theme.palette.primary.main,
})}
/>;
<Box
sx={(theme) => ({
...theme.mixins.toolbar,
color: theme.palette.primary.main,
})}
/>;
<Box
sx={(theme) => ({
...theme.mixins.toolbar,
color: 'primary.main',
})}
/>;
}
// Compatibility with Material UI's Box
const defaultTheme = createTheme({});
const CustomBox = createBox({ defaultTheme });
expectType<typeof Box, typeof CustomBox>(CustomBox);
// @ts-expect-error System's Box has different type than Material UI's Box
expectType<typeof SystemBox, typeof CustomBox>(CustomBox);
function ColorTest() {
<Box
color={(theme) => theme.vars.palette.common.black}
sx={(theme) => ({ backgroundColor: theme.vars.palette.background.default })}
/>;
}
function ComponentTest() {
return <span />;
}
expectType<SystemBoxProps['component'], MaterialBoxProps['component']>('span');
expectType<SystemBoxProps['component'], MaterialBoxProps['component']>(ComponentTest);

View File

@@ -0,0 +1,63 @@
import { expect } from 'chai';
import { createRenderer, isJsdom } from '@mui/internal-test-utils';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import Box from '@mui/material/Box';
import { unstable_ClassNameGenerator as ClassNameGenerator } from '@mui/material/className';
import describeConformance from '../../test/describeConformance';
const isJSDOM = isJsdom();
describe('<Box />', () => {
const { render } = createRenderer();
describeConformance(<Box />, () => ({
render,
inheritComponent: 'div',
skip: [
'componentProp',
'componentsProp',
'rootClass',
'themeVariants',
'themeStyleOverrides',
'themeDefaultProps',
],
refInstanceof: window.HTMLDivElement,
}));
it.skipIf(isJSDOM)('respects theme from context', function test() {
const theme = createTheme({
palette: {
primary: {
main: 'rgb(255, 0, 0)',
},
},
});
const { container } = render(
<ThemeProvider theme={theme}>
<Box color="primary.main" />
</ThemeProvider>,
);
expect(container.firstChild).toHaveComputedStyle({
color: 'rgb(255, 0, 0)',
});
});
describe('ClassNameGenerator', () => {
afterEach(() => {
ClassNameGenerator.reset();
});
it('get custom className', () => {
const { container, rerender } = render(<Box />);
expect(container.firstChild).to.have.class('MuiBox-root');
ClassNameGenerator.configure((name) => name.replace('Mui', 'Company'));
rerender(<Box />);
expect(container.firstChild).to.have.class('CompanyBox-root');
});
});
});

Some files were not shown because too many files have changed in this diff Show More