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,56 @@
<!-- #host-reference -->
<!-- markdownlint-disable-next-line -->
<p align="center">
<a href="https://mui.com/material-ui/" rel="noopener" target="_blank"><img width="150" height="133" src="https://mui.com/static/logo.svg" alt="Material UI logo"></a>
</p>
<h1 align="center">Material UI</h1>
Material UI is an open-source React component library that implements Google's [Material Design](https://m2.material.io/design/introduction/). It's comprehensive and can be used in production out of the box.
## Installation
Install the package in your project directory with:
<!-- #npm-tag-reference -->
```bash
npm install @mui/material @emotion/react @emotion/styled
```
## Documentation
Visit [https://mui.com/material-ui/](https://mui.com/material-ui/) to view the full documentation.
## Questions
For how-to questions that don't involve making changes to the code base, please use [Stack Overflow](https://stackoverflow.com/questions/tagged/material-ui) instead of GitHub issues.
Use the "material-ui" tag on Stack Overflow to make it easier for the community to find your question.
## Examples
Our documentation features [a collection of example projects using Material UI](https://mui.com/material-ui/getting-started/example-projects/).
## Contributing
Read the [contributing guide](/CONTRIBUTING.md) to learn about our development process, how to propose bug fixes and improvements, and how to build and test your changes.
Contributing to Material UI is about more than just issues and pull requests!
There are many other ways to [support Material UI](https://mui.com/material-ui/getting-started/faq/#mui-is-awesome-how-can-i-support-the-project) beyond contributing to the code base.
## Changelog
The [changelog](https://github.com/mui/material-ui/releases) is regularly updated to reflect what's changed in each new release.
## Roadmap
Future plans and high-priority features and enhancements can be found in the [roadmap](https://mui.com/material-ui/discover-more/roadmap/).
## License
This project is licensed under the terms of the
[MIT license](/LICENSE).
## Security
For details of supported versions and contact details for reporting security issues, please refer to the [security policy](https://github.com/mui/material-ui/security/policy).

View File

@@ -0,0 +1,113 @@
{
"name": "@mui/material",
"version": "7.3.6",
"author": "MUI Team",
"description": "Material UI is an open-source React component library that implements Google's Material Design. It's comprehensive and can be used in production out of the box.",
"keywords": [
"react",
"react-component",
"mui",
"material-ui",
"material design"
],
"repository": {
"type": "git",
"url": "git+https://github.com/mui/material-ui.git",
"directory": "packages/mui-material"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/mui/material-ui/issues"
},
"homepage": "https://mui.com/material-ui/",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"scripts": {
"build": "code-infra build",
"release": "pnpm build && pnpm publish",
"test": "pnpm --workspace-root test:unit --project \"*:@mui/material\"",
"typescript": "tsc -p tsconfig.json",
"typescript:module-augmentation": "node scripts/testModuleAugmentation.js",
"attw": "attw --pack ./build --exclude-entrypoints esm modern --include-entrypoints Button styles"
},
"dependencies": {
"@babel/runtime": "^7.28.4",
"@mui/core-downloads-tracker": "workspace:^",
"@mui/system": "workspace:^",
"@mui/types": "workspace:^",
"@mui/utils": "workspace:^",
"@popperjs/core": "^2.11.8",
"@types/react-transition-group": "^4.4.12",
"clsx": "^2.1.1",
"csstype": "^3.2.3",
"prop-types": "^15.8.1",
"react-is": "^19.2.1",
"react-transition-group": "^4.4.5"
},
"devDependencies": {
"@mui/internal-test-utils": "workspace:^",
"@types/chai": "^5.2.3",
"@types/prop-types": "^15.7.15",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@types/sinon": "^17.0.4",
"chai": "^6.0.1",
"css-mediaquery": "^0.1.2",
"es-toolkit": "^1.39.10",
"fast-glob": "^3.3.3",
"jsdom": "^26.1.0",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-router": "^7.9.6",
"sinon": "^21.0.0"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@mui/material-pigment-css": "workspace:^",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@mui/material-pigment-css": {
"optional": true
}
},
"sideEffects": false,
"publishConfig": {
"access": "public",
"directory": "build"
},
"engines": {
"node": ">=14.0.0"
},
"exports": {
".": "./src/index.js",
"./ButtonBase/TouchRipple": "./src/ButtonBase/TouchRipple.js",
"./*": "./src/*/index.ts"
},
"pigment-css": {
"vite": {
"include": [
"prop-types",
"react-is",
"hoist-non-react-statics",
"react",
"react-dom",
"@emotion/react"
]
}
}
}

View File

@@ -0,0 +1,61 @@
const childProcess = require('child_process');
const path = require('path');
const { promisify } = require('util');
const { chunk } = require('es-toolkit/array');
const glob = require('fast-glob');
const exec = promisify(childProcess.exec);
const packageRoot = path.resolve(__dirname, '../');
async function test(tsconfigPath) {
try {
await exec(['pnpm', 'tsc', '--project', tsconfigPath].join(' '), { cwd: packageRoot });
} catch (error) {
if (error.stdout !== undefined) {
// `exec` error
throw new Error(`exit code ${error.code}: ${error.stdout}`);
}
// Unknown error
throw error;
}
}
/**
* Tests various module augmentation scenarios.
* We can't run them with a single `tsc` run since these apply globally.
* Running them all would mean they're not isolated.
* Each test case represents a section in our docs.
*/
async function main() {
const tsconfigPaths = await glob('test/typescript/moduleAugmentation/*.tsconfig.json', {
absolute: true,
cwd: packageRoot,
});
// Need to process in chunks or we might run out-of-memory
// approximate pnpm lerna --concurrency 7
const tsconfigPathsChunks = chunk(tsconfigPaths, 7);
for await (const tsconfigPathsChunk of tsconfigPathsChunks) {
await Promise.all(
tsconfigPathsChunk.map(async (tsconfigPath) => {
await test(tsconfigPath).then(
() => {
// eslint-disable-next-line no-console -- test runner feedback
console.log(`PASS ${path.relative(process.cwd(), tsconfigPath)}`);
},
(error) => {
// don't bail but log the error
console.error(`FAIL ${path.relative(process.cwd(), tsconfigPath)}\n ${error}`);
// and mark the test as failed
process.exitCode = 1;
},
);
}),
);
}
}
main().catch((error) => {
console.error(error);
process.exit(1);
});

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;

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