diff --git a/src/browser/frame/user_input.zig b/src/browser/frame/user_input.zig
index 961a47ff0..92c29d59e 100644
--- a/src/browser/frame/user_input.zig
+++ b/src/browser/frame/user_input.zig
@@ -509,7 +509,7 @@ fn deltaToScroll(d: f64) i32 {
fn hasClickActivationBehavior(node: *Node) bool {
const element = node.is(Element) orelse return false;
- const html_element = element.is(Element.Html) orelse return isSvgLink(element);
+ const html_element = element.is(Element.Html) orelse return element.isSvgLink();
return switch (html_element._type) {
.anchor => element.getAttributeInterned("href") != null,
@@ -519,36 +519,11 @@ fn hasClickActivationBehavior(node: *Node) bool {
};
}
-// SVG 2 links via `href`; xlink:href is the deprecated SVG 1.1 spelling.
-fn svgAnchorHref(element: *Element) ?[]const u8 {
- return element.getAttributeInterned("href") orelse element.getAttributeSafe(comptime .wrap("xlink:href"));
-}
-
-fn isSvgLink(element: *Element) bool {
- return element.is(Element.Svg.Graphics.A) != null and svgAnchorHref(element) != null;
-}
-
-/// Focusable without a tabindex attribute.
-fn isNativelyFocusable(el: *Element) bool {
- if (el.is(Element.Html) == null) {
- return isSvgLink(el);
- }
- return switch (el.getTag()) {
- .button, .select, .textarea, .iframe => true,
- .input => el.as(Element.Html.Input)._input_type != .hidden,
- .anchor, .area => el.getAttributeInterned("href") != null,
- else => false,
- };
-}
-
// Clicks on editable content are for editing: they don't activate the
// element or any enclosing link.
-// "contenteditable" is 15 bytes — past the comptime SSO limit — so the
-// String wrap runs at runtime, mirroring Html.getIsContentEditable.
fn isEditingHost(node: *Node) bool {
const element = node.is(Element) orelse return false;
- const value = element.getAttributeSafe(.wrap("contenteditable")) orelse return false;
- return std.ascii.eqlIgnoreCase(value, "false") == false;
+ return element.isEditingHost();
}
fn outermostEditingHost(target: *Element) ?*Element {
@@ -570,17 +545,6 @@ fn outermostEditingHost(target: *Element) ?*Element {
return host.is(Element);
}
-/// Unlike sequential focus, a negative tabindex is still mouse-focusable, and
-/// an unparsable one counts as absent (HTML §6.6.3), not as "not focusable".
-fn isMouseFocusable(el: *Element) bool {
- if (el.isDisabled()) return false;
-
- if (el.getAttributeInterned("tabindex")) |attr| {
- if (Element.Html.parseInteger(attr) != null) return true;
- }
- return isNativelyFocusable(el);
-}
-
/// Mousedown default action. A mousedown outside any focusable element moves
/// focus to the body.
pub fn focusForMouseDown(frame: *Frame, target: *Element) !void {
@@ -592,7 +556,9 @@ pub fn focusForMouseDown(frame: *Frame, target: *Element) !void {
var node: ?*Node = target.asNode();
while (node) |n| : (node = n._parent) {
const el = n.is(Element) orelse continue;
- if (isMouseFocusable(el)) {
+ // Unlike sequential focus navigation, a negative tabindex is still
+ // mouse-focusable, so any focusable area qualifies.
+ if (el.focusTabIndex() != null) {
try el.focus(frame);
return;
}
@@ -690,7 +656,7 @@ pub fn handleClick(frame: *Frame, target: *Node, event_target: *Node) !void {
const element = target.is(Element) orelse return;
if (element.is(Element.Svg.Graphics.A) != null) {
- const href = svgAnchorHref(element) orelse return;
+ const href = element.svgAnchorHref() orelse return;
const target_name = element.getAttributeInterned("target") orelse "";
return followLink(frame, target, element, href, target_name);
}
diff --git a/src/browser/tests/document/focus.html b/src/browser/tests/document/focus.html
index 6e09469b3..474b5de82 100644
--- a/src/browser/tests/document/focus.html
+++ b/src/browser/tests/document/focus.html
@@ -354,3 +354,112 @@
wrapper.remove();
}
+
+
+
+
+
+
+
+
diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig
index f3a644b45..f21ac7ffd 100644
--- a/src/browser/webapi/Element.zig
+++ b/src/browser/webapi/Element.zig
@@ -1181,6 +1181,35 @@ pub fn remove(self: *Element, frame: *Frame) void {
frame.removeNode(parent, node, .{ .reconnect_to = null });
}
+// SVG 2 links via `href`; xlink:href is the deprecated SVG 1.1 spelling.
+pub fn svgAnchorHref(self: *Element) ?[]const u8 {
+ return self.getAttributeInterned("href") orelse self.getAttributeSafe(comptime .wrap("xlink:href"));
+}
+
+pub fn isSvgLink(self: *Element) bool {
+ return self.is(Svg.Graphics.A) != null and self.svgAnchorHref() != null;
+}
+
+// An editing host takes focus like a form control does.
+pub fn isEditingHost(self: *Element) bool {
+ const value = self.getAttributeSafe(.wrap("contenteditable")) orelse return false;
+ return std.ascii.eqlIgnoreCase(value, "false") == false;
+}
+
+/// Focusable without a tabindex attribute.
+fn isNativelyFocusable(self: *Element) bool {
+ if (self.is(Html) == null) {
+ return self.isSvgLink();
+ }
+
+ return switch (self.getTag()) {
+ .button, .select, .textarea, .iframe => true,
+ .input => self.as(Html.Input)._input_type != .hidden,
+ .anchor, .area => self.getAttributeInterned("href") != null,
+ else => false,
+ };
+}
+
// The tabindex of a focusable area, or null when the element can't take focus
// at all. A negative value is still focusable, just skipped by sequential
// focus navigation.
@@ -1189,20 +1218,19 @@ pub fn focusTabIndex(self: *Element) ?i32 {
if (self.isDisabled()) {
return null;
}
- if (self.is(Html) == null) {
- return null;
- }
if (self.getAttributeInterned("tabindex")) |attr| {
- return Html.parseInteger(attr) orelse 0;
+ if (Html.parseInteger(attr)) |tab_index| {
+ return tab_index;
+ } else {
+ // can't be parsed is treated the same as no tabindex
+ }
}
- return switch (self.getTag()) {
- .button, .select, .textarea, .iframe => 0,
- .input => if (self.as(Html.Input)._input_type != .hidden) 0 else null,
- .anchor, .area => if (self.getAttributeInterned("href") != null) 0 else null,
- else => null,
- };
+ if (self.isNativelyFocusable() or self.isEditingHost()) {
+ return 0;
+ }
+ return null;
}
// A focusable area that can take focus right now: connected and being rendered.
@@ -1229,9 +1257,7 @@ pub fn focus(self: *Element, frame: *Frame) !void {
return;
}
- // Per HTML spec §6.4.4, an element must be "being rendered" (not
- // display:none on self or any ancestor) to be focusable.
- if (!self.isVisible(owner)) {
+ if (self.isFocusable(owner) == false) {
return;
}