diff --git a/docs/data/system/getting-started/the-sx-prop/the-sx-prop.md b/docs/data/system/getting-started/the-sx-prop/the-sx-prop.md index a3e75054c37355..17b147787e83b6 100644 --- a/docs/data/system/getting-started/the-sx-prop/the-sx-prop.md +++ b/docs/data/system/getting-started/the-sx-prop/the-sx-prop.md @@ -179,7 +179,7 @@ Read more on the [Typography page](/system/typography/). ## Responsive values -All properties associated with the `sx` prop also support responsive values for specific breakpoints. +All properties associated with the `sx` prop also support responsive values for specific breakpoints and container queries. Read more on the [Usage page—Responsive values](/system/getting-started/usage/#responsive-values). diff --git a/docs/data/system/getting-started/usage/ContainerQueries.js b/docs/data/system/getting-started/usage/ContainerQueries.js new file mode 100644 index 00000000000000..aa1e523a0989e4 --- /dev/null +++ b/docs/data/system/getting-started/usage/ContainerQueries.js @@ -0,0 +1,88 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; + +export default function ContainerQueries() { + return ( + + + + +
+ + 123 Main St, Phoenix AZ + + + $280,000 — $310,000 + +
+ + Confidence score: 85% + +
+
+
+ ); +} diff --git a/docs/data/system/getting-started/usage/ContainerQueries.tsx b/docs/data/system/getting-started/usage/ContainerQueries.tsx new file mode 100644 index 00000000000000..aa1e523a0989e4 --- /dev/null +++ b/docs/data/system/getting-started/usage/ContainerQueries.tsx @@ -0,0 +1,88 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; + +export default function ContainerQueries() { + return ( + + + + +
+ + 123 Main St, Phoenix AZ + + + $280,000 — $310,000 + +
+ + Confidence score: 85% + +
+
+
+ ); +} diff --git a/docs/data/system/getting-started/usage/usage.md b/docs/data/system/getting-started/usage/usage.md index 487340ad2b1585..1e5d3de608f2f2 100644 --- a/docs/data/system/getting-started/usage/usage.md +++ b/docs/data/system/getting-started/usage/usage.md @@ -291,6 +291,19 @@ The following demo shows how to define a set of breakpoints using the object syn {{"demo": "BreakpointsAsObject.js"}} +:::info +📣 Starting from v6, the object structure supports [container queries](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries) shorthand with `@`. + +We recommend you to check the [browser support](https://caniuse.com/?search=container%20que) before using CSS container queries. +::: + +The shorthand syntax is `@{breakpoint}/{container}`: + +- **breakpoint**: a number for `px` unit or a breakpoint key (e.g. `sm`, `md`, `lg`, `xl` for default breakpoints) or a valid CSS value (e.g. `40em`). +- **container** (optional): the name of the [containment context](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries#naming_containment_contexts). + +{{"demo": "ContainerQueries.js"}} + #### Breakpoints as an array The second option is to define your breakpoints as an array, from smallest to largest. diff --git a/docs/public/static/error-codes.json b/docs/public/static/error-codes.json index a6486f38f45d49..c5acc584689575 100644 --- a/docs/public/static/error-codes.json +++ b/docs/public/static/error-codes.json @@ -18,5 +18,6 @@ "17": "MUI: Expected valid input target. Did you use a custom `slots.input` and forget to forward refs? See https://mui.com/r/input-component-ref-interface for more info.", "18": "MUI: `vars` is a private field used for CSS variables support.\nPlease use another name.", "19": "MUI: `useColorScheme` must be called under ", - "20": "MUI: The `experimental_sx` has been moved to `theme.unstable_sx`.For more details, see https://github.com/mui/material-ui/pull/35150." + "20": "MUI: The `experimental_sx` has been moved to `theme.unstable_sx`.For more details, see https://github.com/mui/material-ui/pull/35150.", + "21": "MUI: The provided shorthand %s is invalid. The format should be `@` or `@/`.\nFor example, `@sm` or `@600` or `@40rem/sidebar`." } diff --git a/packages/mui-babel-macros/MuiError.macro.d.ts b/packages/mui-babel-macros/MuiError.macro.d.ts index 53d894b2241058..54293408e59b1a 100644 --- a/packages/mui-babel-macros/MuiError.macro.d.ts +++ b/packages/mui-babel-macros/MuiError.macro.d.ts @@ -1,3 +1,3 @@ export default class MuiError { - constructor(message: string); + constructor(message: string, ...args: string[]); } diff --git a/packages/mui-joy/src/styles/defaultTheme.test.js b/packages/mui-joy/src/styles/defaultTheme.test.js index 25469909a49cd6..80e9c510728db0 100644 --- a/packages/mui-joy/src/styles/defaultTheme.test.js +++ b/packages/mui-joy/src/styles/defaultTheme.test.js @@ -9,6 +9,7 @@ describe('defaultTheme', () => { 'colorSchemeSelector', 'defaultColorScheme', 'breakpoints', + 'containerQueries', 'components', 'colorSchemes', 'focus', @@ -46,4 +47,11 @@ describe('defaultTheme', () => { expect(defaultTheme.palette.mode).to.equal('light'); expect(defaultTheme.palette.colorScheme).to.equal('light'); }); + + it('has `containerQueries` in the theme', () => { + expect(defaultTheme.containerQueries('sidebar').up('sm')).to.equal( + '@container sidebar (min-width:600px)', + ); + expect(defaultTheme.containerQueries.up(300)).to.equal('@container (min-width:300px)'); + }); }); diff --git a/packages/mui-joy/src/styles/extendTheme.test.js b/packages/mui-joy/src/styles/extendTheme.test.js index c13f29d5c99315..4fcbb49a26e0a3 100644 --- a/packages/mui-joy/src/styles/extendTheme.test.js +++ b/packages/mui-joy/src/styles/extendTheme.test.js @@ -10,6 +10,7 @@ describe('extendTheme', () => { expect([ 'attribute', 'breakpoints', + 'containerQueries', 'colorSchemeSelector', 'components', 'colorSchemes', diff --git a/packages/mui-joy/src/styles/extendTheme.ts b/packages/mui-joy/src/styles/extendTheme.ts index 7404c27310ef29..eb73ba81d40c2d 100644 --- a/packages/mui-joy/src/styles/extendTheme.ts +++ b/packages/mui-joy/src/styles/extendTheme.ts @@ -10,6 +10,7 @@ import { unstable_styleFunctionSx as styleFunctionSx, SxConfig, } from '@mui/system'; +import cssContainerQueries from '@mui/system/cssContainerQueries'; import { unstable_applyStyles as applyStyles } from '@mui/system/createTheme'; import { createUnarySpacing } from '@mui/system/spacing'; import defaultSxConfig from './sxConfig'; @@ -559,7 +560,7 @@ export default function extendTheme(themeOptions?: CssVarsThemeOptions): Theme { ? deepmerge(defaultScales, scalesInput) : defaultScales; - const theme = { + let theme = { colorSchemes, defaultColorScheme: 'light', ...mergedScales, @@ -605,6 +606,7 @@ export default function extendTheme(themeOptions?: CssVarsThemeOptions): Theme { getCssVar, spacing: getSpacingVal(spacing), } as unknown as Theme & { attribute: string; colorSchemeSelector: string }; // Need type casting due to module augmentation inside the repo + theme = cssContainerQueries(theme); /** Color channels generation diff --git a/packages/mui-joy/src/styles/types/theme.ts b/packages/mui-joy/src/styles/types/theme.ts index a75ec6487cba59..78e83cf59dde2d 100644 --- a/packages/mui-joy/src/styles/types/theme.ts +++ b/packages/mui-joy/src/styles/types/theme.ts @@ -1,6 +1,7 @@ import { OverridableStringUnion } from '@mui/types'; import { Breakpoints, + CssContainerQueries, Spacing, SxProps as SystemSxProps, SystemProps as SystemSystemProps, @@ -95,7 +96,7 @@ export type TextColor = export type ThemeCssVar = OverridableStringUnion, ThemeCssVarOverrides>; -export interface Theme extends ThemeScales, RuntimeColorSystem { +export interface Theme extends ThemeScales, RuntimeColorSystem, CssContainerQueries { colorSchemes: Record; defaultColorScheme: DefaultColorScheme | ExtendedColorScheme; focus: Focus; diff --git a/packages/mui-material/src/styles/experimental_extendTheme.test.js b/packages/mui-material/src/styles/experimental_extendTheme.test.js index 297418eea7ada1..0a942ae032eb23 100644 --- a/packages/mui-material/src/styles/experimental_extendTheme.test.js +++ b/packages/mui-material/src/styles/experimental_extendTheme.test.js @@ -408,6 +408,16 @@ describe('experimental_extendTheme', () => { }); }); + describe('container queries', () => { + it('should generate container queries', () => { + const theme = extendTheme(); + expect(theme.containerQueries('sidebar').up('sm')).to.equal( + '@container sidebar (min-width:600px)', + ); + expect(theme.containerQueries.up(300)).to.equal('@container (min-width:300px)'); + }); + }); + it('shallow merges multiple arguments', () => { const theme = extendTheme({ foo: 'I am foo' }, { bar: 'I am bar' }); expect(theme.foo).to.equal('I am foo'); diff --git a/packages/mui-system/src/breakpoints/breakpoints.js b/packages/mui-system/src/breakpoints/breakpoints.js index 4d6fdf84b3eaba..ee98789726298e 100644 --- a/packages/mui-system/src/breakpoints/breakpoints.js +++ b/packages/mui-system/src/breakpoints/breakpoints.js @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import deepmerge from '@mui/utils/deepmerge'; import merge from '../merge'; +import { isCqShorthand, getContainerQuery } from '../cssContainerQueries'; // The breakpoint **start** at this value. // For instance with the first breakpoint xs: [xs, sm[. @@ -19,6 +20,20 @@ const defaultBreakpoints = { up: (key) => `@media (min-width:${values[key]}px)`, }; +const defaultContainerQueries = { + containerQueries: (containerName) => ({ + up: (key) => { + let result = typeof key === 'number' ? key : values[key] || key; + if (typeof result === 'number') { + result = `${result}px`; + } + return containerName + ? `@container ${containerName} (min-width:${result})` + : `@container (min-width:${result})`; + }, + }), +}; + export function handleBreakpoints(props, propValue, styleFromPropValue) { const theme = props.theme || {}; @@ -33,8 +48,17 @@ export function handleBreakpoints(props, propValue, styleFromPropValue) { if (typeof propValue === 'object') { const themeBreakpoints = theme.breakpoints || defaultBreakpoints; return Object.keys(propValue).reduce((acc, breakpoint) => { + if (isCqShorthand(themeBreakpoints.keys, breakpoint)) { + const containerKey = getContainerQuery( + theme.containerQueries ? theme : defaultContainerQueries, + breakpoint, + ); + if (containerKey) { + acc[containerKey] = styleFromPropValue(propValue[breakpoint], breakpoint); + } + } // key is breakpoint - if (Object.keys(themeBreakpoints.values || values).indexOf(breakpoint) !== -1) { + else if (Object.keys(themeBreakpoints.values || values).indexOf(breakpoint) !== -1) { const mediaKey = themeBreakpoints.up(breakpoint); acc[mediaKey] = styleFromPropValue(propValue[breakpoint], breakpoint); } else { diff --git a/packages/mui-system/src/createTheme/createTheme.d.ts b/packages/mui-system/src/createTheme/createTheme.d.ts index aee763ec114398..0d6d555539d1b9 100644 --- a/packages/mui-system/src/createTheme/createTheme.d.ts +++ b/packages/mui-system/src/createTheme/createTheme.d.ts @@ -4,6 +4,7 @@ import { Shape, ShapeOptions } from './shape'; import { Spacing, SpacingOptions } from './createSpacing'; import { SxConfig, SxProps } from '../styleFunctionSx'; import { ApplyStyles } from './applyStyles'; +import { CssContainerQueries } from '../cssContainerQueries'; export { Breakpoint, BreakpointOverrides } from './createBreakpoints'; @@ -24,7 +25,7 @@ export interface ThemeOptions { unstable_sxConfig?: SxConfig; } -export interface Theme { +export interface Theme extends CssContainerQueries { shape: Shape; breakpoints: Breakpoints; direction: Direction; diff --git a/packages/mui-system/src/createTheme/createTheme.js b/packages/mui-system/src/createTheme/createTheme.js index 24c1c4d29971cc..dce4ff93cc428d 100644 --- a/packages/mui-system/src/createTheme/createTheme.js +++ b/packages/mui-system/src/createTheme/createTheme.js @@ -1,5 +1,6 @@ import deepmerge from '@mui/utils/deepmerge'; import createBreakpoints from './createBreakpoints'; +import cssContainerQueries from '../cssContainerQueries'; import shape from './shape'; import createSpacing from './createSpacing'; import styleFunctionSx from '../styleFunctionSx/styleFunctionSx'; @@ -29,6 +30,7 @@ function createTheme(options = {}, ...args) { }, other, ); + muiTheme = cssContainerQueries(muiTheme); muiTheme.applyStyles = applyStyles; diff --git a/packages/mui-system/src/cssContainerQueries/cssContainerQueries.test.ts b/packages/mui-system/src/cssContainerQueries/cssContainerQueries.test.ts new file mode 100644 index 00000000000000..785aa3186732c7 --- /dev/null +++ b/packages/mui-system/src/cssContainerQueries/cssContainerQueries.test.ts @@ -0,0 +1,119 @@ +import { expect } from 'chai'; + +import createTheme from '@mui/system/createTheme'; +import { + isCqShorthand, + sortContainerQueries, + getContainerQuery, +} from '@mui/system/cssContainerQueries'; + +describe('cssContainerQueries', () => { + it('should return false if the shorthand is not a container query', () => { + expect(isCqShorthand(['xs', 'sm', 'md'], '@container (min-width:600px)')).to.equal(false); + expect(isCqShorthand(['xs', 'sm', 'md'], '@media (min-width:600px)')).to.equal(false); + expect(isCqShorthand(['xs', 'sm', 'md'], '@page')).to.equal(false); + expect(isCqShorthand(['xs', 'sm', 'md'], '@support (display: flex)')).to.equal(false); + }); + + it('should return true if the shorthand is a container query', () => { + expect(isCqShorthand(['xs', 'sm', 'md'], '@xs')).to.equal(true); + expect(isCqShorthand(['xs', 'sm', 'md'], '@xs/sidebar')).to.equal(true); + expect(isCqShorthand(['xs', 'sm', 'md'], '@md')).to.equal(true); + expect(isCqShorthand(['xs', 'sm', 'md'], '@200')).to.equal(true); + expect(isCqShorthand(['xs', 'sm', 'md'], '@15.5rem')).to.equal(true); + }); + + it('should have `up`, `down`, `between`, `only`, and `not` functions', () => { + const theme = createTheme(); + + expect(theme.containerQueries.up('sm')).to.equal('@container (min-width:600px)'); + expect(theme.containerQueries.down('sm')).to.equal('@container (max-width:599.95px)'); + expect(theme.containerQueries.between('sm', 'md')).to.equal( + '@container (min-width:600px) and (max-width:899.95px)', + ); + expect(theme.containerQueries.only('sm')).to.equal( + '@container (min-width:600px) and (max-width:899.95px)', + ); + expect(theme.containerQueries.not('xs')).to.equal('@container (min-width:600px)'); + expect(theme.containerQueries.not('xl')).to.equal('@container (max-width:1535.95px)'); + expect(theme.containerQueries.not('md')).to.equal( + '@container (width<900px) and (width>1199.95px)', + ); + }); + + it('should be able to create named containment context', () => { + const theme = createTheme(); + + expect(theme.containerQueries('sidebar').up('sm')).to.equal( + '@container sidebar (min-width:600px)', + ); + expect(theme.containerQueries('sidebar').down('sm')).to.equal( + '@container sidebar (max-width:599.95px)', + ); + expect(theme.containerQueries('sidebar').between('sm', 'md')).to.equal( + '@container sidebar (min-width:600px) and (max-width:899.95px)', + ); + expect(theme.containerQueries('sidebar').only('sm')).to.equal( + '@container sidebar (min-width:600px) and (max-width:899.95px)', + ); + expect(theme.containerQueries('sidebar').not('xs')).to.equal( + '@container sidebar (min-width:600px)', + ); + expect(theme.containerQueries('sidebar').not('xl')).to.equal( + '@container sidebar (max-width:1535.95px)', + ); + expect(theme.containerQueries('sidebar').not('sm')).to.equal( + '@container sidebar (width<600px) and (width>899.95px)', + ); + }); + + it('should sort container queries', () => { + const theme = createTheme(); + + const css = { + '@container (min-width:960px)': {}, + '@container (min-width:1280px)': {}, + '@container (min-width:0px)': {}, + '@container (min-width:600px)': {}, + }; + + const sorted = sortContainerQueries(theme, css); + + expect(Object.keys(sorted)).to.deep.equal([ + '@container (min-width:0px)', + '@container (min-width:600px)', + '@container (min-width:960px)', + '@container (min-width:1280px)', + ]); + }); + + it('should sort container queries with other unit', () => { + const theme = createTheme(); + + const css = { + '@container (min-width:30.5rem)': {}, + '@container (min-width:20rem)': {}, + '@container (min-width:50.5rem)': {}, + '@container (min-width:40rem)': {}, + }; + + const sorted = sortContainerQueries(theme, css); + + expect(Object.keys(sorted)).to.deep.equal([ + '@container (min-width:20rem)', + '@container (min-width:30.5rem)', + '@container (min-width:40rem)', + '@container (min-width:50.5rem)', + ]); + }); + + it('should throw an error if shorthand is invalid', () => { + expect(() => { + const theme = createTheme(); + getContainerQuery(theme, 'cq0'); + }).to.throw( + 'MUI: The provided shorthand (cq0) is invalid. The format should be `@` or `@/`.\n' + + 'For example, `@sm` or `@600` or `@40rem/sidebar`.', + ); + }); +}); diff --git a/packages/mui-system/src/cssContainerQueries/cssContainerQueries.ts b/packages/mui-system/src/cssContainerQueries/cssContainerQueries.ts new file mode 100644 index 00000000000000..49b85f558cff00 --- /dev/null +++ b/packages/mui-system/src/cssContainerQueries/cssContainerQueries.ts @@ -0,0 +1,118 @@ +import MuiError from '@mui/internal-babel-macros/MuiError.macro'; +import { Breakpoints, Breakpoint } from '../createTheme/createBreakpoints'; + +interface ContainerQueries { + up: Breakpoints['up']; + down: Breakpoints['down']; + between: Breakpoints['between']; + only: Breakpoints['only']; + not: Breakpoints['not']; +} + +export interface CssContainerQueries { + containerQueries: ((name: string) => ContainerQueries) & ContainerQueries; +} + +/** + * For using in `sx` prop to sort the breakpoint from low to high. + * Note: this function does not work and will not support multiple units. + * e.g. input: { '@container (min-width:300px)': '1rem', '@container (min-width:40rem)': '2rem' } + * output: { '@container (min-width:40rem)': '2rem', '@container (min-width:300px)': '1rem' } // since 40 < 300 eventhough 40rem > 300px + */ +export function sortContainerQueries( + theme: Partial, + css: Record, +) { + if (!theme.containerQueries) { + return css; + } + const sorted = Object.keys(css) + .filter((key) => key.startsWith('@container')) + .sort((a, b) => { + const regex = /min-width:\s*([0-9.]+)/; + return +(a.match(regex)?.[1] || 0) - +(b.match(regex)?.[1] || 0); + }); + if (!sorted.length) { + return css; + } + return sorted.reduce( + (acc, key) => { + const value = css[key]; + delete acc[key]; + acc[key] = value; + return acc; + }, + { ...css }, + ); +} + +export function isCqShorthand(breakpointKeys: string[], value: string) { + return ( + value.startsWith('@') && + (breakpointKeys.some((key) => value.startsWith(`@${key}`)) || !!value.match(/^@\d/)) + ); +} + +export function getContainerQuery(theme: CssContainerQueries, shorthand: string) { + const matches = shorthand.match(/^@([^/]+)\/?(.+)?$/); + if (!matches) { + if (process.env.NODE_ENV !== 'production') { + throw new MuiError( + 'MUI: The provided shorthand %s is invalid. The format should be `@` or `@/`.\n' + + 'For example, `@sm` or `@600` or `@40rem/sidebar`.', + `(${shorthand})`, + ); + } + return null; + } + const [, containerQuery, containerName] = matches; + const value = (Number.isNaN(+containerQuery) ? containerQuery : +containerQuery) as + | Breakpoint + | number; + return theme.containerQueries(containerName).up(value); +} + +export default function cssContainerQueries( + themeInput: T, +): T & CssContainerQueries { + const toContainerQuery = (mediaQuery: string, name?: string) => + mediaQuery.replace('@media', name ? `@container ${name}` : '@container'); + + function attachCq(node: any, name?: string) { + node.up = (...args: Parameters) => + toContainerQuery(themeInput.breakpoints.up(...args), name); + + node.down = (...args: Parameters) => + toContainerQuery(themeInput.breakpoints.down(...args), name); + + node.between = (...args: Parameters) => + toContainerQuery(themeInput.breakpoints.between(...args), name); + + node.only = (...args: Parameters) => + toContainerQuery(themeInput.breakpoints.only(...args), name); + + node.not = (...args: Parameters) => { + const result = toContainerQuery(themeInput.breakpoints.not(...args), name); + if (result.includes('not all and')) { + // `@container` does not work with `not all and`, so need to invert the logic + return result + .replace('not all and ', '') + .replace('min-width:', 'width<') + .replace('max-width:', 'width>'); + } + return result; + }; + } + const node = {}; + const containerQueries = ((name: string) => { + attachCq(node, name); + return node; + }) as CssContainerQueries['containerQueries']; + + attachCq(containerQueries); + + return { + ...themeInput, + containerQueries, + }; +} diff --git a/packages/mui-system/src/cssContainerQueries/index.ts b/packages/mui-system/src/cssContainerQueries/index.ts new file mode 100644 index 00000000000000..e1061e417fc187 --- /dev/null +++ b/packages/mui-system/src/cssContainerQueries/index.ts @@ -0,0 +1,3 @@ +export { default } from './cssContainerQueries'; +export { isCqShorthand, getContainerQuery, sortContainerQueries } from './cssContainerQueries'; +export type { CssContainerQueries } from './cssContainerQueries'; diff --git a/packages/mui-system/src/cssGrid/cssGrid.test.js b/packages/mui-system/src/cssGrid/cssGrid.test.js index a12b8d0e511821..62266bcdfb42c4 100644 --- a/packages/mui-system/src/cssGrid/cssGrid.test.js +++ b/packages/mui-system/src/cssGrid/cssGrid.test.js @@ -37,4 +37,25 @@ describe('grid', () => { }, }); }); + + it('should support container queries', () => { + const output1 = grid({ + gap: { + '@sm': 1, + '@900/sidebar': 2, + '@80rem/sidebar': 3, + }, + }); + expect(output1).to.deep.equal({ + '@container (min-width:600px)': { + gap: 8, + }, + '@container sidebar (min-width:900px)': { + gap: 16, + }, + '@container sidebar (min-width:80rem)': { + gap: 24, + }, + }); + }); }); diff --git a/packages/mui-system/src/index.d.ts b/packages/mui-system/src/index.d.ts index ea3580a981cc9f..ec606cc14d467b 100644 --- a/packages/mui-system/src/index.d.ts +++ b/packages/mui-system/src/index.d.ts @@ -5,6 +5,8 @@ export * from './borders'; export { default as breakpoints, handleBreakpoints, mergeBreakpointsInOrder } from './breakpoints'; +export { default as cssContainerQueries, type CssContainerQueries } from './cssContainerQueries'; + export { default as compose } from './compose'; export * from './display'; diff --git a/packages/mui-system/src/index.js b/packages/mui-system/src/index.js index b7215ef8dee1b4..8bc93c6d64d0c9 100644 --- a/packages/mui-system/src/index.js +++ b/packages/mui-system/src/index.js @@ -5,6 +5,7 @@ export { default as GlobalStyles } from './GlobalStyles'; export { default as borders } from './borders'; export * from './borders'; export { default as breakpoints } from './breakpoints'; +export { default as cssContainerQueries } from './cssContainerQueries'; export { handleBreakpoints, mergeBreakpointsInOrder, diff --git a/packages/mui-system/src/spacing/spacing.test.js b/packages/mui-system/src/spacing/spacing.test.js index 92f8f5306e94fd..8373c3cbbee208 100644 --- a/packages/mui-system/src/spacing/spacing.test.js +++ b/packages/mui-system/src/spacing/spacing.test.js @@ -184,6 +184,27 @@ describe('system spacing', () => { }); }); + it('should support container queries', () => { + const output1 = spacing({ + p: { + '@sm': 1, + '@900/sidebar': 2, + '@80rem/sidebar': 3, + }, + }); + expect(output1).to.deep.equal({ + '@container (min-width:600px)': { + padding: 8, + }, + '@container sidebar (min-width:900px)': { + padding: 16, + }, + '@container sidebar (min-width:80rem)': { + padding: 24, + }, + }); + }); + it('should support full version', () => { const output1 = spacing({ paddingTop: 1, diff --git a/packages/mui-system/src/styleFunctionSx/styleFunctionSx.js b/packages/mui-system/src/styleFunctionSx/styleFunctionSx.js index 151f117c416477..a74b44b553311c 100644 --- a/packages/mui-system/src/styleFunctionSx/styleFunctionSx.js +++ b/packages/mui-system/src/styleFunctionSx/styleFunctionSx.js @@ -6,6 +6,7 @@ import { createEmptyBreakpointObject, removeUnusedBreakpoints, } from '../breakpoints'; +import { sortContainerQueries } from '../cssContainerQueries'; import defaultSxConfig from './defaultSxConfig'; function objectsHaveSameKeys(...objects) { @@ -127,7 +128,7 @@ export function unstable_createStyleFunctionSx() { } }); - return removeUnusedBreakpoints(breakpointsKeys, css); + return sortContainerQueries(theme, removeUnusedBreakpoints(breakpointsKeys, css)); } return Array.isArray(sx) ? sx.map(traverse) : traverse(sx); diff --git a/packages/mui-system/src/styleFunctionSx/styleFunctionSx.test.js b/packages/mui-system/src/styleFunctionSx/styleFunctionSx.test.js index dced5959cb72aa..d8ca1d7a8e778e 100644 --- a/packages/mui-system/src/styleFunctionSx/styleFunctionSx.test.js +++ b/packages/mui-system/src/styleFunctionSx/styleFunctionSx.test.js @@ -1,5 +1,6 @@ import { expect } from 'chai'; import styleFunctionSx from './styleFunctionSx'; +import cssContainerQueries from '../cssContainerQueries'; describe('styleFunctionSx', () => { const breakpointsValues = { @@ -12,7 +13,7 @@ describe('styleFunctionSx', () => { const round = (value) => Math.round(value * 1e5) / 1e5; - const theme = { + const theme = cssContainerQueries({ spacing: (val) => `${val * 10}px`, breakpoints: { keys: ['xs', 'sm', 'md', 'lg', 'xl'], @@ -49,7 +50,7 @@ describe('styleFunctionSx', () => { lineHeight: 1.43, }, }, - }; + }); describe('system', () => { it('resolves system ', () => { @@ -243,6 +244,79 @@ describe('styleFunctionSx', () => { }); }); + describe('container queries', () => { + const queriesExpectedResult = { + '@container (min-width:0px)': { border: '1px solid' }, + '@container (min-width:600px)': { border: '2px solid' }, + '@container (min-width:960px)': { border: '3px solid' }, + '@container (min-width:1280px)': { border: '4px solid' }, + '@container (min-width:1920px)': { border: '5px solid' }, + }; + + it('resolves queries object', () => { + const result = styleFunctionSx({ + theme, + sx: { + border: { + '@xs': 1, + '@sm': 2, + '@md': 3, + '@lg': 4, + '@xl': 5, + }, + }, + }); + + expect(result).to.deep.equal(queriesExpectedResult); + }); + + it('merges multiple queries object', () => { + const result = styleFunctionSx({ + theme, + sx: { + m: { + '@xs': 1, + '@sm': 2, + '@md': 3, + }, + p: { + '@xs': 5, + '@sm': 6, + '@md': 7, + }, + }, + }); + + expect(result).to.deep.equal({ + '@container (min-width:0px)': { padding: '50px', margin: '10px' }, + '@container (min-width:600px)': { padding: '60px', margin: '20px' }, + '@container (min-width:960px)': { padding: '70px', margin: '30px' }, + }); + }); + + it('writes queries in correct order', () => { + const result = styleFunctionSx({ + theme, + sx: { m: { '@md': 1, '@lg': 2 }, p: { '@xs': 0, '@sm': 1, '@md': 2 } }, + }); + + // Test the order + expect(Object.keys(result)).to.deep.equal([ + '@container (min-width:0px)', + '@container (min-width:600px)', + '@container (min-width:960px)', + '@container (min-width:1280px)', + ]); + + expect(result).to.deep.equal({ + '@container (min-width:0px)': { padding: '0px' }, + '@container (min-width:600px)': { padding: '10px' }, + '@container (min-width:960px)': { padding: '20px', margin: '10px' }, + '@container (min-width:1280px)': { margin: '20px' }, + }); + }); + }); + describe('theme callback', () => { it('works on CSS properties', () => { const result = styleFunctionSx({