browser: improve visibility and interactivity CSS checks

Adds support for `pointer-events: none` in interactivity classification
and expands `checkVisibility` to include `visibility` and `opacity`.
Refactors CSS property lookup into a shared helper.
This commit is contained in:
Adrià Arrufat
2026-03-13 13:33:33 +09:00
parent e12f28fb70
commit 84d76cf90d
4 changed files with 84 additions and 30 deletions

View File

@@ -133,7 +133,7 @@ fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_nam
}
if (el.is(Element.Html)) |html_el| {
if (interactive.classifyInteractivity(el, html_el, listener_targets) != null) {
if (interactive.classifyInteractivity(self.page, el, html_el, listener_targets) != null) {
is_interactive = true;
}
}

View File

@@ -146,7 +146,7 @@ pub fn collectInteractiveElements(
else => {},
}
const itype = classifyInteractivity(el, html_el, listener_targets) orelse continue;
const itype = classifyInteractivity(page, el, html_el, listener_targets) orelse continue;
const listener_types = getListenerTypes(
el.asEventTarget(),
@@ -210,10 +210,13 @@ pub fn buildListenerTargetMap(page: *Page, arena: Allocator) !ListenerTargetMap
}
pub fn classifyInteractivity(
page: *Page,
el: *Element,
html_el: *Element.Html,
listener_targets: ListenerTargetMap,
) ?InteractivityType {
if (el.hasPointerEventsNone(page)) return null;
// 1. Native interactive by tag
switch (el.getTag()) {
.button, .summary, .details, .select, .textarea => return .native,
@@ -519,6 +522,11 @@ test "browser.interactive: disabled by fieldset" {
try testing.expect(!elements[1].disabled);
}
test "browser.interactive: pointer-events none" {
const elements = try testInteractive("<button style=\"pointer-events: none;\">Click me</button>");
try testing.expectEqual(0, elements.len);
}
test "browser.interactive: non-interactive div" {
const elements = try testInteractive("<div>Just text</div>");
try testing.expectEqual(0, elements.len);

View File

@@ -77,3 +77,33 @@
testing.expectEqual('\uFFFDabc', CSS.escape('\x00abc'));
}
</script>
<script id="checkVisibility">
{
const el = document.createElement("div");
document.documentElement.appendChild(el);
testing.expectEqual(true, el.checkVisibility());
el.style.display = "none";
testing.expectEqual(false, el.checkVisibility());
el.style.display = "block";
testing.expectEqual(true, el.checkVisibility());
el.style.visibility = "hidden";
testing.expectEqual(false, el.checkVisibility());
el.style.visibility = "collapse";
testing.expectEqual(false, el.checkVisibility());
el.style.visibility = "visible";
testing.expectEqual(true, el.checkVisibility());
el.style.opacity = "0";
testing.expectEqual(false, el.checkVisibility());
el.style.opacity = "1";
testing.expectEqual(true, el.checkVisibility());
document.documentElement.removeChild(el);
}
</script>

View File

@@ -1042,20 +1042,26 @@ pub fn parentElement(self: *Element) ?*Element {
}
const CSSStyleRule = @import("css/CSSStyleRule.zig");
const StyleSheetList = @import("css/StyleSheetList.zig");
pub fn checkVisibility(self: *Element, page: *Page) bool {
pub fn hasPointerEventsNone(self: *Element, page: *Page) bool {
const doc_sheets = page.document.getStyleSheets(page) catch null;
var current: ?*Element = self;
while (current) |el| {
if (el.getStyle(page)) |style| {
const display = style.asCSSStyleDeclaration().getPropertyValue("display", page);
if (std.mem.eql(u8, display, "none")) {
if (checkCssProperty(el, page, doc_sheets, "pointer-events", &[_][]const u8{"none"})) return true;
current = el.parentElement();
}
return false;
}
fn checkCssProperty(el: *Element, page: *Page, doc_sheets: ?*StyleSheetList, property_name: []const u8, target_values: []const []const u8) bool {
if (el.getOrCreateStyle(page) catch null) |style| {
const val = style.asCSSStyleDeclaration().getPropertyValue(property_name, page);
for (target_values) |target| {
if (std.mem.eql(u8, val, target)) return true;
}
}
// Also check if any global stylesheet hides this element
if (doc_sheets) |sheets| {
for (0..sheets.length()) |i| {
const sheet = sheets.item(i) orelse continue;
@@ -1064,18 +1070,28 @@ pub fn checkVisibility(self: *Element, page: *Page) bool {
const rule = rules.item(j) orelse continue;
if (rule.is(CSSStyleRule)) |style_rule| {
const selector = style_rule.getSelectorText();
const does_match = el.matches(selector, page) catch false;
if (does_match) {
if (el.matches(selector, page) catch false) {
const style = (style_rule.getStyle(page) catch continue).asCSSStyleDeclaration();
const display = style.getPropertyValue("display", page);
if (std.mem.eql(u8, display, "none")) {
const val = style.getPropertyValue(property_name, page);
for (target_values) |target| {
if (std.mem.eql(u8, val, target)) return true;
}
}
}
}
}
}
return false;
}
}
}
}
}
}
}
pub fn checkVisibility(self: *Element, page: *Page) bool {
const doc_sheets = page.document.getStyleSheets(page) catch null;
var current: ?*Element = self;
while (current) |el| {
if (checkCssProperty(el, page, doc_sheets, "display", &[_][]const u8{"none"})) return false;
if (checkCssProperty(el, page, doc_sheets, "visibility", &[_][]const u8{ "hidden", "collapse" })) return false;
if (checkCssProperty(el, page, doc_sheets, "opacity", &[_][]const u8{"0"})) return false;
current = el.parentElement();
}