[@layer] Use layer order to resolve @property and @scroll-timeline name conflicts
This patch adds a new class AtRuleCascadeMap, a miniature CascadeMap,
to cascade @property and @scroll-time rules across origins and cascade
layers.
We can't use it to cascade other name-defining at-rules because:
- @font-face rules with the same 'font-family' may co-exist
- @counter-style and @keyframes rules are managed per tree scope, and
allow the "append-only" update in Blink
Bug: 1095765
Change-Id: Iec5dad0b932b1aed6e9c18bca9da1443e2d1fd88
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3152514
Commit-Queue: Xiaocheng Hu <xiaochengh@chromium.org>
Reviewed-by: Anders Hartvoll Ruud <andruud@chromium.org>
Cr-Commit-Position: refs/heads/main@{#920947}
diff --git a/css/css-cascade/layer-property-override.html b/css/css-cascade/layer-property-override.html
new file mode 100644
index 0000000..f0f8d83
--- /dev/null
+++ b/css/css-cascade/layer-property-override.html
@@ -0,0 +1,154 @@
+<!DOCTYPE html>
+<title>Resolving @property name conflicts with cascade layers</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#layering">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+#target, #reference {
+ width: 100px;
+ height: 100px;
+}
+
+#reference {
+ background-color: green;
+}
+</style>
+
+<div id="target"></div>
+<div id="reference"></div>
+
+<script>
+// In all tests, background color of #target should be green, same as #reference
+
+const testCases = [
+ {
+ title: '@property layered overrides unlayered',
+ style: `
+ #target {
+ background-color: var(--foo);
+ }
+
+ @layer {
+ @property --foo {
+ syntax: '<color>';
+ inherits: false;
+ initial-value: green;
+ }
+ }
+
+ @property --foo {
+ syntax: '<color>';
+ inherits: false;
+ initial-value: red;
+ }
+ `
+ },
+
+ {
+ title: '@property override between layers',
+ style: `
+ @layer base, override;
+
+ #target {
+ background-color: var(--foo);
+ }
+
+ @layer override {
+ @property --foo {
+ syntax: '<color>';
+ inherits: false;
+ initial-value: green;
+ }
+ }
+
+ @layer base {
+ @property --foo {
+ syntax: '<color>';
+ inherits: false;
+ initial-value: red;
+ }
+ }
+ `
+ },
+
+ {
+ title: '@property override update with appended sheet 1',
+ style: `
+ @layer base, override;
+
+ #target {
+ background-color: var(--foo);
+ }
+
+ @layer override {
+ @property --foo {
+ syntax: '<color>';
+ inherits: false;
+ initial-value: green;
+ }
+ }
+ `,
+ append: `
+ @layer base {
+ @property --foo {
+ syntax: '<color>';
+ inherits: false;
+ initial-value: red;
+ }
+ }
+ `
+ },
+
+ {
+ title: '@property override update with appended sheet 2',
+ style: `
+ @layer base, override;
+
+ #target {
+ background-color: var(--foo);
+ }
+
+ @layer base {
+ @property --foo {
+ syntax: '<color>';
+ inherits: false;
+ initial-value: red;
+ }
+ }
+ `,
+ append: `
+ @layer override {
+ @property --foo {
+ syntax: '<color>';
+ inherits: false;
+ initial-value: green;
+ }
+ }
+ `
+ },
+];
+
+for (let testCase of testCases) {
+ var documentStyle = document.createElement('style');
+ documentStyle.appendChild(document.createTextNode(testCase['style']));
+ document.head.appendChild(documentStyle);
+
+ var appendedStyle;
+ if (testCase['append']) {
+ document.body.offsetLeft; // Force style update
+ appendedStyle = document.createElement('style');
+ appendedStyle.appendChild(document.createTextNode(testCase['append']));
+ document.head.appendChild(appendedStyle);
+ }
+
+ test(function () {
+ assert_equals(getComputedStyle(target).backgroundColor,
+ getComputedStyle(reference).backgroundColor);
+ }, testCase['title']);
+
+ if (appendedStyle)
+ appendedStyle.remove();
+ documentStyle.remove();
+}
+</script>
diff --git a/css/css-cascade/layer-scroll-timeline-override.html b/css/css-cascade/layer-scroll-timeline-override.html
new file mode 100644
index 0000000..9a50914
--- /dev/null
+++ b/css/css-cascade/layer-scroll-timeline-override.html
@@ -0,0 +1,183 @@
+<!DOCTYPE html>
+<title>Resolving @scroll-timeline name conflicts with cascade layers</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#layering">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<style>
+#scroller {
+ overflow: scroll;
+ width: 100px;
+ height: 100px;
+}
+
+#scroller div {
+ height: 200px;
+}
+
+@keyframes expand {
+ from { width: 100px; }
+ to { width: 200px; }
+}
+
+#target, #reference {
+ height: 100px;
+}
+
+#reference {
+ width: 150px;
+}
+
+#target {
+ animation: expand 10s linear;
+ height: 100px;
+}
+</style>
+
+<div id="scroller">
+ <div></div>
+</div>
+<div id="target"></div>
+<div id="reference"></div>
+
+<script>
+// In all tests, width of #target should be 150px, same as #reference
+
+const testCases = [
+ {
+ title: '@scroll-timeline layered overrides unlayered',
+ style: `
+ #target {
+ animation-timeline: timeline;
+ }
+
+ @layer {
+ @scroll-timeline timeline {
+ source: selector(#scroller);
+ start: 0px;
+ end: 50px;
+ }
+ }
+
+ @scroll-timeline timeline {
+ source: selector(#scroller);
+ start: 0px;
+ end: 100px;
+ }
+ `
+ },
+
+ {
+ title: '@scroll-timeline override between layers',
+ style: `
+ @layer base, override;
+
+ #target {
+ animation-timeline: timeline;
+ }
+
+ @layer override {
+ @scroll-timeline timeline {
+ source: selector(#scroller);
+ start: 0px;
+ end: 50px;
+ }
+ }
+
+ @layer base {
+ @scroll-timeline timeline {
+ source: selector(#scroller);
+ start: 0px;
+ end: 100px;
+ }
+ }
+ `
+ },
+
+ {
+ title: '@scroll-timeline override update with appended sheet 1',
+ style: `
+ @layer base, override;
+
+ #target {
+ animation-timeline: timeline;
+ }
+
+ @layer override {
+ @scroll-timeline timeline {
+ source: selector(#scroller);
+ start: 0px;
+ end: 50px;
+ }
+ }
+ `,
+ append: `
+ @layer base {
+ @scroll-timeline timeline {
+ source: selector(#scroller);
+ start: 0px;
+ end: 100px;
+ }
+ }
+ `
+ },
+
+ {
+ title: '@scroll-timeline override update with appended sheet 2',
+ style: `
+ @layer base, override;
+
+ #target {
+ animation-timeline: timeline;
+ }
+
+ @layer base {
+ @scroll-timeline timeline {
+ source: selector(#scroller);
+ start: 0px;
+ end: 100px;
+ }
+ }
+ `,
+ append: `
+ @layer override {
+ @scroll-timeline timeline {
+ source: selector(#scroller);
+ start: 0px;
+ end: 50px;
+ }
+ }
+ `
+ },
+];
+
+for (let testCase of testCases) {
+ promise_test(async function() {
+ assert_true(
+ CSS.supports('animation-timeline', 'foo'),
+ 'This test requires @scroll-timeline support');
+
+ var documentStyle = document.createElement('style');
+ documentStyle.appendChild(document.createTextNode(testCase['style']));
+ document.head.appendChild(documentStyle);
+
+ var appendedStyle;
+ if (testCase['append']) {
+ document.body.offsetLeft; // Force style update
+ appendedStyle = document.createElement('style');
+ appendedStyle.appendChild(document.createTextNode(testCase['append']));
+ document.head.appendChild(appendedStyle);
+ }
+
+ scroller.scrollTop = 25;
+ await waitForNextFrame();
+ assert_equals(getComputedStyle(target).width,
+ getComputedStyle(reference).width);
+
+ if (appendedStyle)
+ appendedStyle.remove();
+ documentStyle.remove();
+ }, testCase['title']);
+}
+</script>