script: Use HTMLElement.scrollParent to implement Element.scrollIntoView (#39144)

To find scrolling ancestors, we need to walk up the flat tree and only
consider the elements that are in the chain of containing block
ancestors of an element. `scrollParent` now does this so we can use it
to properly implement `scrollIntoView`.

Testing: There are WPT tests for this change.

---------

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Oriol Brufau <obrufau@igalia.com>
This commit is contained in:
Martin Robinson 2025-09-07 14:44:11 -07:00 committed by GitHub
parent 286bbe6cb1
commit 9f4f598f44
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 141 additions and 71 deletions

View file

@ -24,7 +24,7 @@ use html5ever::{LocalName, Namespace, Prefix, QualName, local_name, namespace_pr
use js::jsapi::Heap;
use js::jsval::JSVal;
use js::rust::HandleObject;
use layout_api::LayoutDamage;
use layout_api::{LayoutDamage, ScrollContainerQueryType, ScrollContainerResponse};
use net_traits::ReferrerPolicy;
use net_traits::request::CorsSettings;
use selectors::Element as SelectorsElement;
@ -161,7 +161,7 @@ use crate::dom::mutationobserver::{Mutation, MutationObserver};
use crate::dom::namednodemap::NamedNodeMap;
use crate::dom::node::{
BindContext, ChildrenMutation, CloneChildrenFlag, LayoutNodeHelpers, Node, NodeDamage,
NodeFlags, NodeTraits, ShadowIncluding, UnbindContext,
NodeFlags, NodeTraits, ShadowIncluding, UnbindContext, from_untrusted_node_address,
};
use crate::dom::nodelist::NodeList;
use crate::dom::promise::Promise;
@ -290,6 +290,20 @@ impl ScrollingBox {
ScrollingBox::Viewport(document) => document.window().scroll_offset(),
}
}
fn parent(&self) -> Option<ScrollingBox> {
match self {
ScrollingBox::Element(element) => element.scrolling_box(),
ScrollingBox::Viewport(_) => None,
}
}
fn node(&self) -> &Node {
match self {
ScrollingBox::Element(element) => element.upcast(),
ScrollingBox::Viewport(document) => document.upcast(),
}
}
}
//
@ -856,6 +870,21 @@ impl Element {
.retain(|reg_obs| *reg_obs.observer != *observer)
}
#[allow(unsafe_code)]
fn scrolling_box(&self) -> Option<ScrollingBox> {
self.owner_window()
.scroll_container_query(self.upcast(), ScrollContainerQueryType::ForScrollIntoView)
.and_then(|response| match response {
ScrollContainerResponse::Viewport => {
Some(ScrollingBox::Viewport(self.owner_document()))
},
ScrollContainerResponse::Element(parent_node_address) => {
let node = unsafe { from_untrusted_node_address(parent_node_address) };
Some(ScrollingBox::Element(DomRoot::downcast(node)?))
},
})
}
/// <https://drafts.csswg.org/cssom-view/#scroll-a-target-into-view>
fn scroll_into_view_with_options(
&self,
@ -864,30 +893,11 @@ impl Element {
inline: ScrollLogicalPosition,
container: Option<&Element>,
) {
// Shadow-inclusive ancestors of this node, skipping the node itself.
//
// TODO: This should be the ancestors of the node in the flat tree.
let ancestors = self
.upcast::<Node>()
.inclusive_ancestors(ShadowIncluding::Yes)
.skip(1);
// Step 1: For each ancestor element or viewport that establishes a scrolling box `scrolling
// box`, in order of innermost to outermost scrolling box, run these substeps:
for node in ancestors {
if !node.establishes_scrolling_box() {
continue;
}
// TODO: This should skip elements which are not containing blocks of the element we
// are scrolling.
let scrolling_box = if let Some(document) = node.downcast::<Document>() {
ScrollingBox::Viewport(DomRoot::from_ref(document))
} else if let Some(element) = node.downcast::<Element>() {
ScrollingBox::Element(DomRoot::from_ref(element))
} else {
continue;
};
let mut parent_scrolling_box = self.scrolling_box();
while let Some(scrolling_box) = parent_scrolling_box {
parent_scrolling_box = scrolling_box.parent();
// Step 1.1: If the Document associated with `target` is not same origin with the
// Document associated with the element or viewport associated with `scrolling box`,
@ -906,14 +916,15 @@ impl Element {
//
// TODO: Handle smooth scrolling.
if position != scrolling_box.scroll_position() {
match scrolling_box {
match &scrolling_box {
// ↪ If `scrolling box` is associated with an element
ScrollingBox::Element(element) => {
// Perform a scroll of the elements scrolling box to `position`,
// with the `element` as the associated element and `behavior` as the
// scroll behavior.
let window = element.owner_window();
window.scroll_an_element(&element, position.x, position.y, behavior);
element
.owner_window()
.scroll_an_element(element, position.x, position.y, behavior);
},
// ↪ If `scrolling box` is associated with a viewport
ScrollingBox::Viewport(document) => {
@ -932,7 +943,9 @@ impl Element {
// inclusive ancestor of `container`, abort the rest of these steps.
if container.is_some_and(|container| {
let container_node = container.upcast::<Node>();
node.is_shadow_including_inclusive_ancestor_of(container_node)
scrolling_box
.node()
.is_shadow_including_inclusive_ancestor_of(container_node)
}) {
break;
}