Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
add containerQueries
  • Loading branch information
siriwatknp committed Mar 26, 2024
commit ca0d7794bed97d7f8764af17481ca3aa24fdf5cc
2 changes: 1 addition & 1 deletion packages/mui-babel-macros/MuiError.macro.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export default class MuiError {
constructor(message: string);
constructor(message: string, ...args: string[]);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Come across this since my test is the first .ts that uses MuiError with multiple parameters.

}
19 changes: 17 additions & 2 deletions packages/mui-system/src/breakpoints/breakpoints.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import deepmerge from '@mui/utils/deepmerge';
import merge from '../merge';
import { getContainerQuery } from '../cssContainerQueries';

// The breakpoint **start** at this value.
// For instance with the first breakpoint xs: [xs, sm[.
Expand All @@ -16,7 +17,15 @@ const defaultBreakpoints = {
// Sorted ASC by size. That's important.
// It can't be configured as it's used statically for propTypes.
keys: ['xs', 'sm', 'md', 'lg', 'xl'],
up: (key) => `@media (min-width:${values[key]}px)`,
up: (key) => {
let result = typeof key === 'number' ? key : values[key];
if (typeof result === 'number') {
result = `${result}px`;
} else {
result = key;
}
return `@media (min-width:${result})`;
},
};

export function handleBreakpoints(props, propValue, styleFromPropValue) {
Expand All @@ -33,8 +42,14 @@ export function handleBreakpoints(props, propValue, styleFromPropValue) {
if (typeof propValue === 'object') {
const themeBreakpoints = theme.breakpoints || defaultBreakpoints;
return Object.keys(propValue).reduce((acc, breakpoint) => {
if (breakpoint.startsWith('cq')) {
const containerKey = getContainerQuery({ breakpoints: themeBreakpoints }, 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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { expect } from 'chai';

import createTheme from '@mui/system/createTheme';
import cssContainerQueries from '@mui/system/cssContainerQueries';
import cssContainerQueries, {
sortContainerQueries,
getContainerQuery,
} from '@mui/system/cssContainerQueries';

describe('cssContainerQueries', () => {
it('should have `up`, `down`, `between`, `only`, and `not` functions', () => {
Expand Down Expand Up @@ -35,4 +38,54 @@ describe('cssContainerQueries', () => {
'@container sidebar (width<600px) and (width>899.95px)',
);
});

it('should sort container queries', () => {
const theme = cssContainerQueries(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 = cssContainerQueries(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 = cssContainerQueries(createTheme());
getContainerQuery(theme, 'cq0');
}).to.throw(
'MUI: The provided shorthand (cq0) is invalid. The format should be `cq@<breakpoint | number>` or `cq@<breakpoint | number>/<container>`.\n' +
'For example, `cq@sm` or `cq@600` or `cq@40rem/sidebar`.',
);
});
});
70 changes: 66 additions & 4 deletions packages/mui-system/src/cssContainerQueries/cssContainerQueries.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import MuiError from '@mui/internal-babel-macros/MuiError.macro';
import { Breakpoints, Breakpoint } from '../createTheme/createBreakpoints';

type Fn = 'up' | 'down' | 'between' | 'only' | 'not';
Expand All @@ -6,10 +7,8 @@ interface CssContainerQueries {
cq: ((name: string) => Pick<Breakpoints, Fn>) & Pick<Breakpoints, Fn>;
}

export default function cssContainerQueries<T extends { breakpoints: Breakpoints }>(
themeInput: T,
): T & CssContainerQueries {
function toContainerQuery(key: Fn, name?: string) {
function createBreakpointToCQ<T extends { breakpoints: Partial<Breakpoints> }>(themeInput: T) {
return function toContainerQuery(key: Fn, name?: string) {
return (...args: Array<Breakpoint | number>) => {
// @ts-ignore
const result = themeInput.breakpoints[key](...args).replace(
Expand All @@ -24,7 +23,70 @@ export default function cssContainerQueries<T extends { breakpoints: Breakpoints
}
return result;
};
};
}

export function sortContainerQueries(
theme: Partial<CssContainerQueries>,
css: Record<string, any>,
) {
if (!theme.cq) {
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 getContainerQuery(
theme: Partial<CssContainerQueries> & { breakpoints: Pick<Breakpoints, 'up'> },
shorthand: string,
) {
if (shorthand.startsWith('cq')) {
const matches = shorthand.match(/@([^/\n]+)\/?(.+)?/);
if (!matches) {
if (process.env.NODE_ENV !== 'production') {
throw new MuiError(
'MUI: The provided shorthand %s is invalid. The format should be `cq@<breakpoint | number>` or `cq@<breakpoint | number>/<container>`.\n' +
'For example, `cq@sm` or `cq@600` or `cq@40rem/sidebar`.',
`(${shorthand})`,
);
}
return null;
}
const [, containerQuery, containerName] = matches;
const value = (Number.isNaN(+containerQuery) ? containerQuery : +containerQuery) as
| Breakpoint
| number;
if (theme.cq) {
return containerName ? theme.cq(containerName).up(value) : theme.cq.up(value);
}
if (theme.breakpoints) {
return createBreakpointToCQ(theme)('up', containerName)(value);
}
}
return null;
}

export default function cssContainerQueries<T extends { breakpoints: Breakpoints }>(
themeInput: T,
): T & CssContainerQueries {
const toContainerQuery = createBreakpointToCQ(themeInput);
function cq(name: string) {
return {
up: toContainerQuery('up', name),
Expand Down
1 change: 1 addition & 0 deletions packages/mui-system/src/cssContainerQueries/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default } from './cssContainerQueries';
export { getContainerQuery, sortContainerQueries } from './cssContainerQueries';
21 changes: 21 additions & 0 deletions packages/mui-system/src/cssGrid/cssGrid.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,25 @@ describe('grid', () => {
},
});
});

it('should support container queries', () => {
const output1 = grid({
gap: {
'cq@sm': 1,
'cq@900/sidebar': 2,
'cq@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,
},
});
});
});
21 changes: 21 additions & 0 deletions packages/mui-system/src/spacing/spacing.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,27 @@ describe('system spacing', () => {
});
});

it('should support container queries', () => {
const output1 = spacing({
p: {
'cq@sm': 1,
'cq@900/sidebar': 2,
'cq@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,
Expand Down
3 changes: 2 additions & 1 deletion packages/mui-system/src/styleFunctionSx/styleFunctionSx.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
createEmptyBreakpointObject,
removeUnusedBreakpoints,
} from '../breakpoints';
import { sortContainerQueries } from '../cssContainerQueries';
import defaultSxConfig from './defaultSxConfig';

function objectsHaveSameKeys(...objects) {
Expand Down Expand Up @@ -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);
Expand Down
78 changes: 76 additions & 2 deletions packages/mui-system/src/styleFunctionSx/styleFunctionSx.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect } from 'chai';
import styleFunctionSx from './styleFunctionSx';
import cssContainerQueries from '../cssContainerQueries';

describe('styleFunctionSx', () => {
const breakpointsValues = {
Expand All @@ -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'],
Expand Down Expand Up @@ -49,7 +50,7 @@ describe('styleFunctionSx', () => {
lineHeight: 1.43,
},
},
};
});

describe('system', () => {
it('resolves system ', () => {
Expand Down Expand Up @@ -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: {
'cq@xs': 1,
'cq@sm': 2,
'cq@md': 3,
'cq@lg': 4,
'cq@xl': 5,
},
},
});

expect(result).to.deep.equal(queriesExpectedResult);
});

it('merges multiple queries object', () => {
const result = styleFunctionSx({
theme,
sx: {
m: {
'cq@xs': 1,
'cq@sm': 2,
'cq@md': 3,
},
p: {
'cq@xs': 5,
'cq@sm': 6,
'cq@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: { 'cq@md': 1, 'cq@lg': 2 }, p: { 'cq@xs': 0, 'cq@sm': 1, 'cq@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({
Expand Down