LibWeb: Apply scroll margin in IntersectionObserver compute_intersection

Per the spec, the observer's [[scrollMargin]] should be applied to
each scroll container's scrollport when walking the containing block
chain. This expands the effective clip rect, allowing targets to be
detected as intersecting before they actually enter the visible area
of the scroll container.

Add a scroll_margin_values() accessor to IntersectionObserver so the
raw LengthPercentage values can be used during intersection computation.
The scroll margin is applied by inflating the scroll container's
padding box rect before clipping the intersection rect against it.
This commit is contained in:
Andreas Kling 2026-03-22 08:58:44 -05:00 committed by Andreas Kling
parent c871c56178
commit 9abb7e4517
3 changed files with 17 additions and 2 deletions

View file

@ -5217,7 +5217,9 @@ static CSSPixelRect compute_intersection(GC::Ref<Element> target, IntersectionOb
// unnecessary here because get_bounding_client_rect() and transform_rect_to_viewport()
// already produce viewport-relative coordinates.
// 3.3. If container has a content clip or a css clip-path property, update intersectionRect
// 3.3. If container is a scroll container, apply the observers [[scrollMargin]]
// to the containers clip rect.
// 3.4. If container has a content clip or a css clip-path property, update intersectionRect
// by applying containers clip.
// FIXME: Handle clip-path.
auto overflow_x = container->computed_values().overflow_x();
@ -5225,6 +5227,18 @@ static CSSPixelRect compute_intersection(GC::Ref<Element> target, IntersectionOb
bool has_content_clip = overflow_x != CSS::Overflow::Visible || overflow_y != CSS::Overflow::Visible;
if (has_content_clip) {
auto clip_rect = container->transform_rect_to_viewport(container->absolute_padding_box_rect());
// Apply scroll margin to expand the scrollport for scroll containers.
auto& scroll_margin = observer.scroll_margin_values();
auto const& layout_node = container->layout_node_with_style_and_box_metrics();
if (layout_node.is_scroll_container() && !scroll_margin.is_empty()) {
clip_rect.inflate(
scroll_margin[0].to_px(layout_node, clip_rect.height()),
scroll_margin[1].to_px(layout_node, clip_rect.width()),
scroll_margin[2].to_px(layout_node, clip_rect.height()),
scroll_margin[3].to_px(layout_node, clip_rect.width()));
}
intersection_rect.intersect(clip_rect);
}
}

View file

@ -45,6 +45,7 @@ public:
Variant<GC::Root<DOM::Element>, GC::Root<DOM::Document>, Empty> root() const;
String root_margin() const;
String scroll_margin() const;
Vector<CSS::LengthPercentage> const& scroll_margin_values() const { return m_scroll_margin; }
Vector<double> const& thresholds() const { return m_thresholds; }
long delay() const { return m_delay; }
bool track_visibility() const { return m_track_visibility; }

View file

@ -1,2 +1,2 @@
no-margin: ratio=0.1875
with-margin: ratio=0.1875
with-margin: ratio=1