Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: compound variants don't work when each variant is determined by media #42

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
45 changes: 45 additions & 0 deletions example/src/Example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,51 @@ export default function Example() {
<Heading underlined heading="h5">
Heading
</Heading>
<Heading
// NOTE: test compound variants with media.
// If it work text color and border color are same
// as text size will be changed by device size.
heading={{
'@xxl': 'h1',
'@xl': 'h1',
'@lg': 'h2',
'@md': 'h3',
}}
underlined={{
'@initial': true,
}}
>
Media
</Heading>
<Heading
heading={{
'@initial': 'h5',
}}
underlined={{
'@initial': false,
'@phone': true,
}}
>
Underlined appears on phone
</Heading>
<Heading
heading="h5"
// NOTE: media style can be marged and overwriten by later applied media styles.
css={{
'@phone': {
borderColor: 'white',
borderRightWidth: 1,
borderTopWidth: 1,
borderLeftWidth: 1,
borderBottomWidth: 1,
},
'@md': {
borderColor: 'black',
},
}}
>
Square border appears on md Phone
</Heading>
</Stack>
)}
{!example && (
Expand Down
6 changes: 1 addition & 5 deletions example/src/components/Heading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,39 +19,34 @@ const underLinedStyle = css({
underlined: true,
css: {
borderBottomColor: 'black',
borderBottomWidth: 1,
},
},
{
heading: 'h4',
underlined: true,
css: {
borderBottomColor: 'red',
borderBottomWidth: 1,
},
},
{
heading: 'h3',
underlined: true,
css: {
borderBottomColor: 'blue',
borderBottomWidth: 1,
},
},
{
heading: 'h2',
underlined: true,
css: {
borderBottomColor: 'green',
borderBottomWidth: 1,
},
},
{
heading: 'h1',
underlined: true,
css: {
borderBottomColor: 'purple',
borderBottomWidth: 1,
},
},
{
Expand Down Expand Up @@ -82,6 +77,7 @@ export const Heading = styled(
true: {
paddingRight: 4,
paddingLeft: 4,
borderBottomWidth: 1,
},
},
},
Expand Down
102 changes: 42 additions & 60 deletions src/internals/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,9 @@ export function createStitches(config = {}) {

let variantStyles = [];
let compoundVariantStyles = [];
const appliedVariants = {};

const { mediaKey, breakpoint } = useMemo(() => {
const matchedMedias = useMemo(() => {
if (typeof config.media === 'object') {
const correctedWindowWidth =
PixelRatio.getPixelSizeForLayoutSize(windowWidth);
Expand All @@ -128,63 +129,58 @@ export function createStitches(config = {}) {
// The order of the media key value pairs should be constant
// but is that guaranteed? So if the keys are ordered from
// smallest screen size to largest everything should work ok...
const _mediaKey = utils.resolveMediaRangeQuery(
const matchedMedias = utils.resolveMediaRangeQuery(
config.media,
correctedWindowWidth
);

return {
mediaKey: _mediaKey,
breakpoint: _mediaKey && `@${_mediaKey}`,
};
return matchedMedias;
}

return {};
return [];
}, [windowWidth]);

if (variants) {
variantStyles = Object.keys(variants)
.map((prop) => {
let propValue = props[prop];
const propValue = props[prop] ?? defaultVariants[prop];

if (propValue === undefined) {
propValue = defaultVariants[prop];
}

let styleSheetKey = `${prop}_${propValue}`;
const styleSheetKey = `${prop}_${propValue}`;

// Handle responsive prop value
// NOTE: only one media query will be applied since the `styleSheetKey`
// is being rewritten by the last matching media query and defaults to `@initial`
if (
typeof propValue === 'object' &&
typeof config.media === 'object'
) {
// `@initial` acts as the default value if none of the media query values match
// It's basically the as setting `prop="value"`, eg. `color="primary"`
if (typeof propValue['@initial'] === 'string') {
styleSheetKey = `${prop}_${propValue['@initial']}`;
}

if (breakpoint && propValue[breakpoint] !== undefined) {
const val = config.media[mediaKey];

if (val === true || typeof val === 'string') {
styleSheetKey = `${prop}_${propValue[breakpoint]}`;
}
if (typeof propValue !== 'object') {
if (propValue) {
appliedVariants[prop] = propValue;
}
return styleSheet[styleSheetKey];
}

const extractedStyle = styleSheetKey
? styleSheet[styleSheetKey]
: undefined;

if (extractedStyle && breakpoint in extractedStyle) {
// WARNING: lodash merge modifies the first argument reference or skips if object is frozen.
return merge({}, extractedStyle, extractedStyle[breakpoint]);
}
const matchedMediasDetected = [
{
mediaKey: 'initial',
breakpoint: '@initial',
},
...matchedMedias,
];

// NOTE: Even if multiple values are matched, restict variant value is last matched and
// compensated to be determined uniquely.
const finalVariantStyle = matchedMediasDetected.reduce(
(currentStyle, matchedMedia) => {
const breakpoint = matchedMedia.breakpoint;
if (breakpoint && propValue[breakpoint] !== undefined) {
const styleSheetKey = `${prop}_${propValue[breakpoint]}`;
const extractedStyle = styleSheet[styleSheetKey];
if (extractedStyle) {
appliedVariants[prop] = propValue[breakpoint];
return extractedStyle;
}
}
return currentStyle;
},
{}
);

return extractedStyle;
return finalVariantStyle;
})
.filter(Boolean);
}
Expand All @@ -198,46 +194,32 @@ export function createStitches(config = {}) {

if (
compoundEntries.every(([prop, value]) => {
const propValue = props[prop] ?? defaultVariants[prop];
const propValue =
appliedVariants[prop] ?? defaultVariants[prop];
return propValue === value;
})
) {
const key = utils.getCompoundKey(compoundEntries);
const extractedStyle = styleSheet[key];

if (extractedStyle && breakpoint in extractedStyle) {
// WARNING: lodash merge modifies the first argument reference or skips if object is frozen.
return merge({}, extractedStyle, extractedStyle[breakpoint]);
}

return extractedStyle;
return styleSheet[key];
}
})
.filter(Boolean);
}

let cssStyles = props.css
const cssStyles = props.css
? utils.processStyles({
styles: props.css || {},
theme: theme.values,
config,
})
: {};

if (cssStyles && breakpoint in cssStyles) {
// WARNING: lodash merge modifies the first argument reference or skips if object is frozen.
cssStyles = merge({}, cssStyles, cssStyles[breakpoint]);
}

const mediaStyle = styleSheet.base[breakpoint] || {};

const stitchesStyles = [
styleSheet.base,
mediaStyle,
...variantStyles,
...compoundVariantStyles,
cssStyles,
];
].map((style) => utils.applyMediaStyles(style, matchedMedias));

const allStyles =
typeof props.style === 'function'
Expand Down
43 changes: 30 additions & 13 deletions src/internals/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,24 +59,41 @@ function matchMediaRangeQuery(query, windowWidth) {
return result;
}

export function resolveMediaRangeQuery(media, windowWidth) {
const entries = Object.entries(media);
let result;

for (let i = 0; i < entries.length; i++) {
const [breakpoint, queryOrFlag] = entries[i];

// TODO: handle boolean flag
if (typeof queryOrFlag !== 'string') continue;

const match = matchMediaRangeQuery(queryOrFlag, windowWidth);
function createMatchedMedia(mediaKey) {
return {
mediaKey: mediaKey,
breakpoint: mediaKey && `@${mediaKey}`,
};
}

export function resolveMediaRangeQuery(queryObjects, windowWidth) {
const iterator = Object.entries(queryObjects);
const mediaAppliedKeys = [];
for (let i = 0; i < iterator.length; i++) {
const [key, query] = iterator[i];
if (query === true) {
mediaAppliedKeys.push(createMatchedMedia(key));
}
if (typeof query !== 'string') continue;
const match = matchMediaRangeQuery(query, windowWidth);
if (match) {
result = breakpoint;
mediaAppliedKeys.push(createMatchedMedia(key));
}
}
return mediaAppliedKeys;
}

return result;
export function applyMediaStyles(originalStyle, matchedMedias) {
return matchedMedias.reduce(
(currentStyle, matcheMedia) => {
const breakpoint = matcheMedia.breakpoint;
if (breakpoint && breakpoint in originalStyle) {
return merge(currentStyle, originalStyle[breakpoint]);
}
return currentStyle;
},
{ ...originalStyle }
);
}

export function processThemeMap(themeMap) {
Expand Down