LibWeb: Account for visual viewport when scrolling a target into view

This ensures the correct portion of the screen is scrolled into view
when the window is pinch-zoomed.
This commit is contained in:
Tim Ledbetter 2026-06-22 09:00:28 +01:00 committed by Andreas Kling
parent 5440f0797b
commit a87f7b9561
3 changed files with 56 additions and 3 deletions

View file

@ -50,6 +50,7 @@
#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
#include <LibWeb/CSS/StyleValues/RandomValueSharingStyleValue.h>
#include <LibWeb/CSS/StyleValues/StyleValueList.h>
#include <LibWeb/CSS/VisualViewport.h>
#include <LibWeb/DOM/AbstractElement.h>
#include <LibWeb/DOM/Attr.h>
#include <LibWeb/DOM/DOMTokenList.h>
@ -2896,9 +2897,16 @@ static CSSPixelPoint determine_the_scroll_into_view_position(Element& target, Bi
CSSPixelRect scrolling_box_rect;
CSSPixelPoint current_scroll_position;
if (scrolling_box.is_document()) {
// NOTE: Element::getBoundingClientRect() returns coordinates relative to the viewport.
scrolling_box_rect = { {}, scrolling_box.document().viewport_rect().size() };
current_scroll_position = scrolling_box.document().navigable()->viewport_scroll_offset();
auto& document = scrolling_box.document();
auto& visual_viewport = *document.visual_viewport();
// NB: Use the visual viewport as the scrolling box, this ensures that the target is scrolled into the visible
// region on screen when the page is pinch-zoomed.
CSSPixelSize visible_size {
CSSPixels::nearest_value_for(visual_viewport.width()),
CSSPixels::nearest_value_for(visual_viewport.height()),
};
scrolling_box_rect = { visual_viewport.offset(), visible_size };
current_scroll_position = document.navigable()->viewport_scroll_offset() + visual_viewport.offset();
} else if (auto paintable_box = scrolling_box.paintable_box()) {
current_scroll_position = paintable_box->scroll_offset();
scrolling_box_rect = paintable_box->absolute_rect();

View file

@ -0,0 +1,2 @@
target visible before scrollIntoView: false
target visible after scrollIntoView: true

View file

@ -0,0 +1,43 @@
<!DOCTYPE html>
<style>
body {
margin: 0;
}
#target {
position: absolute;
top: 450px;
width: 100px;
height: 100px;
}
.spacer {
height: 3000px;
}
</style>
<div id="target"></div>
<div class="spacer"></div>
<script src="include.js"></script>
<script>
// scrollIntoView() must account for the visual viewport when the page is pinch-zoomed: a target inside the
// layout viewport but below the smaller visible region must still be scrolled into view.
test(() => {
const visualViewport = window.visualViewport;
const target = document.getElementById("target");
// getBoundingClientRect() is relative to the layout viewport; the visible region is the visual viewport,
// which starts at offsetTop and is height tall.
function isTargetVisible() {
const rect = target.getBoundingClientRect();
return rect.top >= visualViewport.offsetTop - 0.5
&& rect.bottom <= visualViewport.offsetTop + visualViewport.height + 0.5;
}
// Zoom in near the top so the visible region only covers the top of the page.
internals.pinch(10, 10, 1.0);
println(`target visible before scrollIntoView: ${isTargetVisible()}`);
target.scrollIntoView({ block: "nearest", behavior: "instant" });
println(`target visible after scrollIntoView: ${isTargetVisible()}`);
});
</script>