LibWeb: Clip against scroll containers in IntersectionObserver

Implement the containing block chain traversal in compute_intersection
(steps 2-3 of the spec algorithm). Walk from the target's paintable box
up through the containing block chain, clipping the intersection rect
against each ancestor that has a content clip (overflow != visible).

This fixes the case where a target is inside an overflow:hidden or
overflow:clip container and pushed below the visible area. Previously,
the intersection ratio was incorrectly non-zero because we only
intersected with the root bounds (viewport), not intermediate
scroll containers.

Also update the scroll container clipping tests to verify the
intersection ratio (which is what compute_intersection affects)
rather than isIntersecting (which per spec is based on targetRect
vs rootBounds, not the clipped intersection rect).
This commit is contained in:
Andreas Kling
2026-03-22 08:55:30 -05:00
committed by Andreas Kling
parent d1b485e6d6
commit 229eba9a06
Notes: github-actions[bot] 2026-03-22 19:11:11 +00:00
5 changed files with 45 additions and 17 deletions

View File

@@ -23,12 +23,13 @@ body { margin: 0; }
<script>
asyncTest(done => {
// The target is inside an overflow:clip container, pushed below
// the visible area by a spacer. It should NOT be intersecting.
// the visible area by a spacer. The intersection ratio should be 0
// because compute_intersection clips against the container.
let callbackCount = 0;
let observer = new IntersectionObserver(entries => {
callbackCount++;
entries.forEach(entry => {
println(`callback ${callbackCount}: isIntersecting=${entry.isIntersecting}`);
println(`callback ${callbackCount}: ratio=${entry.intersectionRatio}`);
});
if (callbackCount === 1) {
requestAnimationFrame(() => {

View File

@@ -24,13 +24,13 @@ body { margin: 0; }
asyncTest(done => {
// The target is inside a scroll container with overflow:hidden.
// The spacer pushes the target below the scroll container's visible area.
// With the implicit root (viewport), the target should NOT be intersecting
// because it is clipped by the scroll container.
// With the implicit root (viewport), the intersection ratio should be 0
// because compute_intersection clips against the scroll container.
let callbackCount = 0;
let observer = new IntersectionObserver(entries => {
callbackCount++;
entries.forEach(entry => {
println(`callback ${callbackCount}: isIntersecting=${entry.isIntersecting}`);
println(`callback ${callbackCount}: ratio=${entry.intersectionRatio}`);
});
if (callbackCount === 1) {
requestAnimationFrame(() => {