diff --git a/src/browser/actions.zig b/src/browser/actions.zig index 19a51d70f..40d66d523 100644 --- a/src/browser/actions.zig +++ b/src/browser/actions.zig @@ -46,7 +46,7 @@ fn dispatch(el: *Element, event: *Event, comptime typ: []const u8, frame: *Frame }; } -// Dispatches a trusted pointer event and reports whether it was cancelled. +/// Dispatches a trusted pointer event and reports whether it was cancelled. fn dispatchPointer(el: *Element, comptime typ: []const u8, buttons: u16, detail: u32, frame: *Frame) !bool { const event: *PointerEvent = try .initTrusted(typ, .{ .bubbles = true, @@ -70,7 +70,7 @@ fn dispatchPointer(el: *Element, comptime typ: []const u8, buttons: u16, detail: return base_event.getDefaultPrevented(); } -fn dispatchMouse(el: *Element, comptime typ: []const u8, buttons: u16, frame: *Frame) !void { +fn dispatchMouse(el: *Element, comptime typ: []const u8, buttons: u16, frame: *Frame) !bool { const event: *MouseEvent = try .initTrusted(comptime .wrap(typ), .{ .bubbles = true, .cancelable = true, @@ -78,7 +78,13 @@ fn dispatchMouse(el: *Element, comptime typ: []const u8, buttons: u16, frame: *F .buttons = buttons, .detail = 1, }, frame); - try dispatch(el, event.asEvent(), typ, frame); + + const base_event = event.asEvent(); + base_event.acquireRef(); + defer base_event.releaseRef(frame._page); + + try dispatch(el, base_event, typ, frame); + return base_event.getDefaultPrevented(); } /// A full trusted primary-button click sequence, as a real user click would @@ -97,15 +103,17 @@ pub fn click(node: *DOMNode, frame: *Frame) !void { // still fires. const suppress_mouse = try dispatchPointer(el, "pointerdown", 1, 0, frame); if (!suppress_mouse) { - try dispatchMouse(el, "mousedown", 1, frame); - Frame.user_input.focusForMouseDown(frame, el) catch |err| { - lp.log.warn(.app, "click mousedown focus", .{ .err = err }); - }; + const suppress_focus = try dispatchMouse(el, "mousedown", 1, frame); + if (!suppress_focus) { + Frame.user_input.focusForMouseDown(frame, el) catch |err| { + lp.log.warn(.app, "click mousedown focus", .{ .err = err }); + }; + } } _ = try dispatchPointer(el, "pointerup", 0, 0, frame); if (!suppress_mouse) { - try dispatchMouse(el, "mouseup", 0, frame); + _ = try dispatchMouse(el, "mouseup", 0, frame); } _ = try dispatchPointer(el, "click", 0, 1, frame); diff --git a/src/browser/frame/user_input.zig b/src/browser/frame/user_input.zig index 2bffc7347..6a3121b5f 100644 --- a/src/browser/frame/user_input.zig +++ b/src/browser/frame/user_input.zig @@ -180,10 +180,11 @@ fn dispatchBoundaryEvent(frame: *Frame, target: *Element, comptime mouse_typ: [] }; } -// Dispatch a single trusted mouse event of the given type on `target`, carrying -// the pressed button and pointer position. `detail` is the click count (used for -// click/dblclick); 0 for events where it does not apply. -fn dispatchMouseEventOn(frame: *Frame, target: *Element, comptime typ: []const u8, x: f64, y: f64, button: i32, detail: u32) !void { +/// Dispatch a single trusted mouse event of the given type on `target`, carrying +/// the pressed button and pointer position. `detail` is the click count (used for +/// click/dblclick); 0 for events where it does not apply. Reports whether the +/// event was cancelled via preventDefault(). +fn dispatchMouseEventOn(frame: *Frame, target: *Element, comptime typ: []const u8, x: f64, y: f64, button: i32, detail: u32) !bool { const event: *MouseEvent = try .initTrusted(comptime .wrap(typ), .{ .bubbles = true, .cancelable = true, @@ -193,7 +194,13 @@ fn dispatchMouseEventOn(frame: *Frame, target: *Element, comptime typ: []const u .button = button, .detail = detail, }, frame); - try frame._event_manager.dispatch(target.asEventTarget(), event.asEvent()); + + const base_event = event.asEvent(); + base_event.acquireRef(); + defer base_event.releaseRef(frame._page); + + try frame._event_manager.dispatch(target.asEventTarget(), base_event); + return base_event.getDefaultPrevented(); } pub fn triggerMousePress(frame: *Frame, x: f64, y: f64, button: i32) !void { @@ -208,8 +215,10 @@ pub fn triggerMousePress(frame: *Frame, x: f64, y: f64, button: i32) !void { .type = frame._type, }); } - try dispatchMouseEventOn(frame, target, "mousedown", x, y, button, 0); - try focusForMouseDown(frame, target); + const suppressed = try dispatchMouseEventOn(frame, target, "mousedown", x, y, button, 0); + if (!suppressed) { + try focusForMouseDown(frame, target); + } } pub fn triggerMouseMove(frame: *Frame, x: f64, y: f64) !void { @@ -251,19 +260,19 @@ pub fn triggerMouseRelease(frame: *Frame, x: f64, y: f64, button: i32, click_cou const detail: u32 = if (click_count > 0) @intCast(click_count) else 1; - try dispatchMouseEventOn(frame, target, "mouseup", x, y, button, detail); + _ = try dispatchMouseEventOn(frame, target, "mouseup", x, y, button, detail); // After mouseup, the activation event depends on the button. switch (button) { mouse_button.main => { - try dispatchMouseEventOn(frame, target, "click", x, y, button, detail); + _ = try dispatchMouseEventOn(frame, target, "click", x, y, button, detail); // A second click in quick succession also fires dblclick. if (click_count == 2) { - try dispatchMouseEventOn(frame, target, "dblclick", x, y, button, detail); + _ = try dispatchMouseEventOn(frame, target, "dblclick", x, y, button, detail); } }, - mouse_button.auxiliary => try dispatchMouseEventOn(frame, target, "auxclick", x, y, button, detail), - mouse_button.secondary => try dispatchMouseEventOn(frame, target, "contextmenu", x, y, button, detail), + mouse_button.auxiliary => _ = try dispatchMouseEventOn(frame, target, "auxclick", x, y, button, detail), + mouse_button.secondary => _ = try dispatchMouseEventOn(frame, target, "contextmenu", x, y, button, detail), else => {}, } } @@ -353,8 +362,8 @@ fn isEditingHost(node: *Node) bool { return std.ascii.eqlIgnoreCase(value, "false") == false; } -// Find the outermost element of the contiguous editable chain containing the -// target. +/// Find the outermost element of the contiguous editable chain containing the +/// target. fn outermostEditingHost(target: *Element) ?*Element { var node: ?*Node = target.asNode(); var editable: ?*Node = null; @@ -374,31 +383,42 @@ fn outermostEditingHost(target: *Element) ?*Element { return host.is(Element); } -/// `null` means the element is not mouse-focusable. Unlike sequential focus, -/// any explicit tabindex value, including a negative one, is mouse-focusable. -fn mouseFocusTabIndex(el: *Element) ?i32 { - if (el.isDisabled()) return null; - if (el.is(Element.Html) == null) return null; +/// Unlike sequential focus, any explicit, parseable tabindex value, including +/// a negative one, is mouse-focusable โ€” on any element, not just HTML ones, so +/// this is checked before the HTML-only guard below. An unparsable tabindex is +/// treated as if the attribute were absent (HTML ยง6.6.3), so native +/// focusability still applies โ€” mirrors HtmlElement.getTabIndex's +/// parse-failure fallthrough. +fn isMouseFocusable(el: *Element) bool { + if (el.isDisabled()) return false; if (el.getAttributeSafe(comptime .wrap("tabindex"))) |attr| { - return Element.Html.parseInteger(attr) orelse 0; + if (Element.Html.parseInteger(attr)) |_| { + return true; + } } - const native = switch (el.getTag()) { + if (el.is(Element.Html) == null) { + // Mirrors hasClickActivationBehavior: an SVG link is focusable by the + // same href it activates on. + if (el.is(Element.Svg.Graphics.A) != null) { + return svgAnchorHref(el) != null; + } + return false; + } + + return switch (el.getTag()) { .button, .select, .textarea, .iframe => true, .input => el.as(Element.Html.Input)._input_type != .hidden, .anchor, .area => el.getAttributeSafe(comptime .wrap("href")) != null, else => false, }; - return if (native) 0 else null; -} - -fn isMouseFocusable(el: *Element) bool { - return mouseFocusTabIndex(el) != null; } /// Mousedown default action: focus the editing host if the click is inside -/// one, otherwise the nearest mouse-focusable element (self or ancestor). +/// one, otherwise the nearest mouse-focusable element (self or ancestor). If +/// no ancestor is mouse-focusable, blur whatever was previously focused โ€” +/// a mousedown outside any focusable element moves focus to the body. pub fn focusForMouseDown(frame: *Frame, target: *Element) !void { if (outermostEditingHost(target)) |host| { try host.focus(frame); @@ -413,6 +433,11 @@ pub fn focusForMouseDown(frame: *Frame, target: *Element) !void { return; } } + + const doc = target.asNode().ownerDocument(frame) orelse frame.document; + if (doc._active_element) |active| { + try active.blur(frame); + } } // Per the DOM dispatch algorithm, a click's activation target is the event diff --git a/src/browser/tests/mcp_actions.html b/src/browser/tests/mcp_actions.html index 377d39395..2bab8999f 100644 --- a/src/browser/tests/mcp_actions.html +++ b/src/browser/tests/mcp_actions.html @@ -22,7 +22,7 @@
Focus me
-
Not focusable
+
Not focusable