LibWeb: Improve scrollingElement handling

This change is currently entirely undetectable because of what the
added FIXME talks about. Currently, the HTML element's overflow is
always set to visible in both axes, so it getting set to "clip" in
the imported test ends up not mattering at all.
This commit is contained in:
Psychpsyo 2025-11-06 17:17:34 +01:00 committed by Sam Atkins
parent 03f262f03d
commit fe2bc2bfe7
5 changed files with 177 additions and 12 deletions

View file

@ -1357,6 +1357,7 @@ static void propagate_overflow_to_viewport(Element& root_element, Layout::Viewpo
viewport_computed_values.set_overflow_y(overflow_y_to_apply);
// The element from which the value is propagated must then have a used overflow value of visible.
// FIXME: Apply this to the used values, not the computed ones.
overflow_origin_computed_values.set_overflow_x(CSS::Overflow::Visible);
overflow_origin_computed_values.set_overflow_y(CSS::Overflow::Visible);
}
@ -5758,7 +5759,7 @@ GC::Ptr<Element const> Document::scrolling_element() const
if (in_quirks_mode()) {
// 1. If the body element exists, and it is not potentially scrollable, return the body element and abort these steps.
// For this purpose, a value of overflow:clip on the the body elements parent element must be treated as overflow:hidden.
if (auto const* body_element = body(); body_element && !body_element->is_potentially_scrollable())
if (auto const* body_element = body(); body_element && !body_element->is_potentially_scrollable(Element::TreatOverflowClipOnBodyParentAsOverflowHidden::Yes))
return body_element;
// 2. Return null and abort these steps.

View file

@ -1889,26 +1889,34 @@ void Element::set_tab_index(i32 tab_index)
}
// https://drafts.csswg.org/cssom-view/#potentially-scrollable
bool Element::is_potentially_scrollable() const
bool Element::is_potentially_scrollable(TreatOverflowClipOnBodyParentAsOverflowHidden treat_overflow_clip_on_body_parent_as_overflow_hidden = TreatOverflowClipOnBodyParentAsOverflowHidden::No) const
{
// NOTE: Ensure that layout is up-to-date before looking at metrics.
const_cast<Document&>(document()).update_layout(UpdateLayoutReason::ElementIsPotentiallyScrollable);
const_cast<Document&>(document()).update_style();
// NOTE: Since this should always be the body element, the body element must have a <html> element parent. See Document::body().
VERIFY(parent());
// An element body (which will be the body element) is potentially scrollable if all of the following conditions are true:
VERIFY(is<HTML::HTMLBodyElement>(this) || is<HTML::HTMLFrameSetElement>(this));
// Since this should always be the body element, the body element must have a <html> element parent. See Document::body().
VERIFY(parent());
// - body has an associated box.
if (!layout_node())
return false;
// - bodys parent elements computed value of the overflow-x or overflow-y properties is neither visible nor clip.
if (parent()->layout_node()->computed_values().overflow_x() == CSS::Overflow::Visible || parent()->layout_node()->computed_values().overflow_y() == CSS::Overflow::Visible)
return false;
// NOTE: When treating 'overflow:clip' as 'overflow:hidden', we can never fail this condition
if (treat_overflow_clip_on_body_parent_as_overflow_hidden == TreatOverflowClipOnBodyParentAsOverflowHidden::No && (parent()->layout_node()->computed_values().overflow_x() == CSS::Overflow::Clip || parent()->layout_node()->computed_values().overflow_y() == CSS::Overflow::Clip))
return false;
// - bodys computed value of the overflow-x or overflow-y properties is neither visible nor clip.
return layout_node()
&& (parent()->layout_node()
&& parent()->layout_node()->computed_values().overflow_x() != CSS::Overflow::Visible && parent()->layout_node()->computed_values().overflow_x() != CSS::Overflow::Clip
&& parent()->layout_node()->computed_values().overflow_y() != CSS::Overflow::Visible && parent()->layout_node()->computed_values().overflow_y() != CSS::Overflow::Clip)
&& (layout_node()->computed_values().overflow_x() != CSS::Overflow::Visible && layout_node()->computed_values().overflow_x() != CSS::Overflow::Clip
&& layout_node()->computed_values().overflow_y() != CSS::Overflow::Visible && layout_node()->computed_values().overflow_y() != CSS::Overflow::Clip);
if (first_is_one_of(layout_node()->computed_values().overflow_x(), CSS::Overflow::Visible, CSS::Overflow::Clip) || first_is_one_of(layout_node()->computed_values().overflow_y(), CSS::Overflow::Visible, CSS::Overflow::Clip))
return false;
return true;
}
// https://drafts.csswg.org/cssom-view/#dom-element-scrolltop

