Skip to content

Commit c189613

Browse files
committed
fix: Conflict Resolutions with Media Queries.
This fixes two bugs: 1. Conlict resolution was skipping resolution rules if the resolved selector was the same, but this missed the use case where the selector could be in a conditional at-rule. I removed the deduplication. Fixing this uncovered the second issue. 2. We were only preserving the conditional at-rule context on the local side of the conflict resolution. If the conditional was on the remote side of the conflict that context was being lost. To fix this, we now clone the conditional at-rules that form the context of the remote selector into the local stylesheet around the resolution rule. Closes #372.
1 parent 488e23e commit c189613

File tree

3 files changed

+111
-5
lines changed

3 files changed

+111
-5
lines changed

packages/@css-blocks/core/src/BlockCompiler/ConflictResolver.ts

+42-4
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ export class ConflictResolver {
210210
}
211211

212212
// Crawl up inheritance tree of the other block and attempt to resolve the conflict at each level.
213+
// XXX Should this really abort when it finds the first conflict?
213214
let foundConflict = ConflictType.noConflict;
214215
do {
215216
foundConflict = this.resolveConflictWith(resolution.path, other, res);
@@ -244,7 +245,6 @@ export class ConflictResolver {
244245

245246
// Something to consider: when resolving against a sub-block that has overridden a property, do we need
246247
// to include the base object selector(s) in the key selector as well?
247-
const resolvedSelectors = new Set<string>();
248248
const query = new QueryKeySelector(other);
249249
const result = query.execute(root, other.block);
250250
let foundConflict: ConflictType = ConflictType.noConflict;
@@ -261,9 +261,8 @@ export class ConflictResolver {
261261

262262
// avoid duplicate selector via permutation
263263
let newSelStr = newSelectors.join(",\n");
264-
if (resolvedSelectors.has(newSelStr)) { continue; }
265-
resolvedSelectors.add(newSelStr);
266264
let newRule = postcss.rule({ selector: newSelStr });
265+
let newRuleContext = reproduceContext(s.rule, newRule);
267266

268267
// For every declaration in the other ruleset,
269268
const remoteDecls: SimpleDecl[] = [];
@@ -311,7 +310,7 @@ export class ConflictResolver {
311310
let parent = decl.parent.parent;
312311
if (parent) {
313312
let rule = decl.parent as postcss.Rule;
314-
parent.insertAfter(rule, newRule);
313+
parent.insertAfter(rule, newRuleContext);
315314
}
316315
}
317316
}
@@ -440,3 +439,42 @@ export class ConflictResolver {
440439
return sourceRange(this.config, block.stylesheet, blockPath, node);
441440
}
442441
}
442+
443+
interface InstanceOf<T> {
444+
constructor: { new (): T };
445+
}
446+
447+
function shallowClone(node: InstanceOf<postcss.Container>) {
448+
let cloned = new node.constructor();
449+
450+
for (let i in node) {
451+
if (!node.hasOwnProperty(i)) continue;
452+
let value = node[i];
453+
let type = typeof value;
454+
455+
if (i === "parent" && type === "object") {
456+
// skip it
457+
} else if (i === "source") {
458+
cloned[i] = value;
459+
} else if (value instanceof Array) {
460+
// skip it
461+
} else if (type === "object" && value !== null) {
462+
// skip it
463+
} else {
464+
cloned[i] = value;
465+
}
466+
}
467+
468+
return cloned;
469+
}
470+
471+
function reproduceContext(hasContext: postcss.Node, needsContext: postcss.Node): postcss.Node {
472+
if (!hasContext.parent || hasContext.parent.type === "root") {
473+
return needsContext;
474+
}
475+
let parent = hasContext.parent;
476+
// The typings for postcss don't model the nodes as classes so we have to cast through unknown.
477+
let newParent = shallowClone(<InstanceOf<postcss.Container>><unknown>parent);
478+
newParent.append(needsContext);
479+
return reproduceContext(parent, newParent);
480+
}

packages/@css-blocks/core/src/BlockTree/BlockClass.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ export class BlockClass extends Style<BlockClass, Block, Block, Attribute> {
186186
*
187187
* @param optionalRoot The root class is optional on root-level
188188
* Attributes. So when these attributes are being used in conjunction
189-
* with a Attributes, this value is set to true.
189+
* with attributes, this value is set to true.
190190
*/
191191
public asSourceAttributes(optionalRoot = false): Attr[] {
192192
if (!this._sourceAttribute) {

packages/@css-blocks/core/test/resolution-test.ts

+68
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,74 @@ export class BlockInheritance extends BEMProcessor {
430430
});
431431
}
432432

433+
@test "resolves block roots in media queries"() {
434+
let { imports, config } = setupImporting();
435+
imports.registerSource(
436+
"grid.block.css",
437+
`:scope {
438+
width: 1128px;
439+
box-sizing: content-box;
440+
padding: 0 30px;
441+
display: block;
442+
margin: auto;
443+
position: relative;
444+
}
445+
@media (max-width: 1208px) {
446+
:scope {
447+
display: inline-block;
448+
width: calc(100vw - 20px);
449+
box-sizing: border-box;
450+
}
451+
}
452+
@media (max-width: 976px) {
453+
:scope {
454+
padding: 0 18px;
455+
}
456+
}
457+
`,
458+
);
459+
460+
let filename = "header.css";
461+
let inputCSS = `
462+
@block grid from "./grid.block.css";
463+
464+
:scope {
465+
background: blue;
466+
}
467+
468+
.content {
469+
display: flex;
470+
display: resolve("grid");
471+
height: 100%;
472+
}
473+
474+
.content[width="fixed"] {
475+
composes: grid;
476+
}
477+
478+
.content[width="full"] {
479+
margin: resolve("grid");
480+
margin: 0 24px;
481+
}
482+
`;
483+
484+
return this.process(filename, inputCSS, config).then((result) => {
485+
imports.assertImported("grid.block.css");
486+
assert.deepEqual(
487+
result.css.toString(),
488+
".header { background: blue; }\n" +
489+
".header__content { display: flex; height: 100%; }\n" +
490+
".grid.header__content { display: block; }\n" +
491+
"@media (max-width: 1208px) {\n" +
492+
" .grid.header__content { display: inline-block; }\n" +
493+
"}\n" +
494+
".header__content--width-fixed { }\n" +
495+
".header__content--width-full { margin: 0 24px; }\n" +
496+
".grid.header__content--width-full { margin: 0 24px; }\n",
497+
);
498+
});
499+
}
500+
433501
@test "resolves root states"() {
434502
let { imports, config } = setupImporting();
435503
imports.registerSource(

0 commit comments

Comments
 (0)