-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[css-nesting] Implement CSSNestedDeclarations behind a flag
The CSSWG recently resolved to change how "bare" declarations work when mixed with nested style rules. Previously, any declaration following a nested style rule would be "shifted up" to join the leading declarations, but as of the work leading up to Issue 10234 [1], such declarations now instead remain in place (wrapped in a CSSNestedDeclarations rule). This CL implements this rule, as well as the parser changes needed to produce those rules during ConsumeDeclarationList. The parsing behavior is similar to the behavior previously seen for emitting signalling/invisible rules (removed in CL:5593832), although naturally without any signalling, nor any invisibility. Per spec, CSSNestedDeclarations is a kind of style rule that matches exactly what its parent style rule matches, and with the same specificity behavior. This is different from the '&' pseudo-class, which uses the maximum specificity across its arguments, and can't match pseudo-elements. This CL implements this via an inner StyleRule, held by the CSSNestedDeclarations rule. This inner StyleRule can't be observed via CSSOM. It exists primarily to be able to bucket the rule normally on RuleSet. [1] w3c/csswg-drafts#10234 Change-Id: If9afe0cbb41e7de0acdd781ecfbf6884d677c6f8 Binary-Size: crbug.com/344608183 Bug: 343463516, 361600667 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5594117 Reviewed-by: Steinar H Gunderson <sesse@chromium.org> Commit-Queue: Anders Hartvoll Ruud <andruud@chromium.org> Reviewed-by: Philip Pfaffe <pfaffe@chromium.org> Cr-Commit-Position: refs/heads/main@{#1347266}
- Loading branch information
1 parent
957bc89
commit f4e1788
Showing
6 changed files
with
363 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
<!DOCTYPE html> | ||
<title>CSS Nesting: CSSNestedDeclarations CSSOM</title> | ||
<link rel="help" href="https://drafts.csswg.org/css-nesting-1/#nested-declarations-rule"> | ||
<script src="/resources/testharness.js"></script> | ||
<script src="/resources/testharnessreport.js"></script> | ||
<script> | ||
test(() => { | ||
let s = new CSSStyleSheet(); | ||
s.replaceSync(` | ||
.a { | ||
& { --x:1; } | ||
--x:2; | ||
} | ||
`); | ||
assert_equals(s.cssRules.length, 1); | ||
let outer = s.cssRules[0]; | ||
assert_equals(outer.cssRules.length, 2); | ||
assert_equals(outer.cssRules[0].cssText, `& { --x: 1; }`); | ||
assert_equals(outer.cssRules[1].cssText, `--x: 2;`); | ||
}, 'Trailing declarations'); | ||
|
||
test(() => { | ||
let s = new CSSStyleSheet(); | ||
s.replaceSync(` | ||
.a { | ||
--a:1; | ||
--b:1; | ||
& { --c:1; } | ||
--d:1; | ||
--e:1; | ||
& { --f:1; } | ||
--g:1; | ||
--h:1; | ||
--i:1; | ||
& { --j:1; } | ||
--k:1; | ||
--l:1; | ||
} | ||
`); | ||
assert_equals(s.cssRules.length, 1); | ||
let outer = s.cssRules[0]; | ||
assert_equals(outer.cssRules.length, 6); | ||
assert_equals(outer.cssRules[0].cssText, `& { --c: 1; }`); | ||
assert_equals(outer.cssRules[1].cssText, `--d: 1; --e: 1;`); | ||
assert_equals(outer.cssRules[2].cssText, `& { --f: 1; }`); | ||
assert_equals(outer.cssRules[3].cssText, `--g: 1; --h: 1; --i: 1;`); | ||
assert_equals(outer.cssRules[4].cssText, `& { --j: 1; }`); | ||
assert_equals(outer.cssRules[5].cssText, `--k: 1; --l: 1;`); | ||
}, 'Mixed declarations'); | ||
|
||
test(() => { | ||
let s = new CSSStyleSheet(); | ||
s.replaceSync(` | ||
.a { | ||
& { --x:1; } | ||
--y:2; | ||
--z:3; | ||
} | ||
`); | ||
assert_equals(s.cssRules.length, 1); | ||
let outer = s.cssRules[0]; | ||
assert_equals(outer.cssRules.length, 2); | ||
let nested_declarations = outer.cssRules[1]; | ||
assert_true(nested_declarations instanceof CSSNestedDeclarations); | ||
assert_equals(nested_declarations.style.length, 2); | ||
assert_equals(nested_declarations.style.getPropertyValue('--x'), ''); | ||
assert_equals(nested_declarations.style.getPropertyValue('--y'), '2'); | ||
assert_equals(nested_declarations.style.getPropertyValue('--z'), '3'); | ||
}, 'CSSNestedDeclarations.style'); | ||
|
||
test(() => { | ||
let s = new CSSStyleSheet(); | ||
s.replaceSync(` | ||
.a { | ||
@media (width > 100px) { | ||
--x:1; | ||
--y:1; | ||
.b { } | ||
--z:1; | ||
} | ||
--w:1; | ||
} | ||
`); | ||
assert_equals(s.cssRules.length, 1); | ||
let outer = s.cssRules[0]; | ||
assert_equals(outer.cssRules.length, 2); | ||
|
||
// @media | ||
let media = outer.cssRules[0]; | ||
assert_equals(media.cssRules.length, 3); | ||
assert_true(media.cssRules[0] instanceof CSSNestedDeclarations); | ||
assert_equals(media.cssRules[0].cssText, `--x: 1; --y: 1;`); | ||
assert_equals(media.cssRules[1].cssText, `& .b { }`); | ||
assert_true(media.cssRules[2] instanceof CSSNestedDeclarations); | ||
assert_equals(media.cssRules[2].cssText, `--z: 1;`); | ||
|
||
assert_true(outer.cssRules[1] instanceof CSSNestedDeclarations); | ||
assert_equals(outer.cssRules[1].cssText, `--w: 1;`); | ||
}, 'Nested group rule'); | ||
|
||
test(() => { | ||
let s = new CSSStyleSheet(); | ||
s.replaceSync(` | ||
.a { | ||
@scope (.foo) { | ||
--x:1; | ||
--y:1; | ||
.b { } | ||
--z:1; | ||
} | ||
--w:1; | ||
} | ||
`); | ||
assert_equals(s.cssRules.length, 1); | ||
let outer = s.cssRules[0]; | ||
assert_equals(outer.cssRules.length, 2); | ||
|
||
// @scope | ||
let scope = outer.cssRules[0]; | ||
assert_equals(scope.cssRules.length, 3); | ||
assert_true(scope.cssRules[0] instanceof CSSNestedDeclarations); | ||
assert_equals(scope.cssRules[0].cssText, `--x: 1; --y: 1;`); | ||
assert_equals(scope.cssRules[1].cssText, `.b { }`); // Implicit :scope here. | ||
assert_true(scope.cssRules[2] instanceof CSSNestedDeclarations); | ||
assert_equals(scope.cssRules[2].cssText, `--z: 1;`); | ||
|
||
assert_true(outer.cssRules[1] instanceof CSSNestedDeclarations); | ||
assert_equals(outer.cssRules[1].cssText, `--w: 1;`); | ||
}, 'Nested @scope rule'); | ||
|
||
test(() => { | ||
let s = new CSSStyleSheet(); | ||
s.replaceSync(` | ||
a { | ||
& { --x:1; } | ||
width: 100px; | ||
height: 200px; | ||
color:hover {} | ||
--y: 2; | ||
} | ||
`); | ||
assert_equals(s.cssRules.length, 1); | ||
let outer = s.cssRules[0]; | ||
assert_equals(outer.cssRules.length, 4); | ||
assert_equals(outer.cssRules[0].cssText, `& { --x: 1; }`); | ||
assert_equals(outer.cssRules[1].cssText, `width: 100px; height: 200px;`); | ||
assert_equals(outer.cssRules[2].cssText, `& color:hover { }`); | ||
assert_equals(outer.cssRules[3].cssText, `--y: 2;`); | ||
}, 'Inner rule starting with an ident'); | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
<!DOCTYPE html> | ||
<title>CSS Nesting: CSSNestedDeclarations matching</title> | ||
<link rel="help" href="https://drafts.csswg.org/css-nesting-1/#nested-declarations-rule"> | ||
<script src="/resources/testharness.js"></script> | ||
<script src="/resources/testharnessreport.js"></script> | ||
|
||
<style> | ||
.trailing { | ||
--x: FAIL; | ||
& { --x: FAIL; } | ||
--x: PASS; | ||
} | ||
</style> | ||
<div class=trailing></div> | ||
<script> | ||
test(() => { | ||
let e = document.querySelector('.trailing'); | ||
assert_equals(getComputedStyle(e).getPropertyValue('--x'), 'PASS'); | ||
}, 'Trailing declarations apply after any preceding rules'); | ||
</script> | ||
|
||
|
||
<style> | ||
.trailing_no_leading { | ||
& { --x: FAIL; } | ||
--x: PASS; | ||
} | ||
</style> | ||
<div class=trailing_no_leading></div> | ||
<script> | ||
test(() => { | ||
let e = document.querySelector('.trailing_no_leading'); | ||
assert_equals(getComputedStyle(e).getPropertyValue('--x'), 'PASS'); | ||
}, 'Trailing declarations apply after any preceding rules (no leading)'); | ||
</script> | ||
|
||
|
||
<style> | ||
.trailing_multiple { | ||
--x: FAIL; | ||
--y: FAIL; | ||
--z: FAIL; | ||
--w: FAIL; | ||
& { --x: FAIL; } | ||
--x: PASS; | ||
--y: PASS; | ||
& { --z: FAIL; } | ||
--z: PASS; | ||
--w: PASS; | ||
} | ||
</style> | ||
<div class=trailing_multiple></div> | ||
<script> | ||
test(() => { | ||
let e = document.querySelector('.trailing_multiple'); | ||
let s = getComputedStyle(e); | ||
assert_equals(s.getPropertyValue('--x'), 'PASS'); | ||
assert_equals(s.getPropertyValue('--y'), 'PASS'); | ||
assert_equals(s.getPropertyValue('--z'), 'PASS'); | ||
assert_equals(s.getPropertyValue('--w'), 'PASS'); | ||
}, 'Trailing declarations apply after any preceding rules (multiple)'); | ||
</script> | ||
|
||
|
||
<style> | ||
.trailing_specificity { | ||
--x: FAIL; | ||
:is(&, div.nomatch2) { --x: PASS; } /* Specificity: (0, 1, 1) */ | ||
--x: FAIL; /* Specificity: (0, 1, 0) */ | ||
} | ||
</style> | ||
<div class=trailing_specificity></div> | ||
<script> | ||
test(() => { | ||
let e = document.querySelector('.trailing_specificity'); | ||
assert_equals(getComputedStyle(e).getPropertyValue('--x'), 'PASS'); | ||
}, 'Nested declarations rule has same specificity as outer selector'); | ||
</script> | ||
|
||
|
||
<style> | ||
#nomatch, .specificity_top_level { | ||
--x: FAIL; | ||
:is(&, div.nomatch2) { --x: PASS; } /* Specificity: (0, 1, 1) */ | ||
--x: FAIL; /* Specificity: (0, 1, 0). In particular, this does not have | ||
specificity like :is(#nomatch, .specificity_top_level). */ | ||
} | ||
</style> | ||
<div class=specificity_top_level></div> | ||
<script> | ||
test(() => { | ||
let e = document.querySelector('.specificity_top_level'); | ||
assert_equals(getComputedStyle(e).getPropertyValue('--x'), 'PASS'); | ||
}, 'Nested declarations rule has top-level specificity behavior'); | ||
</script> | ||
|
||
|
||
<style> | ||
#nomatch, .specificity_top_level_max, div.specificity_top_level_max { | ||
--x: FAIL; | ||
:is(:where(&), div.nomatch2) { --x: FAIL; } /* Specificity: (0, 1, 1) */ | ||
--x: PASS; /* Specificity: (0, 1, 1) (for div.specificity_top_level_max) */ | ||
} | ||
</style> | ||
<div class=specificity_top_level_max></div> | ||
<script> | ||
test(() => { | ||
let e = document.querySelector('.specificity_top_level_max'); | ||
assert_equals(getComputedStyle(e).getPropertyValue('--x'), 'PASS'); | ||
}, 'Nested declarations rule has top-level specificity behavior (max matching)'); | ||
</script> | ||
|
||
<style> | ||
.nested_pseudo::after { | ||
--x: FAIL; | ||
@media (width > 0px) { | ||
--x: PASS; | ||
} | ||
} | ||
</style> | ||
<div class=nested_pseudo></div> | ||
<script> | ||
test(() => { | ||
let e = document.querySelector('.nested_pseudo'); | ||
assert_equals(getComputedStyle(e, '::after').getPropertyValue('--x'), 'PASS'); | ||
}, 'Bare declartaion in nested grouping rule can match pseudo-element'); | ||
</script> | ||
|
||
<style> | ||
#nomatch, .nested_group_rule { | ||
--x: FAIL; | ||
@media (width > 0px) { | ||
--x: FAIL; /* Specificity: (0, 1, 0) */ | ||
} | ||
--x: PASS; | ||
} | ||
</style> | ||
<div class=nested_group_rule></div> | ||
<script> | ||
test(() => { | ||
let e = document.querySelector('.nested_group_rule'); | ||
assert_equals(getComputedStyle(e).getPropertyValue('--x'), 'PASS'); | ||
}, 'Nested group rules have top-level specificity behavior'); | ||
</script> | ||
|
||
|
||
<style> | ||
.nested_scope_rule { | ||
div:where(&) { /* Specificity: (0, 0, 1) */ | ||
--x: PASS; | ||
} | ||
@scope (&) { | ||
--x: FAIL; /* Specificity: (0, 0, 0) */ | ||
} | ||
} | ||
</style> | ||
<div class=nested_scope_rule></div> | ||
<script> | ||
test(() => { | ||
let e = document.querySelector('.nested_scope_rule'); | ||
assert_equals(getComputedStyle(e).getPropertyValue('--x'), 'PASS'); | ||
}, 'Nested @scope rules behave like :where(:scope)'); | ||
</script> | ||
|
||
<style id=set_parent_selector_text_style> | ||
.set_parent_selector_text { | ||
div { | ||
color: red; | ||
} | ||
.a1 { | ||
& { color: green }; | ||
} | ||
} | ||
</style> | ||
<div class=set_parent_selector_text> | ||
<div class=a1>A1</div> | ||
<div class=a2>A2</div> | ||
</div> | ||
<script> | ||
test(() => { | ||
let a1 = document.querySelector('.set_parent_selector_text > .a1'); | ||
let a2 = document.querySelector('.set_parent_selector_text > .a2'); | ||
assert_equals(getComputedStyle(a1).color, 'rgb(0, 128, 0)'); | ||
assert_equals(getComputedStyle(a2).color, 'rgb(255, 0, 0)'); | ||
|
||
let rules = set_parent_selector_text_style.sheet.cssRules; | ||
assert_equals(rules.length, 1); | ||
assert_equals(rules[0].cssRules.length, 2); | ||
|
||
let a_rule = rules[0].cssRules[1]; | ||
assert_equals(a_rule.selectorText, '& .a1'); | ||
a_rule.selectorText = '.a2'; | ||
|
||
assert_equals(getComputedStyle(a1).color, 'rgb(255, 0, 0)'); | ||
assert_equals(getComputedStyle(a2).color, 'rgb(0, 128, 0)'); | ||
}, 'Nested declarations rule responds to parent selector text change'); | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.