diff --git a/src/SemanticTree.zig b/src/SemanticTree.zig index 1a0910d2..b60b3ea6 100644 --- a/src/SemanticTree.zig +++ b/src/SemanticTree.zig @@ -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; } } diff --git a/src/browser/interactive.zig b/src/browser/interactive.zig index 2d03db51..fa45257f 100644 --- a/src/browser/interactive.zig +++ b/src/browser/interactive.zig @@ -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(""); + try testing.expectEqual(0, elements.len); +} + test "browser.interactive: non-interactive div" { const elements = try testInteractive("
Just text
"); try testing.expectEqual(0, elements.len); diff --git a/src/browser/tests/css.html b/src/browser/tests/css.html index 70fdff47..ede62285 100644 --- a/src/browser/tests/css.html +++ b/src/browser/tests/css.html @@ -77,3 +77,33 @@ testing.expectEqual('\uFFFDabc', CSS.escape('\x00abc')); } + + diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 0d41ee1f..9fa62280 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -1042,40 +1042,56 @@ pub fn parentElement(self: *Element) ?*Element { } const CSSStyleRule = @import("css/CSSStyleRule.zig"); +const StyleSheetList = @import("css/StyleSheetList.zig"); + +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 (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; + } + } + + if (doc_sheets) |sheets| { + for (0..sheets.length()) |i| { + const sheet = sheets.item(i) orelse continue; + const rules = sheet.getCssRules(page) catch continue; + for (0..rules.length()) |j| { + const rule = rules.item(j) orelse continue; + if (rule.is(CSSStyleRule)) |style_rule| { + const selector = style_rule.getSelectorText(); + if (el.matches(selector, page) catch false) { + const style = (style_rule.getStyle(page) catch continue).asCSSStyleDeclaration(); + 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 (el.getStyle(page)) |style| { - const display = style.asCSSStyleDeclaration().getPropertyValue("display", page); - if (std.mem.eql(u8, display, "none")) { - return false; - } - } - - // 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; - const rules = sheet.getCssRules(page) catch continue; - for (0..rules.length()) |j| { - 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) { - const style = (style_rule.getStyle(page) catch continue).asCSSStyleDeclaration(); - const display = style.getPropertyValue("display", page); - if (std.mem.eql(u8, display, "none")) { - return false; - } - } - } - } - } - } + 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(); }