Skip to content

Commit

Permalink
feat(806): implement #400 container queries
Browse files Browse the repository at this point in the history
  • Loading branch information
LukeFinch committed Apr 24, 2023
1 parent 17111bb commit a162a43
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 9 deletions.
33 changes: 32 additions & 1 deletion src/utils/__tests__/base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,38 @@ describe('getXFromTheme', () => {
'@media screen and (min-width: 768px)': {width: '12px'},
});
});

test('getXFromTheme with CQ value', () => {
const result = getXFromTheme('sizing')('width', {
'300px': 'sizing010',
'500px': 'sizing020',
'800px': 'sizing030',
})({theme});
expect(result).toEqual({
'@container (min-width: 300px)': {width: '4px'},
'@container (min-width: 500px)': {width: '8px'},
'@container (min-width: 800px)': {width: '12px'},
});
});
test('getXFromTheme with CQ & MQ value', () => {
const result = getXFromTheme('sizing')('width', {
xs: 'sizing010',
sm: 'sizing020',
'300px': 'sizing010',
'500px': 'sizing020',
'800px': 'sizing030',
})({theme});
expect(result).toEqual({
'@container (min-width: 300px)': {width: '4px'},
'@container (min-width: 500px)': {width: '8px'},
'@container (min-width: 800px)': {width: '12px'},
'@media screen and (max-width: 479px)': {
width: '4px',
},
'@media screen and (min-width: 480px)': {
width: '8px',
},
});
});
test('getXFromTheme with non MQ and callback', () => {
const cb = (value: string) => ({padding: `${value} 0`, width: value});
const result = getXFromTheme('sizing')(cb, 'sizing050')({theme});
Expand Down
21 changes: 21 additions & 0 deletions src/utils/__tests__/is-valid-css-size.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {isValidCSSSizeUnit} from '../style/utils';

describe('isValidCSSSizeUnit', () => {
it('should return true for valid CSS size units', () => {
expect(isValidCSSSizeUnit('12px')).toBe(true);
expect(isValidCSSSizeUnit('100%')).toBe(true);
expect(isValidCSSSizeUnit('10em')).toBe(true);
expect(isValidCSSSizeUnit('24pt')).toBe(true);
expect(isValidCSSSizeUnit('100cm')).toBe(true);
expect(isValidCSSSizeUnit('0.5rem')).toBe(true);
expect(isValidCSSSizeUnit('0.5vmin')).toBe(true);
});

it('should return false for invalid CSS size units', () => {
expect(isValidCSSSizeUnit('10')).toBe(false);
expect(isValidCSSSizeUnit('10pt ')).toBe(false);
expect(isValidCSSSizeUnit('abc')).toBe(false);
expect(isValidCSSSizeUnit('50deg')).toBe(false);
expect(isValidCSSSizeUnit('2px2em')).toBe(false);
});
});
18 changes: 17 additions & 1 deletion src/utils/responsive-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Breakpoints, BreakpointKeys, Theme} from '../theme';
import {hasOwnProperty} from './has-own-property';
import {isValidCSSSizeUnit} from './style/utils';

interface ThemeProp {
theme: Theme;
Expand Down Expand Up @@ -50,4 +51,19 @@ export const isResponsive = (
): prop is Record<keyof Breakpoints, unknown> =>
!!prop &&
typeof prop === 'object' &&
Object.keys(breakpoints).some(bp => prop && hasOwnProperty(prop, bp));
(Object.keys(breakpoints).some(bp => prop && hasOwnProperty(prop, bp)) ||
Object.keys(prop).some(p => isValidCSSSizeUnit(p)));

export const getContainerQuery = (
minWidth: string,
maxWidth?: string,
): string => {
const queries = [];
if (minWidth) {
queries.push(`(min-width: ${minWidth})`);
}
if (maxWidth) {
queries.push(`(max-width: ${maxWidth})`);
}
return `@container ${queries.join(' AND ')}`;
};
39 changes: 35 additions & 4 deletions src/utils/style/base.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import {Theme, BreakpointKeys} from '../../theme';
import {isResponsive, getMediaQueryFromTheme} from '../responsive-helpers';
import {filterObject} from '../filter-object';
import {
isResponsive,
getMediaQueryFromTheme,
getContainerQuery,
} from '../responsive-helpers';
import {filterObject, rejectObject} from '../filter-object';
import {getToken} from '../get-token';
import {ThemeProp} from '../style-types';
import {MQ} from './types';
Expand Down Expand Up @@ -76,7 +80,7 @@ export const getResponsiveValueFromTheme = <ThemeToken extends string>(
([a], [b]) =>
mq.indexOf(a as BreakpointKeys) - mq.indexOf(b as BreakpointKeys),
) as any; // eslint-disable-line @typescript-eslint/no-explicit-any
const cssObject = presetKeys
const cssMediaQueryObject = presetKeys
.filter(
// Exclude invalid breakpoints and theme section keys
([breakpointKey, presetKey]) =>
Expand Down Expand Up @@ -114,7 +118,34 @@ export const getResponsiveValueFromTheme = <ThemeToken extends string>(
return acc;
}, {} as Record<string, unknown>);

return Object.entries(cssObject);
// get container queries
const containerKeys: [string, ThemeToken][] = Object.entries(
rejectObject(propKeys, mq),
) as any; // eslint-disable-line @typescript-eslint/no-explicit-any

const cssContainerQueryObject = containerKeys.reduce(
(acc, [minWidth, presetKey]) => {
let preset = '' as Record<ThemeToken, unknown>[ThemeToken];
const MQtokens =
typeof presetKey === 'string' && (presetKey as string).split(' ');
if (themeKey === 'spacePresets' && isMQTokenArray(MQtokens)) {
preset = mapTokensArray(MQtokens);
} else {
preset =
section[presetKey] ||
(canHaveNonThemeValue &&
isValidUnit(themeKey, presetKey) &&
presetKey);
}

const mediaQuery = getContainerQuery(minWidth);
acc[mediaQuery] = preset;
return acc;
},
{} as Record<string, unknown>,
);

return Object.entries({...cssMediaQueryObject, ...cssContainerQueryObject});
}

const noMQtokens =
Expand Down
6 changes: 4 additions & 2 deletions src/utils/style/getters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ export const handleResponsiveProp = <Props extends ThemeProp, T>(
? commonMQKeys
: ['xs', ...commonMQKeys];

const cssObject = usedMQKeys.reduce((acc, mqKey, index) => {
const cssMediaQueryObject = usedMQKeys.reduce((acc, mqKey, index) => {
const fromMqKey = mqKey;
const toMqKey = usedMQKeys[index + 1] ? usedMQKeys[index + 1] : undefined;

Expand All @@ -378,5 +378,7 @@ export const handleResponsiveProp = <Props extends ThemeProp, T>(
return acc;
}, {} as Record<string, unknown>) as CSSObject;

return cssObject;
const cssContentQueryObject = {};

return {...cssMediaQueryObject, ...cssContentQueryObject};
};
2 changes: 1 addition & 1 deletion src/utils/style/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ export type MQPartial<T> = Partial<{
xl: T;
}>;

export type MQ<T> = T | MQPartial<T>;
export type MQ<T> = T | MQPartial<T> | {[minWidth: string]: T};
11 changes: 11 additions & 0 deletions src/utils/style/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ export const CSSUnits = [
'vmin',
'vmax',
'%',
'cqw',
'cqh',
'cqi',
'cqb',
'cqmin',
'cqmax',
];

export const CSSColorNames = [
Expand Down Expand Up @@ -219,6 +225,11 @@ export const isValidUnit = (themeKey: string, value: any) => {
);
};

export const isValidCSSSizeUnit = (value: string) => {
const regex = new RegExp(`^\\d+(\\.\\d+)?(${CSSUnits.join('|')})$`);
return regex.test(value);
};

export const isArrayLikeObject = (value: string | object) =>
typeof value === 'object' && '0' in value;

Expand Down

0 comments on commit a162a43

Please sign in to comment.