From d58cbc54406ffa5a138b47df11ef8308e31c536e Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sat, 25 Jul 2026 08:56:39 +0800 Subject: [PATCH] explicit visibility check on root --- src/browser/tests/element/position.html | 20 ++++++++ src/browser/webapi/Element.zig | 61 ++++++++++++------------- 2 files changed, 48 insertions(+), 33 deletions(-) diff --git a/src/browser/tests/element/position.html b/src/browser/tests/element/position.html index 6a846dfa8..3022384c9 100644 --- a/src/browser/tests/element/position.html +++ b/src/browser/tests/element/position.html @@ -89,6 +89,15 @@ document.body.appendChild(hidden); testing.expectEqual(0, hidden.scrollWidth); + // A visible width:0 box (an off-screen measurement container) is not + // hidden: its content still overflows it. + const zeroWidth = document.createElement('div'); + zeroWidth.style.width = '0'; + zeroWidth.appendChild(document.createElement('span')); + document.body.appendChild(zeroWidth); + testing.expectEqual(0, zeroWidth.clientWidth); + testing.expectTrue(zeroWidth.scrollWidth > 0); + // The root containers keep their synthetic size rather than summing // children, so horizontal page-overflow checks stay stable. testing.expectEqual(document.body.clientWidth, document.body.scrollWidth); @@ -169,6 +178,17 @@ document.body.appendChild(hidden); testing.expectEqual(0, hidden.scrollHeight); + // The collapse/expand idiom: scrollHeight is read while the panel is + // collapsed at height:0 to know how far to open it. A zero-height box is + // not hidden; its content is still measured. + const panel = document.createElement('div'); + panel.style.height = '0'; + panel.appendChild(document.createElement('div')); + panel.appendChild(document.createElement('div')); + document.body.appendChild(panel); + testing.expectEqual(0, panel.clientHeight); + testing.expectTrue(panel.scrollHeight > 0); + // The root containers keep their synthetic size rather than summing // children. This is what keeps infinite-scroll triggers reading // `scrollTop + clientHeight >= scrollHeight` on body/documentElement diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index b1a78ab7c..e44c1f066 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -1377,22 +1377,21 @@ pub fn setScrollLeft(self: *Element, value: i32, frame: *Frame) !void { } pub fn getScrollHeight(self: *Element, frame: *Frame) f64 { - const client_height = self.getClientHeight(frame); - - // Hidden: getClientHeight already reported 0, and a hidden element's - // content doesn't overflow anything. - if (client_height == 0.0) { + var visibility_cache: VisibilityCache = .{}; + if (!self.checkVisibilityCached(&visibility_cache, frame)) { return 0.0; } - switch (self.getTag()) { - // As in getScrollWidth: the root containers carry artificial giant - // defaults, and page-level overflow checks read them. - .html, .body => return client_height, - else => {}, + const height = self.getElementDimensions(frame).height; + + const tag = self.getTag(); + // As in getScrollWidth: the root containers carry artificial giant + // defaults, and page-level overflow checks read them. + if (tag == .html or tag == .body) { + return height; } - return @max(client_height, self.contentHeight(frame)); + return @max(height, self.contentHeight(frame, &visibility_cache)); } // The height of the direct child elements stacked vertically, the counterpart @@ -1405,15 +1404,13 @@ pub fn getScrollHeight(self: *Element, frame: *Frame) f64 { // extent per axis rather than describing one coherent layout: reporting no // overflow when there is some is what wedges measure-then-mutate loops, // while the reverse merely over-reports. -fn contentHeight(self: *Element, frame: *Frame) f64 { +fn contentHeight(self: *Element, frame: *Frame, visibility_cache: *VisibilityCache) f64 { var total: f64 = 0; - var visibility_cache: VisibilityCache = .{}; - var child = self.asNode().firstChild(); while (child) |node| : (child = node.nextSibling()) { if (node.is(Element)) |el| { - if (el.checkVisibilityCached(&visibility_cache, frame)) { + if (el.checkVisibilityCached(visibility_cache, frame)) { total += el.getElementDimensions(frame).height; } } @@ -1423,23 +1420,22 @@ fn contentHeight(self: *Element, frame: *Frame) f64 { } pub fn getScrollWidth(self: *Element, frame: *Frame) f64 { - const client_width = self.getClientWidth(frame); - - // Hidden: getClientWidth already reported 0, and a hidden element's - // content doesn't overflow anything. - if (client_width == 0.0) { + var visibility_cache: VisibilityCache = .{}; + if (!self.checkVisibilityCached(&visibility_cache, frame)) { return 0.0; } - switch (self.getTag()) { - // The root containers carry artificial giant defaults (1920 and - // 100_000_000, see getElementDimensions). Stacking their children on - // top would inflate a value sites read to detect page overflow. - .html, .body => return client_width, - else => {}, + const width = self.getElementDimensions(frame).width; + + const tag = self.getTag(); + // The root containers carry artificial giant defaults (1920 and + // 100_000_000, see getElementDimensions). Stacking their children on + // top would inflate a value sites read to detect page overflow. + if (tag == .html or tag == .body) { + return width; } - return @max(client_width, self.contentWidth(frame)); + return @max(width, self.contentWidth(frame, &visibility_cache)); } // The width of the direct child elements laid end to end on a single row. @@ -1465,17 +1461,16 @@ pub fn getScrollWidth(self: *Element, frame: *Frame) f64 { // would report overflow for practically every element containing text, since a // few words already exceed the default box. Element children are what content // grown by script actually consists of. -fn contentWidth(self: *Element, frame: *Frame) f64 { +fn contentWidth(self: *Element, frame: *Frame, visibility_cache: *VisibilityCache) f64 { var total: f64 = 0; - // Siblings share their entire ancestor chain, so one cache across the loop - // collapses N ancestor walks into one walk plus N own-element checks. - var visibility_cache: VisibilityCache = .{}; - + // The cache arrives seeded by the caller's own visibility walk, and + // siblings share that ancestor chain, so the loop costs one own-element + // check per child rather than N ancestor walks. var child = self.asNode().firstChild(); while (child) |node| : (child = node.nextSibling()) { if (node.is(Element)) |el| { - if (el.checkVisibilityCached(&visibility_cache, frame)) { + if (el.checkVisibilityCached(visibility_cache, frame)) { total += el.getElementDimensions(frame).width; } }