scope-nesting.html (20726B)
1 <!DOCTYPE html> 2 <title>@scope - nesting (&)</title> 3 <link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scope-atrule"> 4 <link rel="help" href="https://drafts.csswg.org/css-nesting-1/#nest-selector"> 5 <link rel="help" href="https://github.com/w3c/csswg-drafts/issues/9740"> 6 <script src="/resources/testharness.js"></script> 7 <script src="/resources/testharnessreport.js"></script> 8 <main id=main></main> 9 10 <template id=test_nest_scope_end> 11 <div> 12 <style> 13 /* (& > .b) behaves like (:where(:scope) > .b), due & mapping to :where(:scope).*/ 14 @scope (.a) to (& > .b) { 15 :scope { z-index:1; } 16 } 17 18 /* Should not match, since <scope-end> refers to the scope itself. */ 19 @scope (.a) to (.b&) { 20 :scope { z-index:42; } 21 } 22 </style> 23 <div class="a b"> 24 <div class=b> 25 <div id=below></div> 26 </div> 27 </div> 28 </div> 29 <div id=outside></div> 30 </template> 31 <script> 32 test((t) => { 33 t.add_cleanup(() => main.replaceChildren()); 34 main.append(test_nest_scope_end.content.cloneNode(true)); 35 let a = document.querySelector('.a'); 36 let b = document.querySelector('.a > .b'); 37 assert_equals(getComputedStyle(a).zIndex, '1'); 38 assert_equals(getComputedStyle(b).zIndex, 'auto'); 39 assert_equals(getComputedStyle(below).zIndex, 'auto'); 40 assert_equals(getComputedStyle(outside).zIndex, 'auto'); 41 }, 'Nesting-selector in <scope-end>'); 42 </script> 43 44 <template id=test_nest_scope_end_implicit_scope> 45 <div> 46 <style> 47 /* (.b) behaves like (:scope .b), due :scope being prepended 48 implicitly. */ 49 @scope (.a) to (.b) { 50 :scope { z-index:1; } 51 } 52 53 /* Should not match, since <scope-end> refers to the scope itself. */ 54 @scope (.a) to (.b:scope) { 55 :scope { z-index:42; } 56 } 57 </style> 58 <div class="a b"> 59 <div class=b> 60 <div id=below></div> 61 </div> 62 </div> 63 </div> 64 <div id=outside></div> 65 </template> 66 <script> 67 test((t) => { 68 t.add_cleanup(() => main.replaceChildren()); 69 main.append(test_nest_scope_end_implicit_scope.content.cloneNode(true)); 70 let a = document.querySelector('.a'); 71 let b = document.querySelector('.a > .b'); 72 assert_equals(getComputedStyle(a).zIndex, '1'); 73 assert_equals(getComputedStyle(b).zIndex, 'auto'); 74 assert_equals(getComputedStyle(below).zIndex, 'auto'); 75 assert_equals(getComputedStyle(outside).zIndex, 'auto'); 76 }, 'Implicit :scope in <scope-end>'); 77 </script> 78 79 <template id=test_relative_selector_scope_end> 80 <div> 81 <style> 82 @scope (.a) to (> .b) { 83 *, :scope { z-index:1; } 84 } 85 </style> 86 <div class="a b"> 87 <div class=b> 88 <div id=below></div> 89 </div> 90 </div> 91 </div> 92 <div id=outside></div> 93 </template> 94 <script> 95 test((t) => { 96 t.add_cleanup(() => main.replaceChildren()); 97 main.append(test_relative_selector_scope_end.content.cloneNode(true)); 98 let a = document.querySelector('.a'); 99 let b = document.querySelector('.a > .b'); 100 assert_equals(getComputedStyle(a).zIndex, '1'); 101 assert_equals(getComputedStyle(b).zIndex, 'auto'); 102 assert_equals(getComputedStyle(below).zIndex, 'auto'); 103 assert_equals(getComputedStyle(outside).zIndex, 'auto'); 104 }, 'Relative selectors in <scope-end>'); 105 </script> 106 107 <template id=test_inner_nest> 108 <div> 109 <style> 110 @scope (#div) { 111 & { 112 z-index: 1; 113 & { 114 z-index: 2; 115 } 116 } 117 } 118 </style> 119 <div id=div></div> 120 </div> 121 </template> 122 <script> 123 test((t) => { 124 t.add_cleanup(() => main.replaceChildren()); 125 main.append(test_inner_nest.content.cloneNode(true)); 126 127 assert_equals(getComputedStyle(div).zIndex, '2'); 128 }, 'Nested nesting-selectors within scope\'s <stylesheet> select inclusive descendants of the scope root'); 129 </script> 130 131 <template id=test_parent_in_pseudo_scope> 132 <div> 133 <style> 134 @scope (#div) { 135 :scope { 136 z-index: 1; 137 & { 138 z-index: 2; 139 } 140 } 141 } 142 </style> 143 <div id=div></div> 144 </div> 145 </template> 146 <script> 147 test((t) => { 148 t.add_cleanup(() => main.replaceChildren()); 149 main.append(test_parent_in_pseudo_scope.content.cloneNode(true)); 150 151 assert_equals(getComputedStyle(div).zIndex, '2'); 152 }, 'Nesting-selector within :scope rule'); 153 </script> 154 155 <template id=test_parent_in_pseudo_scope_double> 156 <div> 157 <style> 158 @scope (#div) { 159 :scope { 160 z-index: 1; 161 & { 162 & { 163 z-index: 2; 164 } 165 } 166 } 167 } 168 </style> 169 <div id=div></div> 170 </div> 171 </template> 172 <script> 173 test((t) => { 174 t.add_cleanup(() => main.replaceChildren()); 175 main.append(test_parent_in_pseudo_scope_double.content.cloneNode(true)); 176 177 assert_equals(getComputedStyle(div).zIndex, '2'); 178 }, 'Nesting-selector within :scope rule (double nested)'); 179 </script> 180 181 <template id=test_scope_within_style_rule> 182 <div> 183 <style> 184 .a { 185 @scope (.b) { 186 .c { z-index: 1; } 187 } 188 } 189 </style> 190 <div class=a> 191 <div class=b> 192 <div class=c> 193 </div> 194 </div> 195 <div id=out_of_scope class=c> 196 </div> 197 </div> 198 </div> 199 </template> 200 <script> 201 test((t) => { 202 t.add_cleanup(() => main.replaceChildren()); 203 main.append(test_scope_within_style_rule.content.cloneNode(true)); 204 205 let c = document.querySelector('.c'); 206 assert_equals(getComputedStyle(c).zIndex, '1'); 207 assert_equals(getComputedStyle(out_of_scope).zIndex, 'auto'); 208 }, '@scope nested within style rule'); 209 </script> 210 211 <template id=test_parent_pseudo_in_nested_scope_start> 212 <div> 213 <style> 214 .a { 215 @scope (&.b) { 216 :scope { z-index: 1; } 217 } 218 } 219 </style> 220 <div class=a></div> 221 <div class=b></div> 222 <div class="a b"></div> 223 </div> 224 </template> 225 <script> 226 test((t) => { 227 t.add_cleanup(() => main.replaceChildren()); 228 main.append(test_parent_pseudo_in_nested_scope_start.content.cloneNode(true)); 229 230 let a = document.querySelector('.a:not(.b)'); 231 let b = document.querySelector('.b:not(.a)'); 232 let ab = document.querySelector('.a.b'); 233 assert_equals(getComputedStyle(a).zIndex, 'auto'); 234 assert_equals(getComputedStyle(b).zIndex, 'auto'); 235 assert_equals(getComputedStyle(ab).zIndex, '1'); 236 }, 'Parent pseudo class within scope-start'); 237 </script> 238 239 240 <template id=test_parent_pseudo_in_nested_scope_body> 241 <div> 242 <style> 243 .a { 244 @scope (.b) { 245 /* The & points to <scope-start>, which contains an implicit & 246 which points to .a. */ 247 &.c { z-index: 1; } 248 } 249 } 250 </style> 251 <div class=a> 252 <div class=b> 253 <div class="c"></div> 254 <div class="a c"></div> 255 <div class="a b c" matching></div> 256 </div> 257 </div> 258 <div> 259 <div class=a></div> 260 <div class=b></div> 261 <div class=c></div> 262 <div class="a b"></div> 263 <div class="a c"></div> 264 <div class="b c"></div> 265 </div> 266 </div> 267 </template> 268 <script> 269 test((t) => { 270 t.add_cleanup(() => main.replaceChildren()); 271 main.append(test_parent_pseudo_in_nested_scope_body.content.cloneNode(true)); 272 273 let matching = main.querySelectorAll("div[matching]"); 274 let non_matching = main.querySelectorAll("div:not([matching])"); 275 276 for (let m of matching) { 277 assert_equals(getComputedStyle(m).zIndex, '1', `matching: ${m.nodeName}${m.className}`); 278 } 279 for (let m of non_matching) { 280 assert_equals(getComputedStyle(m).zIndex, 'auto', `non-matching: ${m.nodeName}${m.className}`); 281 } 282 }, 'Parent pseudo class within body of nested @scope'); 283 </script> 284 285 <template id=test_direct_declarations_in_nested_scope> 286 <div> 287 <style> 288 .a { 289 @scope (.b) { 290 z-index: 1; 291 } 292 } 293 </style> 294 <div class=a> 295 <div class=b> 296 <div class="c"></div> 297 </div> 298 </div> 299 </div> 300 </template> 301 <script> 302 test((t) => { 303 t.add_cleanup(() => main.replaceChildren()); 304 main.append(test_direct_declarations_in_nested_scope.content.cloneNode(true)); 305 306 let a = document.querySelector('.a'); 307 let b = document.querySelector('.b'); 308 let c = document.querySelector('.c'); 309 assert_equals(getComputedStyle(a).zIndex, 'auto'); 310 assert_equals(getComputedStyle(b).zIndex, '1'); 311 assert_equals(getComputedStyle(c).zIndex, 'auto'); 312 }, 'Implicit rule within nested @scope '); 313 </script> 314 315 <template id=test_direct_declarations_in_nested_scope_proximity> 316 <div> 317 <style> 318 .a { 319 /* The '& .b' selector is wrapped in :where() to prevent a false 320 positive when the implementation incorrectly wraps 321 the z-index declaration in a rule with &-behavior 322 rather than :where(:scope)-behavior. */ 323 @scope (:where(& .b)) { 324 z-index: 1; /* Should win due to proximity */ 325 } 326 } 327 :where(.b) { z-index: 2; } 328 </style> 329 <div class=a> 330 <div class="b x"> 331 <div class=c> 332 </div> 333 </div> 334 </div> 335 </div> 336 </template> 337 <script> 338 test((t) => { 339 t.add_cleanup(() => main.replaceChildren()); 340 main.append(test_direct_declarations_in_nested_scope_proximity.content.cloneNode(true)); 341 342 let a = document.querySelector('.a'); 343 let b = document.querySelector('.b'); 344 let c = document.querySelector('.c'); 345 assert_equals(getComputedStyle(a).zIndex, 'auto'); 346 assert_equals(getComputedStyle(b).zIndex, '1'); 347 assert_equals(getComputedStyle(c).zIndex, 'auto'); 348 }, 'Implicit rule within nested @scope (proximity)'); 349 </script> 350 351 <template id=test_nested_scope_inside_an_is> 352 <div> 353 <style> 354 @scope (.a) { 355 .b { 356 /* When nesting, because we’re inside a defined scope, 357 the `:scope` should reference the scoping root node properly, and 358 check for the presence of an extra class on it, essentially 359 being equal to `:scope.x .b { z-index: 1 }`. */ 360 &:is(:scope.x *) { 361 z-index: 1; 362 } 363 /* This should not match, as we have a defined scope, and should 364 not skip to the root. */ 365 &:is(:root:scope *) { 366 z-index: 2; 367 } 368 } 369 /* The nested case can be though of the following when expanded: */ 370 .c:is(:scope.x *) { 371 z-index: 3; 372 } 373 } 374 </style> 375 <div class="b"> 376 </div> 377 <div class="a x"> 378 <div class="b"> 379 </div> 380 <div class="c"> 381 </div> 382 </div> 383 </div> 384 </template> 385 <script> 386 test((t) => { 387 t.add_cleanup(() => main.replaceChildren()); 388 main.append(test_nested_scope_inside_an_is.content.cloneNode(true)); 389 390 let b_outside = document.querySelector('.b'); 391 let b_inside = document.querySelector('.a .b'); 392 let c = document.querySelector('.c'); 393 assert_equals(getComputedStyle(b_outside).zIndex, 'auto'); 394 assert_equals(getComputedStyle(b_inside).zIndex, '1'); 395 assert_equals(getComputedStyle(c).zIndex, '3'); 396 }, 'Nested :scope inside an :is'); 397 </script> 398 399 <template id=test_nested_scope_pseudo> 400 <div> 401 <style> 402 @scope (.b) { 403 .a:not(:scope) { 404 & :scope { 405 z-index: 1; 406 } 407 } 408 } 409 </style> 410 <div class="b"> 411 </div> 412 <div class="a"> 413 <div class="b"> 414 </div> 415 </div> 416 </div> 417 </template> 418 <script> 419 test((t) => { 420 t.add_cleanup(() => main.replaceChildren()); 421 main.append(test_nested_scope_pseudo.content.cloneNode(true)); 422 423 let b_outside = document.querySelector('.b'); 424 let b_inside = document.querySelector('.a .b'); 425 assert_equals(getComputedStyle(b_outside).zIndex, 'auto'); 426 assert_equals(getComputedStyle(b_inside).zIndex, '1'); 427 }, ':scope within nested and scoped rule'); 428 </script> 429 430 <template id=test_nested_scope_pseudo_implied> 431 <div> 432 <style> 433 @scope (.b) { 434 .a:not(:scope) { 435 :scope { /* & implied */ 436 z-index: 1; 437 } 438 } 439 } 440 </style> 441 <div class="b"> 442 </div> 443 <div class="a"> 444 <div class="b"> 445 </div> 446 </div> 447 </div> 448 </template> 449 <script> 450 test((t) => { 451 t.add_cleanup(() => main.replaceChildren()); 452 main.append(test_nested_scope_pseudo_implied.content.cloneNode(true)); 453 454 let b_outside = document.querySelector('.b'); 455 let b_inside = document.querySelector('.a .b'); 456 assert_equals(getComputedStyle(b_outside).zIndex, 'auto'); 457 assert_equals(getComputedStyle(b_inside).zIndex, '1'); 458 }, ':scope within nested and scoped rule (implied &)'); 459 </script> 460 461 <template id=test_nested_scope_pseudo_relative> 462 <div> 463 <style> 464 @scope (.b) { 465 .a:not(:scope) { 466 > :scope { /* & implied */ 467 z-index: 1; 468 } 469 } 470 } 471 </style> 472 <div class="b"> 473 </div> 474 <div class="a"> 475 <div class="b"> 476 </div> 477 </div> 478 </div> 479 </template> 480 <script> 481 test((t) => { 482 t.add_cleanup(() => main.replaceChildren()); 483 main.append(test_nested_scope_pseudo_relative.content.cloneNode(true)); 484 485 let b_outside = document.querySelector('.b'); 486 let b_inside = document.querySelector('.a .b'); 487 assert_equals(getComputedStyle(b_outside).zIndex, 'auto'); 488 assert_equals(getComputedStyle(b_inside).zIndex, '1'); 489 }, ':scope within nested and scoped rule (relative)'); 490 </script> 491 492 <template id=test_scoped_nested_group_rule> 493 <div> 494 <style> 495 @scope (.a) { 496 .b:not(:scope) { 497 @media (width) { 498 z-index: 1; 499 } 500 } 501 } 502 </style> 503 <div class="b"> 504 </div> 505 <div class="a"> 506 <div class="b"> 507 </div> 508 </div> 509 </div> 510 </template> 511 <script> 512 test((t) => { 513 t.add_cleanup(() => main.replaceChildren()); 514 main.append(test_scoped_nested_group_rule.content.cloneNode(true)); 515 516 let b_outside = document.querySelector('.b'); 517 let b_inside = document.querySelector('.a .b'); 518 assert_equals(getComputedStyle(b_outside).zIndex, 'auto'); 519 assert_equals(getComputedStyle(b_inside).zIndex, '1'); 520 }, 'Scoped nested group rule'); 521 </script> 522 523 <template id=test_scoped_within_scoped> 524 <div> 525 <style> 526 @scope (.a) { 527 @scope(#descendant) { 528 :scope { 529 z-index: 1; 530 } 531 } 532 @scope (> #child) { 533 :scope { 534 z-index: 1; 535 } 536 } 537 } 538 </style> 539 <div class="a"> 540 <div id="descendant"> 541 </div> 542 <div id="child"> 543 </div> 544 </div> 545 </div> 546 </template> 547 <script> 548 test((t) => { 549 t.add_cleanup(() => main.replaceChildren()); 550 main.append(test_scoped_within_scoped.content.cloneNode(true)); 551 552 assert_equals(getComputedStyle(descendant).zIndex, '1'); 553 assert_equals(getComputedStyle(child).zIndex, '1'); 554 }, 'Scoped nested within another scope'); 555 </script> 556 557 <template id=test_implicit_scope_nested_group_rule> 558 <div class=nest> 559 <style> 560 .nest { 561 @scope { 562 #child { 563 color: green; 564 } 565 } 566 } 567 </style> 568 <div id=child>Foo</div> 569 </div> 570 </template> 571 <script> 572 test((t) => { 573 t.add_cleanup(() => main.replaceChildren()); 574 main.append(test_implicit_scope_nested_group_rule.content.cloneNode(true)); 575 assert_equals(getComputedStyle(child).color, 'rgb(0, 128, 0)'); 576 }, 'Implicit (prelude-less) @scope as a nested group rule'); 577 </script> 578 579 <template id=test_insert_ampersand_rule_within_scope> 580 <style> 581 .a { 582 @scope (.b) { 583 #child { 584 color: red; 585 } 586 } 587 } 588 </style> 589 <div class=a> 590 <div class=b> 591 <div id=child>Foo</div> 592 </div> 593 </div> 594 </template> 595 <script> 596 test((t) => { 597 t.add_cleanup(() => main.replaceChildren()); 598 main.append(test_insert_ampersand_rule_within_scope.content.cloneNode(true)); 599 assert_equals(getComputedStyle(child).color, 'rgb(255, 0, 0)'); 600 let scope_rule = main.querySelector('style').sheet.cssRules[0].cssRules[0]; 601 // & does not add specificity - inserting it up front does nothing... 602 scope_rule.insertRule('& #child { color: green; }', 0); 603 assert_equals(getComputedStyle(child).color, 'rgb(255, 0, 0)'); 604 scope_rule.deleteRule(0); 605 // ... But inserting it at the end makes it win by order of appearance. 606 scope_rule.insertRule('& #child { color: green; }', scope_rule.cssRules.length); 607 assert_equals(getComputedStyle(child).color, 'rgb(0, 128, 0)'); 608 }, 'Insert a nested style rule within @scope, &'); 609 </script> 610 611 <template id=test_insert_pseudo_scope_rule_within_scope> 612 <style> 613 .a { 614 @scope (.b) { 615 #child { 616 color: red; 617 } 618 } 619 } 620 </style> 621 <div class=a> 622 <div class=b> 623 <div id=child>Foo</div> 624 </div> 625 </div> 626 </template> 627 <script> 628 test((t) => { 629 t.add_cleanup(() => main.replaceChildren()); 630 main.append(test_insert_pseudo_scope_rule_within_scope.content.cloneNode(true)); 631 assert_equals(getComputedStyle(child).color, 'rgb(255, 0, 0)'); 632 let scope_rule = main.querySelector('style').sheet.cssRules[0].cssRules[0]; 633 scope_rule.insertRule(':scope #child { color: green; }'); 634 assert_equals(getComputedStyle(child).color, 'rgb(0, 128, 0)'); 635 }, 'Insert a nested style rule within @scope, :scope'); 636 </script> 637 638 <template id=test_insert_nested_declarations_directly> 639 <style> 640 :where(.a) { 641 color: red; 642 } 643 @scope (.a) { 644 } 645 </style> 646 <div class=a> 647 <div id=child>Foo</div> 648 </div> 649 </template> 650 <script> 651 test((t) => { 652 t.add_cleanup(() => main.replaceChildren()); 653 main.append(test_insert_nested_declarations_directly.content.cloneNode(true)); 654 assert_equals(getComputedStyle(child).color, 'rgb(255, 0, 0)'); 655 let scope_rule = main.querySelector('style').sheet.cssRules[1]; 656 assert_true(scope_rule instanceof CSSScopeRule); 657 scope_rule.insertRule('color: green'); 658 assert_true(scope_rule.cssRules[0] instanceof CSSNestedDeclarations); 659 assert_equals(getComputedStyle(child).color, 'rgb(0, 128, 0)'); 660 }, 'Insert a CSSNestedDeclarations rule directly in top-level @scope'); 661 </script> 662 663 <template id=test_mutate_outer_selector_text_nested_declaration> 664 <style> 665 #child { 666 color: green; /* Specificity: (1, 0, 0) */ 667 } 668 .b { 669 #child { 670 @scope (&) { 671 --x: 1; 672 color: red; /* Specificity: (0, 0, 0), effectively :where(:scope) */ 673 } 674 } 675 } 676 </style> 677 <div class=a> 678 <div id=child>Foo</div> 679 </div> 680 </template> 681 <script> 682 test((t) => { 683 t.add_cleanup(() => main.replaceChildren()); 684 main.append(test_mutate_outer_selector_text_nested_declaration.content.cloneNode(true)); 685 assert_equals(getComputedStyle(child).color, 'rgb(0, 128, 0)'); 686 assert_equals(getComputedStyle(child).getPropertyValue('--x'), ''); 687 688 let outer_rule = main.querySelector('style').sheet.cssRules[1]; 689 assert_equals(outer_rule.selectorText, '.b'); 690 outer_rule.selectorText = '.a'; 691 692 assert_equals(getComputedStyle(child).color, 'rgb(0, 128, 0)'); // Unchanged. 693 assert_equals(getComputedStyle(child).getPropertyValue('--x'), '1'); // Changed. 694 }, 'Mutating selectorText on outer style rule causes correct inner specificity'); 695 </script> 696 697 <template id=test_set_selector_text> 698 <style id=style> 699 :where(.x) { 700 background-color: black; 701 } 702 .a { 703 @scope (&) { 704 :scope .x { background-color: green; } 705 } 706 } 707 </style> 708 <div class=a> 709 <div class=x> 710 </div> 711 </div> 712 <div class=b> 713 <div class=x> 714 </div> 715 </div> 716 </template> 717 <script> 718 test((t) => { 719 t.add_cleanup(() => main.replaceChildren()); 720 main.append(test_set_selector_text.content.cloneNode(true)); 721 let ax = main.querySelector('.a > .x'); 722 let bx = main.querySelector('.b > .x'); 723 assert_equals(getComputedStyle(ax).backgroundColor, 'rgb(0, 128, 0)'); 724 assert_equals(getComputedStyle(bx).backgroundColor, 'rgb(0, 0, 0)'); 725 style.sheet.cssRules[1].selectorText = '.b'; // .a -> . 726 assert_equals(getComputedStyle(ax).backgroundColor, 'rgb(0, 0, 0)'); 727 assert_equals(getComputedStyle(bx).backgroundColor, 'rgb(0, 128, 0)'); 728 }, 'Modifying selectorText invalidates inner scoped rules'); 729 </script> 730 731 <template id=test_set_selector_text_nested_declarations> 732 <style id=style> 733 :where(.x) { 734 background-color: black; 735 } 736 .a { 737 @scope (&) { 738 :scope .x { 739 .unused {} 740 /* CSSNestedDeclarations { */ 741 background-color: green; 742 /* } { */ 743 } 744 } 745 } 746 </style> 747 <div class=a> 748 <div class=x> 749 </div> 750 </div> 751 <div class=b> 752 <div class=x> 753 </div> 754 </div> 755 </template> 756 <script> 757 test((t) => { 758 t.add_cleanup(() => main.replaceChildren()); 759 main.append(test_set_selector_text_nested_declarations.content.cloneNode(true)); 760 let ax = main.querySelector('.a > .x'); 761 let bx = main.querySelector('.b > .x'); 762 assert_equals(getComputedStyle(ax).backgroundColor, 'rgb(0, 128, 0)'); 763 assert_equals(getComputedStyle(bx).backgroundColor, 'rgb(0, 0, 0)'); 764 style.sheet.cssRules[1].selectorText = '.b'; // .a -> . 765 assert_equals(getComputedStyle(ax).backgroundColor, 'rgb(0, 0, 0)'); 766 assert_equals(getComputedStyle(bx).backgroundColor, 'rgb(0, 128, 0)'); 767 }, 'Modifying selectorText invalidates inner scoped rules (nested declarations)'); 768 </script>