diff --git a/docs/data/system/components/container/FixedContainer.js b/docs/data/system/components/container/FixedContainer.js new file mode 100644 index 00000000000000..5a97b16514d338 --- /dev/null +++ b/docs/data/system/components/container/FixedContainer.js @@ -0,0 +1,14 @@ +import * as React from 'react'; +import CssBaseline from '@mui/material/CssBaseline'; +import { Box, Container } from '@mui/system'; + +export default function FixedContainer() { + return ( + + + + + + + ); +} diff --git a/docs/data/system/components/container/FixedContainer.tsx b/docs/data/system/components/container/FixedContainer.tsx new file mode 100644 index 00000000000000..5a97b16514d338 --- /dev/null +++ b/docs/data/system/components/container/FixedContainer.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import CssBaseline from '@mui/material/CssBaseline'; +import { Box, Container } from '@mui/system'; + +export default function FixedContainer() { + return ( + + + + + + + ); +} diff --git a/docs/data/system/components/container/FixedContainer.tsx.preview b/docs/data/system/components/container/FixedContainer.tsx.preview new file mode 100644 index 00000000000000..9260778041e81c --- /dev/null +++ b/docs/data/system/components/container/FixedContainer.tsx.preview @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/docs/data/system/components/container/SimpleContainer.js b/docs/data/system/components/container/SimpleContainer.js new file mode 100644 index 00000000000000..22ce2c2f76b8e3 --- /dev/null +++ b/docs/data/system/components/container/SimpleContainer.js @@ -0,0 +1,14 @@ +import * as React from 'react'; +import CssBaseline from '@mui/material/CssBaseline'; +import { Box, Container } from '@mui/system'; + +export default function SimpleContainer() { + return ( + + + + + + + ); +} diff --git a/docs/data/system/components/container/SimpleContainer.tsx b/docs/data/system/components/container/SimpleContainer.tsx new file mode 100644 index 00000000000000..22ce2c2f76b8e3 --- /dev/null +++ b/docs/data/system/components/container/SimpleContainer.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import CssBaseline from '@mui/material/CssBaseline'; +import { Box, Container } from '@mui/system'; + +export default function SimpleContainer() { + return ( + + + + + + + ); +} diff --git a/docs/data/system/components/container/SimpleContainer.tsx.preview b/docs/data/system/components/container/SimpleContainer.tsx.preview new file mode 100644 index 00000000000000..353572318add8f --- /dev/null +++ b/docs/data/system/components/container/SimpleContainer.tsx.preview @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/docs/data/system/components/container/container.md b/docs/data/system/components/container/container.md new file mode 100644 index 00000000000000..f50a16a1ccbb7f --- /dev/null +++ b/docs/data/system/components/container/container.md @@ -0,0 +1,35 @@ +--- +product: system +title: React Container component +components: Container +githubLabel: 'component: Container' +--- + +# Container + +

The container centers your content horizontally. It's the most basic layout element.

