Explorar el Código

LibWeb: Fix getBoundingClientRect() for elements with "position: sticky"

Use offset from ScrollFrame which is an actual value a box is shifted by
while painting.

Also change `update_paint_and_hit_testing_properties_if_needed()` to
refresh scroll frames state, because `getBoundingClientRect()` now
depends on them.

Fixes wrong file tree sidebar location and excessive layout
invalidations caused by some miscalculation on JS-side when wrong
bounding client rect is provided on Github PR pages like
https://github.com/LadybirdBrowser/ladybird/pull/1232/files
Aliaksandr Kalenik hace 11 meses
padre
commit
20f68106a7

+ 1 - 0
Tests/LibWeb/Text/expected/element-get-bounding-client-rect-of-sticky.txt

@@ -0,0 +1 @@
+  Sticky Element      Bounding Client Rect: top=50, left=10, width=500, height=57

+ 47 - 0
Tests/LibWeb/Text/input/element-get-bounding-client-rect-of-sticky.html

@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<style>
+    body {
+        margin: 0;
+    }
+
+    #scrollable {
+        width: 300px;
+        height: 200px;
+        overflow: auto;
+        position: relative;
+    }
+
+    #inner-content {
+        width: 500px;
+        height: 800px;
+        padding: 10px;
+        background-color: #f06;
+    }
+
+    #sticky-element {
+        position: sticky;
+        top: 50px;
+        padding: 20px;
+        background-color: #4caf50;
+        color: white;
+        font-weight: bold;
+        text-align: center;
+    }
+</style>
+<div id="scrollable">
+    <div id="inner-content">
+        <div id="sticky-element">Sticky Element</div>
+        <div style="height: 600px"></div>
+    </div>
+</div>
+<script src="include.js"></script>
+<script>
+    test(() => {
+        const stickyElement = document.getElementById("sticky-element");
+        const boundingRect = stickyElement.getBoundingClientRect();
+
+        const result = document.createElement("p");
+        result.textContent = `Bounding Client Rect: top=${boundingRect.top}, left=${boundingRect.left}, width=${boundingRect.width}, height=${boundingRect.height}`;
+        document.body.appendChild(result);
+    });
+</script>

+ 6 - 1
Userland/Libraries/LibWeb/DOM/Document.cpp

@@ -1291,11 +1291,16 @@ void Document::update_animated_style_if_needed()
 
 void Document::update_paint_and_hit_testing_properties_if_needed()
 {
+    if (auto* paintable = this->paintable()) {
+        paintable->refresh_scroll_state();
+    }
+
     if (!m_needs_to_resolve_paint_only_properties)
         return;
     m_needs_to_resolve_paint_only_properties = false;
-    if (auto* paintable = this->paintable())
+    if (auto* paintable = this->paintable()) {
         paintable->resolve_paint_only_properties();
+    }
 }
 
 void Document::set_normal_link_color(Color color)

+ 10 - 5
Userland/Libraries/LibWeb/DOM/Element.cpp

@@ -974,7 +974,6 @@ JS::NonnullGCPtr<Geometry::DOMRectList> Element::get_client_rects() const
     //          or inline-table include both the table box and the caption box, if any, but not the anonymous container box.
     // FIXME: - Replace each anonymous block box with its child box(es) and repeat this until no anonymous block boxes
     //          are left in the final list.
-    auto viewport_offset = navigable->viewport_scroll_offset();
 
     // NOTE: Make sure CSS transforms are resolved before it is used to calculate the rect position.
     const_cast<Document&>(document()).update_paint_and_hit_testing_properties_if_needed();
@@ -988,21 +987,27 @@ JS::NonnullGCPtr<Geometry::DOMRectList> Element::get_client_rects() const
         transform = Gfx::extract_2d_affine_transform(paintable_box->transform());
         for (auto const* containing_block = paintable->containing_block(); !containing_block->is_viewport(); containing_block = containing_block->containing_block()) {
             transform = Gfx::extract_2d_affine_transform(containing_block->transform()).multiply(transform);
-            scroll_offset.translate_by(containing_block->scroll_offset());
+        }
+
+        if (auto enclosing_scroll_offset = paintable_box->enclosing_scroll_frame(); enclosing_scroll_offset) {
+            scroll_offset.translate_by(-enclosing_scroll_offset->cumulative_offset());
         }
 
         auto absolute_rect = paintable_box->absolute_border_box_rect();
         auto transformed_rect = transform.map(absolute_rect.translated(-paintable_box->transform_origin()).to_type<float>())
                                     .to_type<CSSPixels>()
                                     .translated(paintable_box->transform_origin())
-                                    .translated(-scroll_offset)
-                                    .translated(-viewport_offset);
+                                    .translated(-scroll_offset);
         rects.append(Geometry::DOMRect::create(realm(), transformed_rect.to_type<float>()));
     } else if (paintable && is<Painting::InlinePaintable>(*paintable)) {
         auto const& inline_paintable = static_cast<Painting::InlinePaintable const&>(*paintable);
+
+        if (auto enclosing_scroll_offset = inline_paintable.enclosing_scroll_frame(); enclosing_scroll_offset) {
+            scroll_offset.translate_by(-enclosing_scroll_offset->cumulative_offset());
+        }
+
         auto absolute_rect = inline_paintable.bounding_rect();
         absolute_rect.translate_by(-scroll_offset);
-        absolute_rect.translate_by(-viewport_offset);
         rects.append(Geometry::DOMRect::create(realm(), transform.map(absolute_rect.to_type<float>())));
     } else if (paintable) {
         dbgln("FIXME: Failed to get client rects for element ({})", debug_description());