View file

@ -335,7 +335,11 @@ public:
i32 tab_index() const;
void set_tab_index(i32 tab_index);
bool is_potentially_scrollable() const;
enum class TreatOverflowClipOnBodyParentAsOverflowHidden {
No,
Yes,
};
bool is_potentially_scrollable(TreatOverflowClipOnBodyParentAsOverflowHidden) const;
double scroll_top() const;
double scroll_left() const;

View file

@ -0,0 +1,14 @@
Harness status: OK
Found 8 tests
4 Pass
4 Fail
Fail scrollingElement in quirks mode
Pass scrollingElement in no-quirks mode
Fail scrollingElement in quirks mode (body table)
Pass scrollingElement in no-quirks mode (body table)
Fail scrollingElement in quirks mode (root table)
Pass scrollingElement in no-quirks mode (root table)
Fail scrollingElement in quirks mode (root table, body table)
Pass scrollingElement in no-quirks mode (root table, body table)

View file

@ -0,0 +1,138 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>cssom-view - scrollingElement</title>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<div id="log"></div>
<script>
function makeDescription(rootDisplay, bodyDisplay) {
let a = [];
if (rootDisplay) {
a.push(`root ${rootDisplay}`);
}
if (bodyDisplay) {
a.push(`body ${bodyDisplay}`);
}
let s = a.join(", ");
if (s) {
s = ` (${s})`;
}
return s;
}
function quirksTest(rootDisplay, bodyDisplay) {
async_test(function() {
let quirksFrame = document.createElement("iframe");
quirksFrame.onload = this.step_func_done(function() {
var quirksDoc = quirksFrame.contentDocument;
assert_equals(quirksDoc.compatMode, "BackCompat", "Should be in quirks mode.");
assert_not_equals(quirksDoc.body, null, "Should have a body element");
quirksDoc.documentElement.style.display = rootDisplay;
quirksDoc.body.style.display = bodyDisplay;
// Tests for quirks mode document.
assert_equals(quirksDoc.scrollingElement, quirksDoc.body,
"scrollingElement in quirks mode should default to body element.");
quirksDoc.documentElement.style.overflow = "clip";
quirksDoc.body.style.overflow = "auto";
assert_equals(quirksDoc.scrollingElement, null);
quirksDoc.body.style.overflow = "hidden";
assert_equals(quirksDoc.scrollingElement, null);
quirksDoc.body.style.overflow = "scroll";
assert_equals(quirksDoc.scrollingElement, null);
quirksDoc.body.style.overflow = "visible";
assert_equals(quirksDoc.scrollingElement, quirksDoc.body);
quirksDoc.body.style.overflow = "clip";
assert_equals(quirksDoc.scrollingElement, quirksDoc.body);
quirksDoc.documentElement.style.overflow = "scroll";
quirksDoc.body.style.overflow = "scroll";
assert_equals(quirksDoc.scrollingElement, null,
"scrollingElement in quirks mode should be null if overflow of body and root element isn't visible.");
quirksDoc.documentElement.style.overflow = "visible";
assert_equals(quirksDoc.scrollingElement, quirksDoc.body);
quirksDoc.documentElement.style.overflow = "scroll";
quirksDoc.body.style.overflow = "visible";
assert_equals(quirksDoc.scrollingElement, quirksDoc.body);
quirksDoc.documentElement.style.overflow = "visible";
assert_equals(quirksDoc.scrollingElement, quirksDoc.body);
quirksDoc.body.style.display = "none";
assert_equals(quirksDoc.scrollingElement, quirksDoc.body)
quirksDoc.body.style.display = "block";
assert_equals(quirksDoc.scrollingElement, quirksDoc.body);
quirksDoc.documentElement.appendChild(quirksDoc.createElement("body"));
assert_equals(quirksDoc.scrollingElement, quirksDoc.body);
assert_equals(quirksDoc.scrollingElement, quirksDoc.getElementsByTagName("body")[0]);
quirksDoc.documentElement.removeChild(quirksDoc.documentElement.lastChild);
assert_equals(quirksDoc.scrollingElement, quirksDoc.body);
quirksDoc.documentElement.removeChild(quirksDoc.body);
assert_equals(quirksDoc.scrollingElement, null);
quirksDoc.documentElement.appendChild(quirksDoc.createElementNS("foobarNS", "body"));
assert_equals(quirksDoc.scrollingElement, null);
quirksDoc.removeChild(quirksDoc.documentElement);
assert_equals(quirksDoc.scrollingElement, null);
quirksDoc.appendChild(quirksDoc.createElementNS("foobarNS", "html"));
quirksDoc.documentElement.appendChild(quirksDoc.createElement("body"));
assert_equals(quirksDoc.scrollingElement, null);
quirksDoc.removeChild(quirksDoc.documentElement);
quirksDoc.appendChild(quirksDoc.createElement("body"));
assert_equals(quirksDoc.scrollingElement, null);
quirksFrame.remove();
});
quirksFrame.src =
URL.createObjectURL(new Blob([], { type: "text/html" }));
document.body.append(quirksFrame);
}, `scrollingElement in quirks mode${makeDescription(rootDisplay, bodyDisplay)}`);
}
function nonQuirksTest(rootDisplay, bodyDisplay) {
async_test(function() {
let nonQuirksFrame = document.createElement("iframe");
nonQuirksFrame.onload = this.step_func_done(function() {
var nonQuirksDoc = nonQuirksFrame.contentDocument;
assert_equals(nonQuirksDoc.compatMode, "CSS1Compat", "Should be in standards mode.");
assert_not_equals(nonQuirksDoc.body, null, "Should have a body element");
nonQuirksDoc.documentElement.style.display = rootDisplay;
nonQuirksDoc.body.style.display = bodyDisplay;
assert_equals(nonQuirksDoc.scrollingElement, nonQuirksDoc.documentElement,
"scrollingElement in standards mode should be the document element.");
for (let rootOverflow of ["", "clip", "scroll", "hidden", "visible"]) {
for (let bodyOverflow of ["", "clip", "scroll", "hidden", "visible"]) {
nonQuirksDoc.documentElement.style.overflow = rootOverflow;
nonQuirksDoc.body.style.overflow = bodyOverflow;
assert_equals(nonQuirksDoc.scrollingElement, nonQuirksDoc.documentElement);
}
}
nonQuirksDoc.removeChild(nonQuirksDoc.documentElement);
assert_equals(nonQuirksDoc.scrollingElement, null);
nonQuirksDoc.appendChild(nonQuirksDoc.createElement("foobar"));
assert_equals(nonQuirksDoc.scrollingElement.localName, "foobar");
nonQuirksFrame.remove();
});
nonQuirksFrame.src =
URL.createObjectURL(new Blob([`<!doctype html>`], { type: "text/html" }));
document.body.append(nonQuirksFrame);
}, `scrollingElement in no-quirks mode ${makeDescription(rootDisplay, bodyDisplay)}`);
}
for (let rootDisplay of ["", "table"]) {
for (let bodyDisplay of ["", "table"]) {
quirksTest(rootDisplay, bodyDisplay);
nonQuirksTest(rootDisplay, bodyDisplay);
}
}
</script>