Skip to content

Commit bfc0c72

Browse files
andruudchromium-wpt-export-bot
authored andcommitted
[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. Change-Id: If9afe0cbb41e7de0acdd781ecfbf6884d677c6f8 Binary-Size: crbug.com/344608183 Bug: 343463516, 361600667 [1] w3c/csswg-drafts#10234
1 parent 23d5b7d commit bfc0c72

6 files changed

+363
-16
lines changed

css/css-cascade/scope-nesting.html

+6-7
Original file line numberDiff line numberDiff line change
@@ -339,16 +339,15 @@
339339
<div>
340340
<style>
341341
.a {
342-
/* We're supposed to prepend :scope to this declaration. If we do that,
343-
then :where() does not matter, since :scope does not gain any
344-
specificity from the enclosing @scope rule. However, if an
345-
implementation incorrectly prepends & instead, then :where() is
346-
needed to avoid the test incorrectly passing due to specificity. */
347-
@scope (:where(&) .b) {
342+
/* The '& .b' selector is wrapped in :where() to prevent a false
343+
positive when the implementation incorrectly wraps
344+
the z-index declaration in a rule with &-behavior
345+
rather than :where(:scope)-behavior. */
346+
@scope (:where(& .b)) {
348347
z-index: 1; /* Should win due to proximity */
349348
}
350349
}
351-
.b { z-index: 2; }
350+
:where(.b) { z-index: 2; }
352351
</style>
353352
<div class=a>
354353
<div class="b x">
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<!DOCTYPE html>
2+
<title>CSS Nesting: CSSNestedDeclarations CSSOM</title>
3+
<link rel="help" href="https://drafts.csswg.org/css-nesting-1/#nested-declarations-rule">
4+
<script src="/resources/testharness.js"></script>
5+
<script src="/resources/testharnessreport.js"></script>
6+
<script>
7+
test(() => {
8+
let s = new CSSStyleSheet();
9+
s.replaceSync(`
10+
.a {
11+
& { --x:1; }
12+
--x:2;
13+
}
14+
`);
15+
assert_equals(s.cssRules.length, 1);
16+
let outer = s.cssRules[0];
17+
assert_equals(outer.cssRules.length, 2);
18+
assert_equals(outer.cssRules[0].cssText, `& { --x: 1; }`);
19+
assert_equals(outer.cssRules[1].cssText, `--x: 2;`);
20+
}, 'Trailing declarations');
21+
22+
test(() => {
23+
let s = new CSSStyleSheet();
24+
s.replaceSync(`
25+
.a {
26+
--a:1;
27+
--b:1;
28+
& { --c:1; }
29+
--d:1;
30+
--e:1;
31+
& { --f:1; }
32+
--g:1;
33+
--h:1;
34+
--i:1;
35+
& { --j:1; }
36+
--k:1;
37+
--l:1;
38+
}
39+
`);
40+
assert_equals(s.cssRules.length, 1);
41+
let outer = s.cssRules[0];
42+
assert_equals(outer.cssRules.length, 6);
43+
assert_equals(outer.cssRules[0].cssText, `& { --c: 1; }`);
44+
assert_equals(outer.cssRules[1].cssText, `--d: 1; --e: 1;`);
45+
assert_equals(outer.cssRules[2].cssText, `& { --f: 1; }`);
46+
assert_equals(outer.cssRules[3].cssText, `--g: 1; --h: 1; --i: 1;`);
47+
assert_equals(outer.cssRules[4].cssText, `& { --j: 1; }`);
48+
assert_equals(outer.cssRules[5].cssText, `--k: 1; --l: 1;`);
49+
}, 'Mixed declarations');
50+
51+
test(() => {
52+
let s = new CSSStyleSheet();
53+
s.replaceSync(`
54+
.a {
55+
& { --x:1; }
56+
--y:2;
57+
--z:3;
58+
}
59+
`);
60+
assert_equals(s.cssRules.length, 1);
61+
let outer = s.cssRules[0];
62+
assert_equals(outer.cssRules.length, 2);
63+
let nested_declarations = outer.cssRules[1];
64+
assert_true(nested_declarations instanceof CSSNestedDeclarations);
65+
assert_equals(nested_declarations.style.length, 2);
66+
assert_equals(nested_declarations.style.getPropertyValue('--x'), '');
67+
assert_equals(nested_declarations.style.getPropertyValue('--y'), '2');
68+
assert_equals(nested_declarations.style.getPropertyValue('--z'), '3');
69+
}, 'CSSNestedDeclarations.style');
70+
71+
test(() => {
72+
let s = new CSSStyleSheet();
73+
s.replaceSync(`
74+
.a {
75+
@media (width > 100px) {
76+
--x:1;
77+
--y:1;
78+
.b { }
79+
--z:1;
80+
}
81+
--w:1;
82+
}
83+
`);
84+
assert_equals(s.cssRules.length, 1);
85+
let outer = s.cssRules[0];
86+
assert_equals(outer.cssRules.length, 2);
87+
88+
// @media
89+
let media = outer.cssRules[0];
90+
assert_equals(media.cssRules.length, 3);
91+
assert_true(media.cssRules[0] instanceof CSSNestedDeclarations);
92+
assert_equals(media.cssRules[0].cssText, `--x: 1; --y: 1;`);
93+
assert_equals(media.cssRules[1].cssText, `& .b { }`);
94+
assert_true(media.cssRules[2] instanceof CSSNestedDeclarations);
95+
assert_equals(media.cssRules[2].cssText, `--z: 1;`);
96+
97+
assert_true(outer.cssRules[1] instanceof CSSNestedDeclarations);
98+
assert_equals(outer.cssRules[1].cssText, `--w: 1;`);
99+
}, 'Nested group rule');
100+
101+
test(() => {
102+
let s = new CSSStyleSheet();
103+
s.replaceSync(`
104+
.a {
105+
@scope (.foo) {
106+
--x:1;
107+
--y:1;
108+
.b { }
109+
--z:1;
110+
}
111+
--w:1;
112+
}
113+
`);
114+
assert_equals(s.cssRules.length, 1);
115+
let outer = s.cssRules[0];
116+
assert_equals(outer.cssRules.length, 2);
117+
118+
// @scope
119+
let scope = outer.cssRules[0];
120+
assert_equals(scope.cssRules.length, 3);
121+
assert_true(scope.cssRules[0] instanceof CSSNestedDeclarations);
122+
assert_equals(scope.cssRules[0].cssText, `--x: 1; --y: 1;`);
123+
assert_equals(scope.cssRules[1].cssText, `.b { }`); // Implicit :scope here.
124+
assert_true(scope.cssRules[2] instanceof CSSNestedDeclarations);
125+
assert_equals(scope.cssRules[2].cssText, `--z: 1;`);
126+
127+
assert_true(outer.cssRules[1] instanceof CSSNestedDeclarations);
128+
assert_equals(outer.cssRules[1].cssText, `--w: 1;`);
129+
}, 'Nested @scope rule');
130+
131+
test(() => {
132+
let s = new CSSStyleSheet();
133+
s.replaceSync(`
134+
a {
135+
& { --x:1; }
136+
width: 100px;
137+
height: 200px;
138+
color:hover {}
139+
--y: 2;
140+
}
141+
`);
142+
assert_equals(s.cssRules.length, 1);
143+
let outer = s.cssRules[0];
144+
assert_equals(outer.cssRules.length, 4);
145+
assert_equals(outer.cssRules[0].cssText, `& { --x: 1; }`);
146+
assert_equals(outer.cssRules[1].cssText, `width: 100px; height: 200px;`);
147+
assert_equals(outer.cssRules[2].cssText, `& color:hover { }`);
148+
assert_equals(outer.cssRules[3].cssText, `--y: 2;`);
149+
}, 'Inner rule starting with an ident');
150+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
<!DOCTYPE html>
2+
<title>CSS Nesting: CSSNestedDeclarations matching</title>
3+
<link rel="help" href="https://drafts.csswg.org/css-nesting-1/#nested-declarations-rule">
4+
<script src="/resources/testharness.js"></script>
5+
<script src="/resources/testharnessreport.js"></script>
6+
7+
<style>
8+
.trailing {
9+
--x: FAIL;
10+
& { --x: FAIL; }
11+
--x: PASS;
12+
}
13+
</style>
14+
<div class=trailing></div>
15+
<script>
16+
test(() => {
17+
let e = document.querySelector('.trailing');
18+
assert_equals(getComputedStyle(e).getPropertyValue('--x'), 'PASS');
19+
}, 'Trailing declarations apply after any preceding rules');
20+
</script>
21+
22+
23+
<style>
24+
.trailing_no_leading {
25+
& { --x: FAIL; }
26+
--x: PASS;
27+
}
28+
</style>
29+
<div class=trailing_no_leading></div>
30+
<script>
31+
test(() => {
32+
let e = document.querySelector('.trailing_no_leading');
33+
assert_equals(getComputedStyle(e).getPropertyValue('--x'), 'PASS');
34+
}, 'Trailing declarations apply after any preceding rules (no leading)');
35+
</script>
36+
37+
38+
<style>
39+
.trailing_multiple {
40+
--x: FAIL;
41+
--y: FAIL;
42+
--z: FAIL;
43+
--w: FAIL;
44+
& { --x: FAIL; }
45+
--x: PASS;
46+
--y: PASS;
47+
& { --z: FAIL; }
48+
--z: PASS;
49+
--w: PASS;
50+
}
51+
</style>
52+
<div class=trailing_multiple></div>
53+
<script>
54+
test(() => {
55+
let e = document.querySelector('.trailing_multiple');
56+
let s = getComputedStyle(e);
57+
assert_equals(s.getPropertyValue('--x'), 'PASS');
58+
assert_equals(s.getPropertyValue('--y'), 'PASS');
59+
assert_equals(s.getPropertyValue('--z'), 'PASS');
60+
assert_equals(s.getPropertyValue('--w'), 'PASS');
61+
}, 'Trailing declarations apply after any preceding rules (multiple)');
62+
</script>
63+
64+
65+
<style>
66+
.trailing_specificity {
67+
--x: FAIL;
68+
:is(&, div.nomatch2) { --x: PASS; } /* Specificity: (0, 1, 1) */
69+
--x: FAIL; /* Specificity: (0, 1, 0) */
70+
}
71+
</style>
72+
<div class=trailing_specificity></div>
73+
<script>
74+
test(() => {
75+
let e = document.querySelector('.trailing_specificity');
76+
assert_equals(getComputedStyle(e).getPropertyValue('--x'), 'PASS');
77+
}, 'Nested declarations rule has same specificity as outer selector');
78+
</script>
79+
80+
81+
<style>
82+
#nomatch, .specificity_top_level {
83+
--x: FAIL;
84+
:is(&, div.nomatch2) { --x: PASS; } /* Specificity: (0, 1, 1) */
85+
--x: FAIL; /* Specificity: (0, 1, 0). In particular, this does not have
86+
specificity like :is(#nomatch, .specificity_top_level). */
87+
}
88+
</style>
89+
<div class=specificity_top_level></div>
90+
<script>
91+
test(() => {
92+
let e = document.querySelector('.specificity_top_level');
93+
assert_equals(getComputedStyle(e).getPropertyValue('--x'), 'PASS');
94+
}, 'Nested declarations rule has top-level specificity behavior');
95+
</script>
96+
97+
98+
<style>
99+
#nomatch, .specificity_top_level_max, div.specificity_top_level_max {
100+
--x: FAIL;
101+
:is(:where(&), div.nomatch2) { --x: FAIL; } /* Specificity: (0, 1, 1) */
102+
--x: PASS; /* Specificity: (0, 1, 1) (for div.specificity_top_level_max) */
103+
}
104+
</style>
105+
<div class=specificity_top_level_max></div>
106+
<script>
107+
test(() => {
108+
let e = document.querySelector('.specificity_top_level_max');
109+
assert_equals(getComputedStyle(e).getPropertyValue('--x'), 'PASS');
110+
}, 'Nested declarations rule has top-level specificity behavior (max matching)');
111+
</script>
112+
113+
<style>
114+
.nested_pseudo::after {
115+
--x: FAIL;
116+
@media (width > 0px) {
117+
--x: PASS;
118+
}
119+
}
120+
</style>
121+
<div class=nested_pseudo></div>
122+
<script>
123+
test(() => {
124+
let e = document.querySelector('.nested_pseudo');
125+
assert_equals(getComputedStyle(e, '::after').getPropertyValue('--x'), 'PASS');
126+
}, 'Bare declartaion in nested grouping rule can match pseudo-element');
127+
</script>
128+
129+
<style>
130+
#nomatch, .nested_group_rule {
131+
--x: FAIL;
132+
@media (width > 0px) {
133+
--x: FAIL; /* Specificity: (0, 1, 0) */
134+
}
135+
--x: PASS;
136+
}
137+
</style>
138+
<div class=nested_group_rule></div>
139+
<script>
140+
test(() => {
141+
let e = document.querySelector('.nested_group_rule');
142+
assert_equals(getComputedStyle(e).getPropertyValue('--x'), 'PASS');
143+
}, 'Nested group rules have top-level specificity behavior');
144+
</script>
145+
146+
147+
<style>
148+
.nested_scope_rule {
149+
div:where(&) { /* Specificity: (0, 0, 1) */
150+
--x: PASS;
151+
}
152+
@scope (&) {
153+
--x: FAIL; /* Specificity: (0, 0, 0) */
154+
}
155+
}
156+
</style>
157+
<div class=nested_scope_rule></div>
158+
<script>
159+
test(() => {
160+
let e = document.querySelector('.nested_scope_rule');
161+
assert_equals(getComputedStyle(e).getPropertyValue('--x'), 'PASS');
162+
}, 'Nested @scope rules behave like :where(:scope)');
163+
</script>
164+
165+
<style id=set_parent_selector_text_style>
166+
.set_parent_selector_text {
167+
div {
168+
color: red;
169+
}
170+
.a1 {
171+
& { color: green };
172+
}
173+
}
174+
</style>
175+
<div class=set_parent_selector_text>
176+
<div class=a1>A1</div>
177+
<div class=a2>A2</div>
178+
</div>
179+
<script>
180+
test(() => {
181+
let a1 = document.querySelector('.set_parent_selector_text > .a1');
182+
let a2 = document.querySelector('.set_parent_selector_text > .a2');
183+
assert_equals(getComputedStyle(a1).color, 'rgb(0, 128, 0)');
184+
assert_equals(getComputedStyle(a2).color, 'rgb(255, 0, 0)');
185+
186+
let rules = set_parent_selector_text_style.sheet.cssRules;
187+
assert_equals(rules.length, 1);
188+
assert_equals(rules[0].cssRules.length, 2);
189+
190+
let a_rule = rules[0].cssRules[1];
191+
assert_equals(a_rule.selectorText, '& .a1');
192+
a_rule.selectorText = '.a2';
193+
194+
assert_equals(getComputedStyle(a1).color, 'rgb(255, 0, 0)');
195+
assert_equals(getComputedStyle(a2).color, 'rgb(0, 128, 0)');
196+
}, 'Nested declarations rule responds to parent selector text change');
197+
</script>

css/css-nesting/nesting-basic.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@
6868

6969
.test-10 {
7070
& {
71-
background-color: green;
71+
background-color: red;
7272
}
73-
background-color: red;
73+
background-color: green;
7474
}
7575

7676
.test-11 {

0 commit comments

Comments
 (0)