commit e90de811ad43a548a3d8fc555d574947986b7142
parent 312ba65ad0c509517d8222f91283202f291c26c8
Author: David Awogbemila <awogbemila@chromium.org>
Date: Thu, 8 Jan 2026 17:36:45 +0000
Bug 2008953 [wpt PR 57042] - [animation-trigger] Update attachments when trigger-scope changes, a=testonly
Automatic update from web-platform-tests
[animation-trigger] Update attachments when trigger-scope changes
This patch modifies the trigger attachment code so that triggers and
their attached animations are updated accordingly when a related
trigger-scope rule changes.
The CL modifies the FragmentBuilder propagation so that during a
trigger-scope change on an ancestor of the trigger source, even if the
trigger source isn't itself re-laid out, its ancestors will update their
named_trigger maps with the up-to-date DOM scoping, rather than use the
stale map of the source.
Bug: 390314945, 466134208
Change-Id: I407cadbab965917b49011c6b23ef02cb4522f1fc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7237813
Commit-Queue: David Awogbemila <awogbemila@chromium.org>
Reviewed-by: Anders Hartvoll Ruud <andruud@chromium.org>
Reviewed-by: David Awogbemila <awogbemila@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1565614}
--
wpt-commits: 3d27afe98884c2d9a9f99dd53ceacb0545fc53f2
wpt-pr: 57042
Diffstat:
1 file changed, 195 insertions(+), 0 deletions(-)
diff --git a/testing/web-platform/tests/scroll-animations/animation-trigger/trigger-scope-add.tentative.html b/testing/web-platform/tests/scroll-animations/animation-trigger/trigger-scope-add.tentative.html
@@ -0,0 +1,195 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <link rel="help" src="https://drafts.csswg.org/css-animations-2/#trigger-scope">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/web-animations/testcommon.js"></script>
+ <script src="/dom/events/scrolling/scroll_support.js"></script>
+ <script src="support/support.js"></script>
+ <script src="support/trigger-scope-support.js"></script>
+ </head>
+ <body>
+ <style>
+ @keyframes expand {
+ from { transform: scaleX(1) }
+ to { transform: scaleX(2) }
+ }
+ #outerscroller {
+ position: relative;
+ overflow-y: scroll;
+ border: solid 1px;
+ height: 400px;
+ width: 400px;
+ }
+ #innerscroller {
+ position: relative;
+ overflow-y: scroll;
+ height: 200px;
+ width: 200px;
+ border: solid 1px;
+ display: block;
+ }
+ #source {
+ position: absolute;
+ top: 100%;
+ height: 100px;
+ width: 100px;
+ background-color: blue;
+ timeline-trigger: --trigger view() contain;
+ }
+ .target {
+ background-color: red;
+ height: 100px;
+ width: 100px;
+ animation: expand linear 1s both;
+ animation-trigger: --trigger play-forwards;
+ }
+
+ #inner_target {
+ /* Let's it be in view when the trigger source comes into view */
+ margin-top: 50%;
+ }
+
+ .long {
+ width: 50%;
+ height: 100%;
+ }
+ </style>
+ <div id="outerscroller">
+ <div id="innerscroller">
+ <div id="inner_target" class="target">In-scope Target</div>
+ <div class="long"></div>
+ <div class="long"></div>
+ </div>
+ <div id="source">SOURCE</div>
+ <div id="outer_target" class="target">
+ Out-of-scope Target
+ </div>
+ <div class="long"></div>
+ <div class="long"></div>
+ </div>
+
+ <script>
+ let inner_target = document.getElementById("inner_target");
+ let outer_target = document.getElementById("outer_target");
+ let source = document.getElementById("source");
+
+ let current_scroller = outerscroller;
+
+ async function scrollToTrigger() {
+ const scrollend_promise =
+ waitForScrollEndFallbackToDelayWithoutScrollEvent(current_scroller);
+ source.scrollIntoView({block: "center"});
+ await scrollend_promise;
+ await waitForCompositorReady();
+ }
+
+ async function resetScrollPositionAndElements(test) {
+ await waitForScrollReset(test, current_scroller);
+
+ // Reset the animations, so we can detect when they trigger again.
+ inner_target.getAnimations()[0].pause();
+ inner_target.getAnimations()[0].currentTime = 0;
+
+ outer_target.getAnimations()[0].pause();
+ outer_target.getAnimations()[0].currentTime = 0;
+
+ await waitForCompositorCommit();
+
+ // Both inner and outer animations should once again be paused.
+ await assert_playstate_and_current_time(
+ inner_target.id, inner_target.getAnimations()[0], "paused");
+ await assert_playstate_and_current_time(outer_target.id,
+ outer_target.getAnimations()[0],
+ "paused");
+ assert_equals(current_scroller.scrollTop, 0, "scroll position reset");
+ }
+
+ async function scrollAndAssert(inner_play_state, outer_play_state) {
+ await scrollToTrigger();
+ assert_greater_than(current_scroller.scrollTop, 0, "did scroll");
+
+ // Both the inner and outer targets should be triggered as there is no
+ // scope and they are both attached to the trigger.
+ await assert_playstate_and_current_time(inner_target.id,
+ inner_target.getAnimations()[0],
+ inner_play_state);
+ await assert_playstate_and_current_time(outer_target.id,
+ outer_target.getAnimations()[0],
+ outer_play_state);
+ }
+
+ promise_test(async(test) => {
+ assert_equals(getComputedStyle(innerscroller).triggerScope, "none");
+ await assert_playstate_and_current_time(
+ inner_target.id, inner_target.getAnimations()[0], "paused");
+ await assert_playstate_and_current_time(outer_target.id,
+ outer_target.getAnimations()[0],
+ "paused");
+
+ // Scroll to make the trigger fire.
+ // Both the inner and outer targets should be triggered as there is no
+ // scope and they are both attached to the trigger.
+ await scrollAndAssert(/*inner=*/"running", /*outer=*/"running");
+
+ // Now, insert a scope.
+ innerscroller.style.triggerScope = "--trigger";
+
+ await resetScrollPositionAndElements(test);
+
+ // Scroll to make the trigger fire again.
+ // Only the outer animation should be triggered as the inner is
+ // prevented from seeing the trigger sue to the scope.
+ await scrollAndAssert(/*inner=*/"paused", /*outer=*/"running");
+
+ // Now, remove the scope.
+ innerscroller.style.triggerScope = "initial";
+
+ await resetScrollPositionAndElements(test);
+
+ // Scroll to make the trigger fire again.
+ // Both the inner and outer targets should be triggered as there is no
+ // scope and they are both attached to the trigger.
+ await scrollAndAssert(/*inner=*/"running", /*outer=*/"running");
+ }, "Added scope prevents subtree from searching for external trigger");
+
+ promise_test(async(test) => {
+ // Move the source within the scope so that inner_target sees the
+ // trigger when the scope is inserted. And set the scroller to watch to
+ // innerscroller.
+ outerscroller.removeChild(source);
+ innerscroller.append(source);
+ current_scroller = innerscroller;
+
+ await resetScrollPositionAndElements(test);
+
+ // Scroll to make the trigger fire.
+ // Both the inner and outer targets should be triggered as there is no
+ // scope and they are both attached to the trigger.
+ await scrollAndAssert(/*inner=*/"running", /*outer=*/"running");
+
+ // Now, insert a scope.
+ innerscroller.style.triggerScope = "--trigger";
+
+ await resetScrollPositionAndElements(test);
+
+ // Scroll to make the trigger fire again.
+ // Only the inner animation should be triggered as the outer is
+ // prevented from seeing the trigger due to the scope.
+ await scrollAndAssert(/*inner=*/"running", /*outer=*/"paused");
+
+ // Now, remove the scope.
+ innerscroller.style.triggerScope = "initial";
+
+ await resetScrollPositionAndElements(test);
+
+ // Scroll to make the trigger fire again.
+ // Both the inner and outer targets should be triggered as there is no
+ // scope and they are both attached to the trigger.
+ await scrollAndAssert(/*inner=*/"running", /*outer=*/"running");
+ }, "Added scope prevents external references from finding trigger " +
+ "within scope.");
+ </script>
+ </body>
+</html>