commit 977361c9c1b0999800da969f405243665f7e6d79
parent c7d0d0103e485a66fd8e3dd4f6cb5105baa67ebb
Author: Blink WPT Bot <blink-w3c-test-autoroller@chromium.org>
Date: Wed, 12 Nov 2025 08:51:54 +0000
Bug 1999545 [wpt PR 55981] - Respect overscroll-behavior when determining scroll bubbling, a=testonly
Automatic update from web-platform-tests
Respect overscroll-behavior when determining scroll bubbling (#55981)
According to the spec[1]:
- `auto`: allows normal scroll chaining and overscroll behavior.
- `contain`: prevents non-local boundary actions such as scroll chaining
or navigation, but keeps local overscroll affordances (e.g. rubberband).
- `none`: same as `contain`, but also suppresses local overscroll effects.
Therefore, when overscroll-behavior is not `auto`, scroll should not
bubble to ancestor elements along the scroll chain.
[1]: https://drafts.csswg.org/css-overscroll/
Bug: 41378182
Change-Id: I884802d26d34825014ae6c881c7203bea031c8c8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7079630
Reviewed-by: Robert Flack <flackr@chromium.org>
Commit-Queue: Peng Zhou <zhoupeng.1996@bytedance.com>
Cr-Commit-Position: refs/heads/main@{#1543176}
Co-authored-by: Peng Zhou <zhoupeng.1996@bytedance.com>
--
wpt-commits: 9f8490256485e4e9a2729e2896c8ce0336932916
wpt-pr: 55981
Diffstat:
4 files changed, 290 insertions(+), 0 deletions(-)
diff --git a/testing/web-platform/tests/css/css-overscroll-behavior/overscroll-behavior-keyboard-scroll-child-frame.html b/testing/web-platform/tests/css/css-overscroll-behavior/overscroll-behavior-keyboard-scroll-child-frame.html
@@ -0,0 +1,62 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>overscroll-behavior for keyboard scroll in child frame</title>
+<meta name="timeout" content="long">
+<link rel="help" href="https://drafts.csswg.org/css-overscroll-behavior">
+<link rel="author" title="Peng Zhou" href="mailto:zhoupeng.1996@bytedance.com">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/dom/events/scrolling/scroll_support.js"></script>
+<script src="/css/css-scroll-snap/support/common.js"></script>
+<style>
+body {
+ margin: 0;
+}
+
+#container {
+ overflow: auto;
+ height: 600px;
+ position: relative;
+}
+
+iframe {
+ width: 600px;
+ height: 400px;
+ position: absolute;
+ top: 500px;
+}
+</style>
+<div id="container">
+ <iframe id="iframe" src="resources/keyboard-scroll-child-frame-iframe.html"></iframe>
+</div>
+<script>
+window.addEventListener('load', () => {
+ promise_test(async () => {
+ const target = iframe.contentWindow;
+ let scrollEndPromise = waitForScrollEndFallbackToDelayWithoutScrollEvent(container);
+ container.scrollTop = 300;
+ await scrollEndPromise;
+
+ scrollEndPromise = waitForScrollEndFallbackToDelayWithoutScrollEvent(target);
+ target.scrollTo(0, 50);
+ await scrollEndPromise;
+ assert_equals(target.scrollY, 50);
+
+ await new test_driver.Actions()
+ .pointerMove(200, 300)
+ .pointerDown()
+ .pointerUp()
+ .send();
+ assert_equals(document.activeElement, iframe);
+
+ scrollEndPromise = waitForScrollEndOrTimeout(target, 1000);
+ await scrollElementByKeyboard('ArrowUp');
+ await scrollEndPromise;
+ assert_equals(container.scrollTop, 300);
+ assert_equals(target.scrollY, 0);
+ }, 'scrolling is not propagated from iframe to the main frame');
+});
+</script>
diff --git a/testing/web-platform/tests/css/css-overscroll-behavior/overscroll-behavior-keyboard-scroll.html b/testing/web-platform/tests/css/css-overscroll-behavior/overscroll-behavior-keyboard-scroll.html
@@ -0,0 +1,168 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>overscroll-behavior for keyboard scroll</title>
+<meta name="timeout" content="long">
+<link rel="help" href="https://drafts.csswg.org/css-overscroll-behavior">
+<link rel="author" title="Peng Zhou" href="mailto:zhoupeng.1996@bytedance.com">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/dom/events/scrolling/scroll_support.js"></script>
+<script src="/css/css-scroll-snap/support/common.js"></script>
+
+<style>
+body {
+ margin: 0px;
+}
+.outer {
+ height: 400px;
+ width: 1000px;
+ background: white
+}
+.content {
+ height: 600px;
+ width: 1200px;
+}
+#root {
+ overflow: scroll;
+ height: 600px;
+ width: 800px;
+ background: white;
+}
+#container {
+ overflow: scroll;
+}
+#non_scrollable {
+ overflow: clip;
+}
+#green {
+ background: repeating-linear-gradient(to bottom right, green 15%, white 30%);
+}
+#blue {
+ background: repeating-linear-gradient(to bottom right, blue 15%, white 30%);
+}
+</style>
+
+<div id='root'>
+ <div id='non_scrollable' class='outer'>
+ <div id='green' class='content'></div>
+ </div>
+ <div id='container' class='outer'>
+ <div id='blue' class='content'></div>
+ </div>
+</div>
+<!--
+Tests that overscroll-behavior prevents scroll-propagation in the area and
+direction as specified.
+ Manual Steps:
+ 1. Make two scrolls on blue, in this order: scroll UP (or drag down), then
+ scroll LEFT (or drag right). Scroll (or drag) until nothing is
+ scrolling.
+ 2. Call verify_y_prevented_and_set_boundary_prevents_x() from console
+ 3. Repeat the same scrolls as in step 1
+ 4. Call verify_x_prevented_and_set_boundary_allows_inner() from console
+ 5. Repeat the same scrolls as in step 1
+ 6. Call verify_inner_allowed_and_set_nonscrollable_allows_propagation()
+ from console
+ 7. Make two separate scrolls on green, in this order: scroll UP
+ (or drag down), then scroll LEFT (or drag right). Scroll (or drag) until
+ nothing is scrolling.
+ 8. Call verify_non_scrollable_allows_propagation() from console
+</ol> -->
+<script>
+function setScrollPosition(scroller, offset) {
+ scroller.scrollTop = offset;
+ scroller.scrollLeft = offset;
+}
+
+async function scrollWithKeyboardWait(scrollElement, overflowScroller) {
+ const scrollerRect = scrollElement.getBoundingClientRect();
+ // Move pointer to scroll_element.
+ await new test_driver.Actions()
+ .pointerMove(scrollerRect.left + 200, scrollerRect.top + 100)
+ .pointerDown()
+ .pointerUp()
+ .send();
+ // Perform vertical scroll.
+ let scrollEndPromise = waitForScrollEndOrTimeout(overflowScroller, 1000);
+ await scrollElementByKeyboard('ArrowUp');
+ await scrollEndPromise;
+ // Perform horizontal scroll.
+ scrollEndPromise = waitForScrollEndOrTimeout(overflowScroller, 1000);
+ await scrollElementByKeyboard('ArrowLeft');
+ await scrollEndPromise;
+}
+
+promise_test(async t => {
+ await waitForCompositorReady();
+
+ container.style.overscrollBehaviorX = 'auto';
+ container.style.overscrollBehaviorY = 'none';
+ setScrollPosition(root, 100);
+ setScrollPosition(container, 0);
+ window.scrollTo(0, 0);
+
+ await scrollWithKeyboardWait(container, root);
+
+ assert_approx_equals(root.scrollTop, 100, 0.5,
+ "root got unexpected scroll on Y axis");
+ assert_approx_equals(root.scrollLeft, 0, 0.5,
+ "root expected to scroll on X axis");
+}, "overscroll-behavior-y: none prevents scroll propagation on y axis");
+
+promise_test(async t => {
+ await waitForCompositorReady();
+
+ container.style.overscrollBehaviorX = 'none';
+ container.style.overscrollBehaviorY = 'auto';
+ setScrollPosition(root, 100);
+ setScrollPosition(container, 0);
+ window.scrollTo(0, 0);
+
+ await scrollWithKeyboardWait(container, root);
+
+ assert_approx_equals(root.scrollTop, 0, 0.5,
+ "root expected to scroll on Y axis");
+ assert_approx_equals(root.scrollLeft, 100, 0.5,
+ "root got unexpected scroll on X axis");
+}, "overscroll-behavior-x: none prevents scroll propagation on x axis");
+
+promise_test(async t => {
+ await waitForCompositorReady();
+
+ container.style.overscrollBehaviorX = 'none';
+ container.style.overscrollBehaviorY = 'none';
+ setScrollPosition(root, 100);
+ setScrollPosition(container, 100);
+ window.scrollTo(0, 0);
+
+ await scrollWithKeyboardWait(container, container);
+
+ assert_approx_equals(container.scrollTop, 0, 0.5,
+ "inner container expected to scroll on Y axis");
+ assert_approx_equals(container.scrollLeft, 0, 0.5,
+ "inner container expected to scroll on X axis");
+ assert_approx_equals(root.scrollTop, 100, 0.5,
+ "root got unexpected scroll on Y axis");
+ assert_approx_equals(root.scrollLeft, 100, 0.5,
+ "root got unexpected scroll on X axis");
+}, "overscroll-behavior allows inner scrolling when both axes are none");
+
+promise_test(async t => {
+ await waitForCompositorReady();
+
+ non_scrollable.style.overscrollBehaviorX = 'none';
+ non_scrollable.style.overscrollBehaviorY = 'none';
+ setScrollPosition(root, 100);
+ window.scrollTo(0, 0);
+
+ await scrollWithKeyboardWait(non_scrollable, root);
+
+ assert_approx_equals(root.scrollLeft, 0, 0.5,
+ "root expected to scroll on X axis");
+ assert_approx_equals(root.scrollTop, 0, 0.5,
+ "root expected to scroll on Y axis");
+}, "overscroll-behavior on non-scrollable area allows scroll propagation");
+</script>
diff --git a/testing/web-platform/tests/css/css-overscroll-behavior/resources/keyboard-scroll-child-frame-iframe.html b/testing/web-platform/tests/css/css-overscroll-behavior/resources/keyboard-scroll-child-frame-iframe.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>:root with overscroll-behavior:none prevents scroll propagation from child frame</title>
+<style>
+:root {
+ overscroll-behavior-y: none;
+}
+
+body {
+ margin: 0;
+ font-family: sans-serif;
+}
+
+.content {
+ height: 400vh;
+ background: red;
+}
+</style>
+<div class="content"></div>
diff --git a/testing/web-platform/tests/dom/events/scrolling/scroll_support.js b/testing/web-platform/tests/dom/events/scrolling/scroll_support.js
@@ -84,6 +84,27 @@ function waitForScrollEndFallbackToDelayWithoutScrollEvent(eventTargets) {
});
}
+// Waits for the end of scrolling, but resolves after the given timeout if no
+// scroll event occurs.
+function waitForScrollEndOrTimeout(eventTarget, timeout) {
+ const rafTimeout = new Promise(resolve => {
+ const startTime = performance.now();
+ const tick = () => {
+ if (performance.now() - startTime >= timeout) {
+ resolve();
+ } else {
+ requestAnimationFrame(tick);
+ }
+ };
+ requestAnimationFrame(tick);
+ });
+
+ return Promise.race([
+ waitForScrollEndFallbackToDelayWithoutScrollEvent(eventTarget),
+ rafTimeout
+ ]);
+}
+
async function waitForPointercancelEvent(test, target, timeoutMs = 500) {
return waitForEvent("pointercancel", test, target, timeoutMs);
}
@@ -345,3 +366,23 @@ function scrollElementLeft(element, scroll_amount) {
.scroll(x, y, delta_x, delta_y, {origin: element});
return actions.send();
}
+
+async function scrollElementByKeyboard(key) {
+ const KEY_CODE_MAP = {
+ 'ArrowLeft': '\uE012',
+ 'ArrowUp': '\uE013',
+ 'ArrowRight': '\uE014',
+ 'ArrowDown': '\uE015',
+ };
+
+ if (!KEY_CODE_MAP.hasOwnProperty(key)) {
+ return Promise.reject(`Invalid key for scrollElementByKeyboard: ${key}`);
+ }
+ const code = KEY_CODE_MAP[key];
+ for (let i = 0; i < 10; i++) {
+ await new test_driver.Actions()
+ .keyDown(code)
+ .keyUp(code)
+ .send();
+ }
+}