+ +While containers can be nested, most layouts do not require a nested container. + +{{"component": "modules/components/ComponentLinkHeader.js", "design": false}} + +## Fluid + +A fluid container width is bounded by the `maxWidth` prop value. + +{{"demo": "SimpleContainer.js", "iframe": true, "defaultCodeOpen": false}} + +```jsx + +``` + +## Fixed + +If you prefer to design for a fixed set of sizes instead of trying to accommodate a fully fluid viewport, you can set the `fixed` prop. +The max-width matches the min-width of the current breakpoint. + +{{"demo": "FixedContainer.js", "iframe": true, "defaultCodeOpen": false}} + +```jsx + +``` diff --git a/docs/data/system/pages.ts b/docs/data/system/pages.ts index cc7340941ede6c..eff454fbaf6d7e 100644 --- a/docs/data/system/pages.ts +++ b/docs/data/system/pages.ts @@ -27,7 +27,10 @@ const pages = [ pathname: '/system/react-', title: 'Components', icon: 'ToggleOnIcon', - children: [{ pathname: '/system/react-box', title: 'Box' }], + children: [ + { pathname: '/system/react-box', title: 'Box' }, + { pathname: '/system/react-container', title: 'Container' }, + ], }, { title: 'Component API', diff --git a/docs/data/system/pagesApi.js b/docs/data/system/pagesApi.js index 3888fe447a36c0..c90736dcfa7829 100644 --- a/docs/data/system/pagesApi.js +++ b/docs/data/system/pagesApi.js @@ -1 +1,4 @@ -module.exports = [{ pathname: '/system/api/box' }]; +module.exports = [ + { pathname: '/system/api/box' }, + { pathname: '/system/api/container' }, +]; diff --git a/docs/pages/material-ui/api/container.json b/docs/pages/material-ui/api/container.json index a0e7c832b8c1ca..170294dcca3495 100644 --- a/docs/pages/material-ui/api/container.json +++ b/docs/pages/material-ui/api/container.json @@ -8,8 +8,7 @@ "type": { "name": "union", "description": "'xs'
| 'sm'
| 'md'
| 'lg'
| 'xl'
| false
| string" - }, - "default": "'lg'" + } }, "sx": { "type": { diff --git a/docs/pages/system/api/container.js b/docs/pages/system/api/container.js new file mode 100644 index 00000000000000..93a80589d308f5 --- /dev/null +++ b/docs/pages/system/api/container.js @@ -0,0 +1,19 @@ +import * as React from 'react'; +import ApiPage from 'docs/src/modules/components/ApiPage'; +import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import jsonPageContent from './container.json'; + +export default function Page(props) { + const { descriptions, pageContent } = props; + return ; +} + +Page.getInitialProps = () => { + const req = require.context('docs/translations/api-docs/container', false, /container.*.json$/); + const descriptions = mapApiPageTranslations(req); + + return { + descriptions, + pageContent: jsonPageContent, + }; +}; diff --git a/docs/pages/system/api/container.json b/docs/pages/system/api/container.json new file mode 100644 index 00000000000000..b48886a4575a1f --- /dev/null +++ b/docs/pages/system/api/container.json @@ -0,0 +1,41 @@ +{ + "props": { + "classes": { "type": { "name": "object" } }, + "component": { "type": { "name": "elementType" } }, + "disableGutters": { "type": { "name": "bool" } }, + "fixed": { "type": { "name": "bool" } }, + "maxWidth": { + "type": { + "name": "union", + "description": "'xs'
| 'sm'
| 'md'
| 'lg'
| 'xl'
| false
| string" + } + }, + "sx": { + "type": { + "name": "union", + "description": "Array<func
| object
| bool>
| func
| object" + } + } + }, + "name": "Container", + "styles": { + "classes": [ + "root", + "disableGutters", + "fixed", + "maxWidthXs", + "maxWidthSm", + "maxWidthMd", + "maxWidthLg", + "maxWidthXl" + ], + "globalClasses": {}, + "name": "MuiContainer" + }, + "spread": true, + "forwardsRefTo": "HTMLElement", + "filename": "/packages/mui-system/src/Container/Container.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/system/react-container.js b/docs/pages/system/react-container.js new file mode 100644 index 00000000000000..3ea49f7cd267c8 --- /dev/null +++ b/docs/pages/system/react-container.js @@ -0,0 +1,11 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; +import { + demos, + docs, + demoComponents, +} from 'docs/data/system/components/container/container.md?@mui/markdown'; + +export default function Page() { + return ; +} diff --git a/docs/scripts/ApiBuilders/ComponentApiBuilder.ts b/docs/scripts/ApiBuilders/ComponentApiBuilder.ts index 041385563fa173..fc6f4c2a5c9303 100644 --- a/docs/scripts/ApiBuilders/ComponentApiBuilder.ts +++ b/docs/scripts/ApiBuilders/ComponentApiBuilder.ts @@ -498,7 +498,7 @@ const attachPropsTable = (reactApi: ReactApi) => { reactApi.propsTable = componentProps; }; -const systemComponents = ['Box']; +const systemComponents = ['Container', 'Box']; /** * - Build react component (specified filename) api by lookup at its definition (.d.ts or ts) diff --git a/docs/translations/translations.json b/docs/translations/translations.json index c8abd3daa57707..a5fed123a67a49 100644 --- a/docs/translations/translations.json +++ b/docs/translations/translations.json @@ -178,6 +178,7 @@ "/system/styled": "styled", "/system/react-": "Components", "/system/react-box": "Box", + "/system/react-container": "Container", "/system/styles": "Styles", "/system/styles/basics": "Basics", "/system/styles/advanced": "Advanced", diff --git a/packages/eslint-plugin-material-ui/src/rules/mui-name-matches-component-name.js b/packages/eslint-plugin-material-ui/src/rules/mui-name-matches-component-name.js index ccf39df768b1e8..721d51f0ff4eca 100644 --- a/packages/eslint-plugin-material-ui/src/rules/mui-name-matches-component-name.js +++ b/packages/eslint-plugin-material-ui/src/rules/mui-name-matches-component-name.js @@ -18,6 +18,9 @@ const rule = { const { customHooks = [] } = options; function resolveUseThemePropsNameLiteral(node) { + if (!node.arguments[0].properties) { + return null; + } const nameProperty = node.arguments[0].properties.find( (property) => property.key.name === 'name', ); diff --git a/packages/mui-joy/src/Box/Box.tsx b/packages/mui-joy/src/Box/Box.tsx index 984271702024bb..468a046a5a977c 100644 --- a/packages/mui-joy/src/Box/Box.tsx +++ b/packages/mui-joy/src/Box/Box.tsx @@ -1,5 +1,5 @@ -import PropTypes from 'prop-types'; import { createBox } from '@mui/system'; +import PropTypes from 'prop-types'; import { OverridableComponent } from '@mui/types'; import { unstable_ClassNameGenerator as ClassNameGenerator } from '../className'; import { BoxTypeMap } from './BoxProps'; diff --git a/packages/mui-joy/src/Container/Container.test.js b/packages/mui-joy/src/Container/Container.test.js new file mode 100644 index 00000000000000..480e74ba57d1bf --- /dev/null +++ b/packages/mui-joy/src/Container/Container.test.js @@ -0,0 +1,33 @@ +import * as React from 'react'; +import { expect } from 'chai'; +import { describeConformance, createRenderer } from 'test/utils'; +import Container, { containerClasses as classes } from '@mui/joy/Container'; + +describe('', () => { + const { render } = createRenderer(); + + const defaultProps = { + children:
, + }; + + describeConformance(, () => ({ + classes, + inheritComponent: 'div', + render, + refInstanceof: window.HTMLElement, + muiName: 'MuiContainer', + skip: ['componentsProp'], + testVariantProps: { fixed: true }, + })); + + describe('prop: maxWidth', () => { + it('should support different maxWidth values', () => { + const { container: firstContainer } = render(); + expect(firstContainer.firstChild).to.have.class(classes.maxWidthLg); + const { container: secondsContainre } = render( + , + ); + expect(secondsContainre.firstChild).not.to.have.class(classes.maxWidthLg); + }); + }); +}); diff --git a/packages/mui-joy/src/Container/Container.tsx b/packages/mui-joy/src/Container/Container.tsx new file mode 100644 index 00000000000000..1aeea54ebdfd31 --- /dev/null +++ b/packages/mui-joy/src/Container/Container.tsx @@ -0,0 +1,66 @@ +/* eslint-disable material-ui/mui-name-matches-component-name */ +import { createContainer } from '@mui/system'; +import PropTypes from 'prop-types'; +import { OverridableComponent } from '@mui/types'; +import { ContainerTypeMap } from './ContainerProps'; +import { Theme } from '../styles/types/theme'; +import styled from '../styles/styled'; +import { useThemeProps } from '../styles'; + +const Container = createContainer({ + createStyledComponent: styled('div', { + name: 'MuiContainer', + slot: 'Root', + overridesResolver: (props, styles) => styles.root, + }), + useThemeProps: (inProps) => useThemeProps({ props: inProps, name: 'MuiContainer' }), +}) as OverridableComponent; + +Container.propTypes /* remove-proptypes */ = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * @ignore + */ + children: PropTypes.node, + /** + * The component used for the root node. + * Either a string to use a HTML element or a component. + */ + component: PropTypes.elementType, + /** + * If `true`, the left and right padding is removed. + * @default false + */ + disableGutters: PropTypes.bool, + /** + * Set the max-width to match the min-width of the current breakpoint. + * This is useful if you'd prefer to design for a fixed set of sizes + * instead of trying to accommodate a fully fluid viewport. + * It's fluid by default. + * @default false + */ + fixed: PropTypes.bool, + /** + * Determine the max-width of the container. + * The container width grows with the size of the screen. + * Set to `false` to disable `maxWidth`. + * @default 'lg' + */ + maxWidth: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([ + PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl', false]), + 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, + ]), +} as any; + +export default Container; diff --git a/packages/mui-joy/src/Container/ContainerProps.ts b/packages/mui-joy/src/Container/ContainerProps.ts new file mode 100644 index 00000000000000..5ab8b439bdeb57 --- /dev/null +++ b/packages/mui-joy/src/Container/ContainerProps.ts @@ -0,0 +1,40 @@ +import * as React from 'react'; +import { OverrideProps } from '@mui/types'; +import { Breakpoint } from '@mui/system'; +import { SxProps } from '../styles/types'; + +export interface ContainerTypeMap

{ + props: P & { + children?: React.ReactNode; + /** + * If `true`, the left and right padding is removed. + * @default false + */ + disableGutters?: boolean; + /** + * Set the max-width to match the min-width of the current breakpoint. + * This is useful if you'd prefer to design for a fixed set of sizes + * instead of trying to accommodate a fully fluid viewport. + * It's fluid by default. + * @default false + */ + fixed?: boolean; + /** + * Determine the max-width of the container. + * The container width grows with the size of the screen. + * Set to `false` to disable `maxWidth`. + * @default 'lg' + */ + maxWidth?: Breakpoint | false; + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx?: SxProps; + }; + defaultComponent: D; +} + +export type ContainerProps< + D extends React.ElementType = ContainerTypeMap['defaultComponent'], + P = {}, +> = OverrideProps, D>; diff --git a/packages/mui-joy/src/Container/containerClasses.ts b/packages/mui-joy/src/Container/containerClasses.ts new file mode 100644 index 00000000000000..dc8c11d99cb839 --- /dev/null +++ b/packages/mui-joy/src/Container/containerClasses.ts @@ -0,0 +1,22 @@ +import { generateUtilityClass, generateUtilityClasses } from '@mui/base'; +import { ContainerClasses } from '@mui/system'; + +export type { ContainerClassKey } from '@mui/system'; +export type { ContainerClasses }; + +export function getContainerUtilityClass(slot: string): string { + return generateUtilityClass('MuiContainer', slot); +} + +const containerClasses: ContainerClasses = generateUtilityClasses('MuiContainer', [ + 'root', + 'disableGutters', + 'fixed', + 'maxWidthXs', + 'maxWidthSm', + 'maxWidthMd', + 'maxWidthLg', + 'maxWidthXl', +]); + +export default containerClasses; diff --git a/packages/mui-joy/src/Container/index.ts b/packages/mui-joy/src/Container/index.ts new file mode 100644 index 00000000000000..40915571d90e2d --- /dev/null +++ b/packages/mui-joy/src/Container/index.ts @@ -0,0 +1,5 @@ +export { default } from './Container'; +export * from './ContainerProps'; + +export { default as containerClasses } from './containerClasses'; +export * from './containerClasses'; diff --git a/packages/mui-joy/src/index.test.js b/packages/mui-joy/src/index.test.js index 8e5d6da9947cd3..c40ceb0beb2f1a 100644 --- a/packages/mui-joy/src/index.test.js +++ b/packages/mui-joy/src/index.test.js @@ -13,6 +13,8 @@ describe('@mui/joy', () => { }); it('should not have undefined exports', () => { - Object.keys(joy).forEach((exportKey) => expect(Boolean(joy[exportKey])).to.equal(true)); + Object.keys(joy).forEach((exportKey) => + expect(`${exportKey}-${Boolean(joy[exportKey])}`).to.equal(`${exportKey}-true`), + ); }); }); diff --git a/packages/mui-joy/src/index.ts b/packages/mui-joy/src/index.ts index a894fbe124e60a..7f6350e532eb13 100644 --- a/packages/mui-joy/src/index.ts +++ b/packages/mui-joy/src/index.ts @@ -64,6 +64,11 @@ export * from './FormHelperText'; export { default as TextField } from './TextField'; export * from './TextField'; +export { default as Box } from './Box'; +export * from './Box'; + +export { default as Container } from './Container'; +export * from './Container'; export { default as Badge } from './Badge'; export * from './Badge'; diff --git a/packages/mui-material/src/Box/Box.js b/packages/mui-material/src/Box/Box.js index 1d5dc41e76e9b7..9b0ff0e60c148d 100644 --- a/packages/mui-material/src/Box/Box.js +++ b/packages/mui-material/src/Box/Box.js @@ -1,5 +1,5 @@ -import PropTypes from 'prop-types'; import { createBox } from '@mui/system'; +import PropTypes from 'prop-types'; import { unstable_ClassNameGenerator as ClassNameGenerator } from '../className'; import { createTheme } from '../styles'; diff --git a/packages/mui-material/src/Container/Container.js b/packages/mui-material/src/Container/Container.js index a263b757a349a8..f2a213bbb42226 100644 --- a/packages/mui-material/src/Container/Container.js +++ b/packages/mui-material/src/Container/Container.js @@ -1,113 +1,26 @@ -import * as React from 'react'; +/* eslint-disable material-ui/mui-name-matches-component-name */ import PropTypes from 'prop-types'; -import clsx from 'clsx'; -import { unstable_composeClasses as composeClasses } from '@mui/base'; -import useThemeProps from '../styles/useThemeProps'; -import styled from '../styles/styled'; -import { getContainerUtilityClass } from './containerClasses'; +import { createContainer } from '@mui/system'; import capitalize from '../utils/capitalize'; +import styled from '../styles/styled'; +import useThemeProps from '../styles/useThemeProps'; -const useUtilityClasses = (ownerState) => { - const { classes, fixed, disableGutters, maxWidth } = ownerState; - - const slots = { - root: [ - 'root', - maxWidth && `maxWidth${capitalize(String(maxWidth))}`, - fixed && 'fixed', - disableGutters && 'disableGutters', - ], - }; - - return composeClasses(slots, getContainerUtilityClass, classes); -}; - -const ContainerRoot = styled('div', { - name: 'MuiContainer', - slot: 'Root', - overridesResolver: (props, styles) => { - const { ownerState } = props; +const Container = createContainer({ + createStyledComponent: styled('div', { + name: 'MuiContainer', + slot: 'Root', + overridesResolver: (props, styles) => { + const { ownerState } = props; - return [ - styles.root, - styles[`maxWidth${capitalize(String(ownerState.maxWidth))}`], - ownerState.fixed && styles.fixed, - ownerState.disableGutters && styles.disableGutters, - ]; - }, -})( - ({ theme, ownerState }) => ({ - width: '100%', - marginLeft: 'auto', - boxSizing: 'border-box', - marginRight: 'auto', - display: 'block', // Fix IE11 layout when used with main. - ...(!ownerState.disableGutters && { - paddingLeft: theme.spacing(2), - paddingRight: theme.spacing(2), - [theme.breakpoints.up('sm')]: { - paddingLeft: theme.spacing(3), - paddingRight: theme.spacing(3), - }, - }), + return [ + styles.root, + styles[`maxWidth${capitalize(String(ownerState.maxWidth))}`], + ownerState.fixed && styles.fixed, + ownerState.disableGutters && styles.disableGutters, + ]; + }, }), - ({ theme, ownerState }) => - ownerState.fixed && - Object.keys(theme.breakpoints.values).reduce((acc, breakpoint) => { - const value = theme.breakpoints.values[breakpoint]; - - if (value !== 0) { - acc[theme.breakpoints.up(breakpoint)] = { - maxWidth: `${value}${theme.breakpoints.unit}`, - }; - } - return acc; - }, {}), - ({ theme, ownerState }) => ({ - ...(ownerState.maxWidth === 'xs' && { - [theme.breakpoints.up('xs')]: { - maxWidth: Math.max(theme.breakpoints.values.xs, 444), - }, - }), - ...(ownerState.maxWidth && - ownerState.maxWidth !== 'xs' && { - [theme.breakpoints.up(ownerState.maxWidth)]: { - maxWidth: `${theme.breakpoints.values[ownerState.maxWidth]}${theme.breakpoints.unit}`, - }, - }), - }), -); - -const Container = React.forwardRef(function Container(inProps, ref) { - const props = useThemeProps({ props: inProps, name: 'MuiContainer' }); - const { - className, - component = 'div', - disableGutters = false, - fixed = false, - maxWidth = 'lg', - ...other - } = props; - - const ownerState = { - ...props, - component, - disableGutters, - fixed, - maxWidth, - }; - - const classes = useUtilityClasses(ownerState); - - return ( - - ); + useThemeProps: (inProps) => useThemeProps({ props: inProps, name: 'MuiContainer' }), }); Container.propTypes /* remove-proptypes */ = { @@ -123,10 +36,6 @@ Container.propTypes /* remove-proptypes */ = { * 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. diff --git a/packages/mui-material/src/Container/containerClasses.ts b/packages/mui-material/src/Container/containerClasses.ts index 4b4540faee1f1f..dc8c11d99cb839 100644 --- a/packages/mui-material/src/Container/containerClasses.ts +++ b/packages/mui-material/src/Container/containerClasses.ts @@ -1,25 +1,8 @@ import { generateUtilityClass, generateUtilityClasses } from '@mui/base'; +import { ContainerClasses } from '@mui/system'; -export interface ContainerClasses { - /** Styles applied to the root element. */ - root: string; - /** Styles applied to the root element if `disableGutters={true}`. */ - disableGutters: string; - /** Styles applied to the root element if `fixed={true}`. */ - fixed: string; - /** Styles applied to the root element if `maxWidth="xs"`. */ - maxWidthXs: string; - /** Styles applied to the root element if `maxWidth="sm"`. */ - maxWidthSm: string; - /** Styles applied to the root element if `maxWidth="md"`. */ - maxWidthMd: string; - /** Styles applied to the root element if `maxWidth="lg"`. */ - maxWidthLg: string; - /** Styles applied to the root element if `maxWidth="xl"`. */ - maxWidthXl: string; -} - -export type ContainerClassKey = keyof ContainerClasses; +export type { ContainerClassKey } from '@mui/system'; +export type { ContainerClasses }; export function getContainerUtilityClass(slot: string): string { return generateUtilityClass('MuiContainer', slot); diff --git a/packages/mui-system/src/Container/Container.test.js b/packages/mui-system/src/Container/Container.test.js new file mode 100644 index 00000000000000..acfd8f566abdec --- /dev/null +++ b/packages/mui-system/src/Container/Container.test.js @@ -0,0 +1,33 @@ +import * as React from 'react'; +import { expect } from 'chai'; +import { describeConformance, createRenderer } from 'test/utils'; +import { Container, containerClasses as classes } from '@mui/system'; + +describe('', () => { + const { render } = createRenderer(); + + const defaultProps = { + children:

, + }; + + describeConformance(, () => ({ + classes, + inheritComponent: 'div', + render, + refInstanceof: window.HTMLElement, + muiName: 'MuiContainer', + skip: ['componentsProp'], + testVariantProps: { fixed: true }, + })); + + describe('prop: maxWidth', () => { + it('should support different maxWidth values', () => { + const { container: firstContainer } = render(); + expect(firstContainer.firstChild).to.have.class(classes.maxWidthLg); + const { container: secondsContainre } = render( + , + ); + expect(secondsContainre.firstChild).not.to.have.class(classes.maxWidthLg); + }); + }); +}); diff --git a/packages/mui-system/src/Container/Container.tsx b/packages/mui-system/src/Container/Container.tsx new file mode 100644 index 00000000000000..800e5e5f296a55 --- /dev/null +++ b/packages/mui-system/src/Container/Container.tsx @@ -0,0 +1,68 @@ +import PropTypes from 'prop-types'; +import createContainer from './createContainer'; + +/** + * + * Demos: + * + * - [Container (Material UI)](https://mui.com/material-ui/react-container/) + * - [Container (MUI System)](https://mui.com/system/react-container/) + * + * API: + * + * - [Container API](https://mui.com/system/api/container/) + */ +const Container = createContainer(); + +Container.propTypes /* remove-proptypes */ = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * @ignore + */ + children: PropTypes.node, + /** + * Override or extend the styles applied to the component. + */ + classes: PropTypes.object, + /** + * The component used for the root node. + * Either a string to use a HTML element or a component. + */ + component: PropTypes.elementType, + /** + * If `true`, the left and right padding is removed. + * @default false + */ + disableGutters: PropTypes.bool, + /** + * Set the max-width to match the min-width of the current breakpoint. + * This is useful if you'd prefer to design for a fixed set of sizes + * instead of trying to accommodate a fully fluid viewport. + * It's fluid by default. + * @default false + */ + fixed: PropTypes.bool, + /** + * Determine the max-width of the container. + * The container width grows with the size of the screen. + * Set to `false` to disable `maxWidth`. + * @default 'lg' + */ + maxWidth: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([ + PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl', false]), + 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, + ]), +} as any; + +export default Container; diff --git a/packages/mui-system/src/Container/ContainerProps.ts b/packages/mui-system/src/Container/ContainerProps.ts new file mode 100644 index 00000000000000..ad62f4b66bf42c --- /dev/null +++ b/packages/mui-system/src/Container/ContainerProps.ts @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { OverrideProps } from '@mui/types'; +import { SxProps } from '../styleFunctionSx'; +import { Theme, Breakpoint } from '../createTheme'; +import { ContainerClasses } from './containerClasses'; + +export interface ContainerTypeMap

{ + props: P & { + children?: React.ReactNode; + /** + * Override or extend the styles applied to the component. + */ + classes?: Partial; + /** + * If `true`, the left and right padding is removed. + * @default false + */ + disableGutters?: boolean; + /** + * Set the max-width to match the min-width of the current breakpoint. + * This is useful if you'd prefer to design for a fixed set of sizes + * instead of trying to accommodate a fully fluid viewport. + * It's fluid by default. + * @default false + */ + fixed?: boolean; + /** + * Determine the max-width of the container. + * The container width grows with the size of the screen. + * Set to `false` to disable `maxWidth`. + * @default 'lg' + */ + maxWidth?: Breakpoint | false; + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx?: SxProps; + }; + defaultComponent: D; +} + +export type ContainerProps< + D extends React.ElementType = ContainerTypeMap['defaultComponent'], + P = {}, +> = OverrideProps, D>; diff --git a/packages/mui-system/src/Container/containerClasses.ts b/packages/mui-system/src/Container/containerClasses.ts new file mode 100644 index 00000000000000..6b10ea28770c22 --- /dev/null +++ b/packages/mui-system/src/Container/containerClasses.ts @@ -0,0 +1,39 @@ +import { generateUtilityClass, generateUtilityClasses } from '@mui/private-classnames'; + +export interface ContainerClasses { + /** Styles applied to the root element. */ + root: string; + /** Styles applied to the root element if `disableGutters={true}`. */ + disableGutters: string; + /** Styles applied to the root element if `fixed={true}`. */ + fixed: string; + /** Styles applied to the root element if `maxWidth="xs"`. */ + maxWidthXs: string; + /** Styles applied to the root element if `maxWidth="sm"`. */ + maxWidthSm: string; + /** Styles applied to the root element if `maxWidth="md"`. */ + maxWidthMd: string; + /** Styles applied to the root element if `maxWidth="lg"`. */ + maxWidthLg: string; + /** Styles applied to the root element if `maxWidth="xl"`. */ + maxWidthXl: string; +} + +export type ContainerClassKey = keyof ContainerClasses; + +export function getContainerUtilityClass(slot: string): string { + return generateUtilityClass('MuiContainer', slot); +} + +const containerClasses: ContainerClasses = generateUtilityClasses('MuiContainer', [ + 'root', + 'disableGutters', + 'fixed', + 'maxWidthXs', + 'maxWidthSm', + 'maxWidthMd', + 'maxWidthLg', + 'maxWidthXl', +]); + +export default containerClasses; diff --git a/packages/mui-system/src/Container/createContainer.tsx b/packages/mui-system/src/Container/createContainer.tsx new file mode 100644 index 00000000000000..b7af2a095bc5dd --- /dev/null +++ b/packages/mui-system/src/Container/createContainer.tsx @@ -0,0 +1,185 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import clsx from 'clsx'; +import { Interpolation, StyledComponent } from '@mui/styled-engine'; +import { unstable_capitalize as capitalize } from '@mui/utils'; +import { OverridableComponent } from '@mui/types'; +import { + unstable_composeClasses as composeClasses, + generateUtilityClass, +} from '@mui/private-classnames'; +import { ContainerProps, ContainerTypeMap } from './ContainerProps'; +import useThemePropsSystem from '../useThemeProps'; +import systemStyled from '../styled'; +import createTheme, { Theme as DefaultTheme, Breakpoint } from '../createTheme'; + +interface StyleFnProps extends ContainerProps { + theme: Theme; + ownerState: ContainerProps; +} + +const defaultTheme = createTheme(); + +const defaultCreateStyledComponent = systemStyled('div', { + name: 'MuiContainer', + slot: 'Root', + overridesResolver: (props, styles) => { + const { ownerState } = props; + + return [ + styles.root, + styles[`maxWidth${capitalize(String(ownerState.maxWidth))}`], + ownerState.fixed && styles.fixed, + ownerState.disableGutters && styles.disableGutters, + ]; + }, +}); + +const useThemePropsDefault = (inProps: ContainerProps) => + useThemePropsSystem({ props: inProps, name: 'MuiContainer', defaultTheme }); + +const useUtilityClasses = (ownerState: ContainerProps, componentName: string) => { + const getContainerUtilityClass = (slot: string) => { + return generateUtilityClass(componentName, slot); + }; + const { classes, fixed, disableGutters, maxWidth } = ownerState; + + const slots = { + root: [ + 'root', + maxWidth && `maxWidth${capitalize(String(maxWidth))}`, + fixed && 'fixed', + disableGutters && 'disableGutters', + ], + }; + + return composeClasses(slots, getContainerUtilityClass, classes); +}; + +type RequiredThemeStructure = Pick; + +export default function createContainer( + options: { + createStyledComponent?: ( + ...styles: Array>> + ) => StyledComponent; + useThemeProps?: (inProps: ContainerProps) => ContainerProps & { component?: React.ElementType }; + componentName?: string; + } = {}, +) { + const { + // This will allow adding custom styled fn (for example for custom sx style function) + createStyledComponent = defaultCreateStyledComponent, + useThemeProps = useThemePropsDefault, + componentName = 'MuiContainer', + } = options; + + const ContainerRoot = createStyledComponent( + ({ theme, ownerState }: StyleFnProps) => + ({ + width: '100%', + marginLeft: 'auto', + boxSizing: 'border-box', + marginRight: 'auto', + display: 'block', // Fix IE11 layout when used with main. + ...(!ownerState.disableGutters && { + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2), + // @ts-ignore module augmentation fails if custom breakpoints are used + [theme.breakpoints.up('sm')]: { + paddingLeft: theme.spacing(3), + paddingRight: theme.spacing(3), + }, + }), + } as Interpolation>), + ({ theme, ownerState }: StyleFnProps) => + ownerState.fixed && + Object.keys(theme.breakpoints.values).reduce((acc, breakpointValueKey) => { + const breakpoint = breakpointValueKey; + const value = theme.breakpoints.values[breakpoint as Breakpoint]; + + if (value !== 0) { + // @ts-ignore + acc[theme.breakpoints.up(breakpoint)] = { + maxWidth: `${value}${theme.breakpoints.unit}`, + }; + } + return acc; + }, {}), + ({ theme, ownerState }: StyleFnProps) => ({ + // @ts-ignore module augmentation fails if custom breakpoints are used + ...(ownerState.maxWidth === 'xs' && { + // @ts-ignore module augmentation fails if custom breakpoints are used + [theme.breakpoints.up('xs')]: { + // @ts-ignore module augmentation fails if custom breakpoints are used + maxWidth: Math.max(theme.breakpoints.values.xs, 444), + }, + }), + ...(ownerState.maxWidth && + // @ts-ignore module augmentation fails if custom breakpoints are used + ownerState.maxWidth !== 'xs' && { + // @ts-ignore module augmentation fails if custom breakpoints are used + [theme.breakpoints.up(ownerState.maxWidth)]: { + // @ts-ignore module augmentation fails if custom breakpoints are used + maxWidth: `${theme.breakpoints.values[ownerState.maxWidth]}${theme.breakpoints.unit}`, + }, + }), + }), + ); + + const Container = React.forwardRef(function Container(inProps, ref) { + const props: ContainerProps & { component?: React.ElementType } = useThemeProps(inProps); + const { + className, + component = 'div', + disableGutters = false, + fixed = false, + maxWidth = 'lg', + classes: classesProp, + ...other + } = props; + + const ownerState = { + ...props, + component, + disableGutters, + fixed, + maxWidth, + }; + + // @ts-ignore module augmentation fails if custom breakpoints are used + const classes = useUtilityClasses(ownerState, componentName); + + return ( + // @ts-ignore theme is injected by the styled util + + ); + }) as OverridableComponent; + + Container.propTypes /* remove-proptypes */ = { + children: PropTypes.node, + classes: PropTypes.object, + className: PropTypes.string, + component: PropTypes.elementType, + disableGutters: PropTypes.bool, + fixed: PropTypes.bool, + maxWidth: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([ + PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl', false]), + PropTypes.string, + ]), + sx: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), + PropTypes.func, + PropTypes.object, + ]), + }; + + return Container; +} diff --git a/packages/mui-system/src/Container/index.d.ts b/packages/mui-system/src/Container/index.d.ts new file mode 100644 index 00000000000000..795a5daccc3b02 --- /dev/null +++ b/packages/mui-system/src/Container/index.d.ts @@ -0,0 +1,5 @@ +export { default } from './Container'; +export * from './Container'; + +export { default as containerClasses } from './containerClasses'; +export * from './containerClasses'; diff --git a/packages/mui-system/src/Container/index.js b/packages/mui-system/src/Container/index.js new file mode 100644 index 00000000000000..be7c5c912f5943 --- /dev/null +++ b/packages/mui-system/src/Container/index.js @@ -0,0 +1,4 @@ +export { default } from './Container'; + +export { default as containerClasses } from './containerClasses'; +export * from './containerClasses'; diff --git a/packages/mui-system/src/createTheme/createBreakpoints.d.ts b/packages/mui-system/src/createTheme/createBreakpoints.d.ts index f82213a9c70117..4404933d0e9b5e 100644 --- a/packages/mui-system/src/createTheme/createBreakpoints.d.ts +++ b/packages/mui-system/src/createTheme/createBreakpoints.d.ts @@ -61,6 +61,11 @@ export interface Breakpoints { * the screen size given by the breakpoint key (exclusive) and starting at the screen size given by the next breakpoint key (inclusive). */ not: (key: Breakpoint) => string; + /** + * The unit used for the breakpoint's values. + * @default 'px' + */ + unit?: string | undefined; } export interface BreakpointsOptions extends Partial { diff --git a/packages/mui-system/src/index.d.ts b/packages/mui-system/src/index.d.ts index f3ba114f05fd21..f9fa2ea4b73b55 100644 --- a/packages/mui-system/src/index.d.ts +++ b/packages/mui-system/src/index.d.ts @@ -169,3 +169,9 @@ export * from './ThemeProvider'; export { default as unstable_createCssVarsProvider, CreateCssVarsProviderResult } from './cssVars'; export { default as unstable_createGetCssVar } from './cssVars/createGetCssVar'; export * from './cssVars'; + +export { default as createContainer } from './Container/createContainer'; +export * from './Container/createContainer'; + +export { default as Container } from './Container'; +export * from './Container'; diff --git a/packages/mui-system/src/index.js b/packages/mui-system/src/index.js index a4aa770e3a1507..e6ace6a8df0a60 100644 --- a/packages/mui-system/src/index.js +++ b/packages/mui-system/src/index.js @@ -48,3 +48,6 @@ export * from './colorManipulator'; export { default as ThemeProvider } from './ThemeProvider'; export { default as unstable_createCssVarsProvider } from './cssVars/createCssVarsProvider'; export { default as unstable_createGetCssVar } from './cssVars/createGetCssVar'; +export { default as createContainer } from './Container/createContainer'; +export { default as Container } from './Container'; +export * from './Container'; diff --git a/packages/mui-system/tsconfig.build.json b/packages/mui-system/tsconfig.build.json index d591edd03a441a..5412a24dd5d6a0 100644 --- a/packages/mui-system/tsconfig.build.json +++ b/packages/mui-system/tsconfig.build.json @@ -11,5 +11,6 @@ "rootDir": "./src" }, "include": ["src/**/*.ts*"], - "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"] + "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"], + "references": [{ "path": "../mui-private-classnames/tsconfig.build.json" }] } diff --git a/scripts/generateProptypes.ts b/scripts/generateProptypes.ts index abd6f22ac8c2ef..ee743e3cd485b6 100644 --- a/scripts/generateProptypes.ts +++ b/scripts/generateProptypes.ts @@ -73,6 +73,7 @@ const useExternalDocumentation: Record = { OutlinedInput: useExternalPropsFromInputBase, Radio: ['disableRipple', 'id', 'inputProps', 'inputRef', 'required'], Checkbox: ['defaultChecked'], + Container: ['component'], Switch: [ 'checked', 'defaultChecked',