commit a9d97f006fbcde69fc94b7e1aab2b0998504e675
parent 672bca6089872f9c626e223996923e3e83db3be7
Author: Hiroyuki Ikezoe <hikezoe.birchill@mozilla.com>
Date: Tue, 23 Dec 2025 11:26:35 +0000
Bug 1988109 - Allow scroll anchoring in contenteditables. r=masayuki
Even without `overflow-anchor: none` for contenteditable, I confirmed
bug 1583135 doesn't happen.
There's already a web platform test for this specific bug,
contenteditable-near-cursor.tentative.html is the one.
contenteditable-insert-line-at-top.tentative.html is a new test, but
its purpose is replicating a scenario of bug 1583135, it's not a test
testing that this change doesn't regress bug 1583135. As per a note
by Masayuki [1] when he tried to write an automated test for bug 1583135,
synthesized events didn't hit the condition causing the bug, thus this test
probably doesn't hit the exact condition either. So, this test is just
for a future reference.
anchor-inside-textarea.tentative.html is another new test that a text
line inside textarea can be an anchor.
[1] https://bugzilla.mozilla.org/show_bug.cgi?id=1583135#c27
Differential Revision: https://phabricator.services.mozilla.com/D277303
Diffstat:
4 files changed, 197 insertions(+), 12 deletions(-)
diff --git a/layout/style/res/ua.css b/layout/style/res/ua.css
@@ -475,15 +475,6 @@ parsererror|sourcetext {
direction: ltr;
}
-/* contenteditable support */
-
-:read-write:focus,
-:root:read-write {
- /* Scroll-anchoring shouldn't work in any editable and scrollable elements
- * when user inserts something. */
- overflow-anchor: none;
-}
-
/* https://drafts.csswg.org/css-view-transitions-1/#ua-styles */
/* :root section moved to the other root selectors for performance */
diff --git a/testing/web-platform/meta/css/css-scroll-anchoring/contenteditable-near-cursor.tentative.html.ini b/testing/web-platform/meta/css/css-scroll-anchoring/contenteditable-near-cursor.tentative.html.ini
@@ -1,3 +0,0 @@
-[contenteditable-near-cursor.tentative.html]
- [Ensure scroll is adjusted to keep element next to the cursor stable]
- expected: FAIL
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/anchor-inside-textarea.tentative.html b/testing/web-platform/tests/css/css-scroll-anchoring/anchor-inside-textarea.tentative.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<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>
+<style>
+
+#container {
+ height: 300px;
+ width: 300px;
+ overflow-y: scroll;
+ scrollbar-width: none;
+}
+
+textarea {
+ /* twice the height of the container so that the container is scrollable */
+ height: 600px;
+ width: 300px;
+ padding: 0px;
+ border: none;
+ /* This textarea has visible 10 lines in the scroll container */
+ font-size: 30px;
+ line-height: 30px;
+}
+
+</style>
+
+<div id="container">
+ <textarea rows="20"></textarea>
+</div>
+<script>
+
+setup(() => {
+ let lines = [];
+ for (let i = 1; i <= 20; i++) {
+ lines.push(`Paragraph ${i}`);
+ }
+ const textarea = document.querySelector("textarea");
+ textarea.value += lines.join("\n");
+});
+
+promise_test(async t => {
+ const textarea = document.querySelector("textarea");
+ textarea.focus();
+
+ // Scroll down to the bottom.
+ let scrollPromise = new Promise(resolve => {
+ container.addEventListener("scroll", () => resolve(), { once: true });
+ });
+ container.scrollTo(0, 300);
+ await scrollPromise;
+
+ assert_approx_equals(container.scrollTop, 300, 1);
+ const lastScrollPosition = container.scrollTop;
+
+ // Wait two frames to settle the scroll anchor position.
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ await new Promise(resolve => requestAnimationFrame(resolve));
+
+ // Now the "Paragraph 11" should be positioned at the top of the scroll
+ // container (since the half of the textarea has been scrolled out),
+ // it will be an anchor.
+
+ // Insert a new 10px height element before the textarea so that
+ // the text area moves 10px down.
+ const beforeContent = document.createElement("div");
+ beforeContent.style.height = "10px";
+ container.insertBefore(beforeContent, textarea);
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ assert_equals(container.scrollTop, lastScrollPosition + 10,
+ "The scroll position should be adjusted");
+
+ // Append a new 10px height element at the bottom textarea so that
+ // the text area moves 10px up.
+ const afterContent = document.createElement("div");
+ afterContent.style.height = "10px";
+ container.insertBefore(afterContent, textarea);
+ container.appendChild(afterContent);
+ await new Promise(resolve => requestAnimationFrame(resolve));
+
+ assert_equals(container.scrollTop, lastScrollPosition + 10,
+ "The scroll position should be unchanged");
+}, "Scroll anchoring works on textarea");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/contenteditable-insert-line-at-top.tentative.html b/testing/web-platform/tests/css/css-scroll-anchoring/contenteditable-insert-line-at-top.tentative.html
@@ -0,0 +1,109 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<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>
+<style>
+
+#container {
+ height: 300px;
+ width: 300px;
+ overflow-y: scroll;
+ scroll-behavior: auto;
+}
+
+p {
+ margin: 0px;
+ line-height: 20px;
+ font-size: 20px;
+}
+
+</style>
+
+<div id="container">
+ <div contenteditable>
+ <p>
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ante
+ nulla, dictum rhoncus libero vel, dictum iaculis sem. Morbi sit amet
+ euismod ligula. Proin et est ex. Pellentesque sollicitudin lobortis diam
+ eu posuere. Donec a diam risus. Fusce quis semper sapien, sed tincidunt
+ mi. Nullam tortor diam, sagittis sed scelerisque ut, scelerisque a
+ turpis. Integer a dignissim turpis. Etiam eu pharetra nisl, ac ultricies
+ sem. Ut at tristique turpis. Aliquam vitae arcu quis turpis gravida
+ luctus at at turpis. Nullam aliquet turpis sed lectus interdum
+ ultricies.
+ </p>
+ </div>
+</div>
+<script>
+
+setup(() => {
+ const editable = document.querySelector("div[contenteditable]");
+ const child = editable.children[0];
+
+ for (let i = 0; i < 10; i++) {
+ const clone = child.cloneNode(true);
+ editable.appendChild(clone);
+ }
+ document.documentElement.getBoundingClientRect();
+});
+
+promise_test(async t => {
+ const editable = document.querySelector("div[contenteditable]");
+ editable.focus();
+
+ // Scroll down to the bottom.
+ let scrollPromise = new Promise(resolve => {
+ container.addEventListener("scroll", () => resolve(), { once: true });
+ });
+ // Hopefully 10000 is greater than the maximum possible scroll position.
+ container.scrollTo(0, 10000);
+ await scrollPromise;
+
+ assert_greater_than(container.scrollTop, 0);
+
+ const selection = window.getSelection();
+ selection.collapse(editable.lastElementChild, 1);
+
+ // Pres Ctrl+Home or Meta+ArrorUp to move back to the top.
+ scrollPromise = new Promise(resolve => {
+ container.addEventListener("scroll", () => resolve(), { once: true });
+ });
+ const kHomeOrArrowUp =
+ navigator.platform.includes("Mac") ? "\uE013" : "\uE011";
+ const kControlOrMeta =
+ navigator.platform.includes("Mac") ? "\uE03d" : "\uE009";
+ await new test_driver.Actions()
+ .keyDown(kControlOrMeta)
+ .keyDown(kHomeOrArrowUp)
+ .keyUp(kHomeOrArrowUp)
+ .keyUp(kControlOrMeta)
+ .send();
+ await scrollPromise;
+
+ assert_equals(container.scrollTop, 0,
+ "The scroll position should be restored to the top");
+
+ container.addEventListener("scroll", () => {
+ assert_false("Any scroll event should not happen");
+ });
+ // Press "Enter" to insert a new line at the top.
+ const kEnter = "\uE007";
+ await new test_driver.Actions()
+ .keyDown(kEnter)
+ .keyUp(kEnter)
+ .send();
+
+ // Wait 3 frames to give a chance to scroll.
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ await new Promise(resolve => requestAnimationFrame(resolve));
+
+ assert_equals(container.scrollTop, 0,
+ "The scroll position should be unchanged");
+}, "The scroll position is unchanged when inserting a new line at the top of contenteditable");
+
+</script>