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 {