From 0d3055716e3cf263625bfdbb8a0c83a2b7b3036a Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sat, 13 Dec 2025 20:33:43 +0800 Subject: [PATCH] tweak timing of intersection observer and how it handles disconnected nodes --- src/browser/Page.zig | 27 +++++++++++++---- src/browser/js/Context.zig | 9 ++++++ .../tests/intersection_observer/basic.html | 30 +++++++++++++++++++ .../legacy/dom/intersection_observer.html | 5 ++-- src/browser/webapi/IntersectionObserver.zig | 22 ++++++++------ 5 files changed, 75 insertions(+), 18 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index fab0690b..f6eb4b06 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -106,6 +106,7 @@ _mutation_delivery_depth: u32 = 0, // List of active IntersectionObservers _intersection_observers: std.ArrayList(*IntersectionObserver) = .{}, +_intersection_check_scheduled: bool = false, _intersection_delivery_scheduled: bool = false, // Lookup for customized built-in elements. Maps element pointer to definition. @@ -250,6 +251,7 @@ fn reset(self: *Page, comptime initializing: bool) !void { self._mutation_delivery_scheduled = false; self._mutation_delivery_depth = 0; self._intersection_observers = .{}; + self._intersection_check_scheduled = false; self._intersection_delivery_scheduled = false; self._customized_builtin_definitions = .{}; self._customized_builtin_connected_callback_invoked = .{}; @@ -781,6 +783,15 @@ pub fn scriptAddedCallback(self: *Page, script: *HtmlScript) !void { pub fn domChanged(self: *Page) void { self.version += 1; + + if (self._intersection_check_scheduled) { + return; + } + + self._intersection_check_scheduled = true; + self.js.queueIntersectionChecks() catch |err| { + log.err(.page, "page.schedIntersectChecks", .{ .err = err }); + }; } pub fn getElementIdMap(page: *Page, node: *Node) *std.StringHashMapUnmanaged(*Element) { @@ -849,27 +860,31 @@ pub fn checkIntersections(self: *Page) !void { } pub fn scheduleMutationDelivery(self: *Page) !void { - // Only queue if not already scheduled if (self._mutation_delivery_scheduled) { return; } self._mutation_delivery_scheduled = true; - - // Queue mutation delivery as a microtask try self.js.queueMutationDelivery(); } pub fn scheduleIntersectionDelivery(self: *Page) !void { - // Only queue if not already scheduled if (self._intersection_delivery_scheduled) { return; } self._intersection_delivery_scheduled = true; - - // Queue intersection delivery as a microtask try self.js.queueIntersectionDelivery(); } +pub fn performScheduledIntersectionChecks(self: *Page) void { + if (!self._intersection_check_scheduled) { + return; + } + self._intersection_check_scheduled = false; + self.checkIntersections() catch |err| { + log.err(.page, "page.schedIntersectChecks", .{ .err = err }); + }; +} + pub fn deliverIntersections(self: *Page) void { if (!self._intersection_delivery_scheduled) { return; diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index 904ab17f..63a6d751 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -1973,6 +1973,15 @@ pub fn queueMutationDelivery(self: *Context) !void { }.run, self.page); } +pub fn queueIntersectionChecks(self: *Context) !void { + self.isolate.enqueueMicrotask(struct { + fn run(data: ?*anyopaque) callconv(.c) void { + const page: *Page = @ptrCast(@alignCast(data.?)); + page.performScheduledIntersectionChecks(); + } + }.run, self.page); +} + pub fn queueIntersectionDelivery(self: *Context) !void { self.isolate.enqueueMicrotask(struct { fn run(data: ?*anyopaque) callconv(.c) void { diff --git a/src/browser/tests/intersection_observer/basic.html b/src/browser/tests/intersection_observer/basic.html index 4131d1dd..dde36231 100644 --- a/src/browser/tests/intersection_observer/basic.html +++ b/src/browser/tests/intersection_observer/basic.html @@ -29,3 +29,33 @@ observer.disconnect(); }); + + diff --git a/src/browser/tests/legacy/dom/intersection_observer.html b/src/browser/tests/legacy/dom/intersection_observer.html index 4067edba..9f4b88ec 100644 --- a/src/browser/tests/legacy/dom/intersection_observer.html +++ b/src/browser/tests/legacy/dom/intersection_observer.html @@ -22,7 +22,6 @@ const div1 = document.createElement('div'); const div2 = document.createElement('div'); new IntersectionObserver((entries) => { - console.log(entries[0]); count += 1; }).observe(div1); @@ -33,7 +32,7 @@ } - --> diff --git a/src/browser/webapi/IntersectionObserver.zig b/src/browser/webapi/IntersectionObserver.zig index 4666e526..7cf65d0c 100644 --- a/src/browser/webapi/IntersectionObserver.zig +++ b/src/browser/webapi/IntersectionObserver.zig @@ -161,11 +161,13 @@ fn calculateIntersection( // For a headless browser without real layout, we treat all elements as fully visible. // This avoids fingerprinting issues (massive viewports) and matches the behavior // scripts expect when querying element visibility. - const is_intersecting = true; - const intersection_ratio: f64 = 1.0; + // However, elements without a parent cannot intersect (they have no containing block). + const has_parent = target.asNode().parentNode() != null; + const is_intersecting = has_parent; + const intersection_ratio: f64 = if (has_parent) 1.0 else 0.0; - // Intersection rect is the same as the target rect (fully visible) - const intersection_rect = target_rect; + // Intersection rect is the same as the target rect if visible, otherwise zero rect + const intersection_rect = if (has_parent) target_rect else &zero_rect; return .{ .is_intersecting = is_intersecting, @@ -199,11 +201,10 @@ fn checkIntersection(self: *IntersectionObserver, target: *Element, page: *Page) const is_now_intersecting = data.is_intersecting and self.meetsThreshold(data.intersection_ratio); // Create entry if: - // 1. First time observing this target (was_intersecting_opt == null) + // 1. First time observing this target AND it's intersecting // 2. State changed - // 3. Currently intersecting - const should_report = was_intersecting_opt == null or - was_intersecting_opt.? != is_now_intersecting; + const should_report = (was_intersecting_opt == null and is_now_intersecting) or + (was_intersecting_opt != null and was_intersecting_opt.? != is_now_intersecting); if (should_report) { const entry = try page.arena.create(IntersectionObserverEntry); @@ -218,8 +219,11 @@ fn checkIntersection(self: *IntersectionObserver, target: *Element, page: *Page) }; try self._pending_entries.append(page.arena, entry); - try self._previous_states.put(page.arena, target, is_now_intersecting); } + + // Always update the previous state, even if we didn't report + // This ensures we can detect state changes on subsequent checks + try self._previous_states.put(page.arena, target, is_now_intersecting); } pub fn checkIntersections(self: *IntersectionObserver, page: *Page) !void {