mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-09-21 03:55:22 -04:00
dom.focus only on focusable elements
This commit is contained in:
3 files changed
+81
-29
No files matched your search
@@ -853,37 +853,10 @@ fn moveFocus(frame: *Frame, forward: bool) !void {
|
||||
|
||||
var tw = TreeWalker.Full.Elements.init(document.asNode(), .{});
|
||||
while (tw.next()) |candidate| {
|
||||
if (candidate.isDisabled()) {
|
||||
const candidate_tab_index = candidate.focusTabIndex() orelse continue;
|
||||
if (candidate_tab_index < 0) {
|
||||
continue;
|
||||
}
|
||||
if (candidate.is(Element.Html) == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const candidate_tab_index = blk: {
|
||||
if (candidate.getAttributeInterned("tabindex")) |attr| {
|
||||
if (Element.Html.parseInteger(attr)) |tab_index| {
|
||||
if (tab_index < 0) {
|
||||
continue;
|
||||
}
|
||||
break :blk tab_index;
|
||||
}
|
||||
break :blk 0;
|
||||
}
|
||||
|
||||
// no tab index, maybe this item isn't focusable..
|
||||
const focusable = switch (candidate.getTag()) {
|
||||
.button, .select, .textarea, .iframe => true,
|
||||
.input => candidate.as(Element.Html.Input)._input_type != .hidden,
|
||||
.anchor, .area => candidate.getAttributeInterned("href") != null,
|
||||
else => false,
|
||||
};
|
||||
if (focusable == false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
break :blk 0;
|
||||
};
|
||||
|
||||
if (edge == null or focusOrderBefore(candidate, candidate_tab_index, edge.?, edge_tab_index) == forward) {
|
||||
edge = candidate;
|
||||
|
||||
@@ -1153,6 +1153,41 @@ pub fn remove(self: *Element, frame: *Frame) void {
|
||||
frame.removeNode(parent, node, .{ .reconnect_to = null });
|
||||
}
|
||||
|
||||
// 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.
|
||||
// https://html.spec.whatwg.org/multipage/interaction.html#focusable-area
|
||||
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;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
// A focusable area that can take focus right now: connected and being rendered.
|
||||
pub fn isFocusable(self: *Element, frame: *Frame, comptime access: StyleManager.InlineAccess) bool {
|
||||
if (self.focusTabIndex() == null) {
|
||||
return false;
|
||||
}
|
||||
if (self.asNode().isConnected() == false) {
|
||||
return false;
|
||||
}
|
||||
return self.isVisible(frame, access);
|
||||
}
|
||||
|
||||
pub fn focus(self: *Element, frame: *Frame) !void {
|
||||
if (self.asNode().isConnected() == false) {
|
||||
// a disconnected node cannot take focus
|
||||
|
||||
@@ -696,6 +696,9 @@ fn focus(cmd: *CDP.Command) !void {
|
||||
|
||||
const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);
|
||||
const element = node.dom.is(DOMNode.Element) orelse return error.NodeIsNotAnElement;
|
||||
if (element.isFocusable(frame, .scan) == false) {
|
||||
return cmd.sendError(-32000, "Element is not focusable", .{});
|
||||
}
|
||||
try element.focus(frame);
|
||||
|
||||
return cmd.sendResult(null, .{});
|
||||
@@ -908,6 +911,47 @@ test "cdp.dom: focus makes the node activeElement and routes key events to it" {
|
||||
try testing.expect(result.isTrue());
|
||||
}
|
||||
|
||||
test "cdp.dom: focus errors on an element that can't take focus" {
|
||||
var ctx = try testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{ .id = "BID-A", .url = "mcp_actions.html" });
|
||||
const frame = bc.mainFrame().?;
|
||||
|
||||
var ls: lp.js.Local.Scope = undefined;
|
||||
frame.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
var try_catch: lp.js.TryCatch = undefined;
|
||||
try_catch.init(&ls.local);
|
||||
defer try_catch.deinit();
|
||||
|
||||
// #btn not rendered, #inp disabled, #hoverTarget a plain <div>.
|
||||
_ = try ls.local.compileAndRun(
|
||||
\\document.getElementById('btn').style.display = 'none';
|
||||
\\document.getElementById('inp').disabled = true;
|
||||
, null);
|
||||
|
||||
try ctx.processMessage(.{ .id = 1, .method = "DOM.performSearch", .params = .{ .query = "#btn, #inp, #hoverTarget" } });
|
||||
try ctx.expectSentResult(.{ .searchId = "0", .resultCount = 3 }, .{ .id = 1 });
|
||||
try ctx.processMessage(.{
|
||||
.id = 2,
|
||||
.method = "DOM.getSearchResults",
|
||||
.params = .{ .searchId = "0", .fromIndex = 0, .toIndex = 3 },
|
||||
});
|
||||
try ctx.expectSentResult(.{ .nodeIds = &.{ 1, 2, 3 } }, .{ .id = 2 });
|
||||
|
||||
try ctx.processMessage(.{ .id = 3, .method = "DOM.focus", .params = .{ .nodeId = 1 } });
|
||||
try ctx.expectSentError(-32000, "Element is not focusable", .{ .id = 3 });
|
||||
try ctx.processMessage(.{ .id = 4, .method = "DOM.focus", .params = .{ .nodeId = 2 } });
|
||||
try ctx.expectSentError(-32000, "Element is not focusable", .{ .id = 4 });
|
||||
try ctx.processMessage(.{ .id = 5, .method = "DOM.focus", .params = .{ .nodeId = 3 } });
|
||||
try ctx.expectSentError(-32000, "Element is not focusable", .{ .id = 5 });
|
||||
|
||||
const result = try ls.local.compileAndRun("document.activeElement === document.body", null);
|
||||
try testing.expect(result.isTrue());
|
||||
}
|
||||
|
||||
test "cdp.dom: setFileInputFiles on file input" {
|
||||
var ctx = try testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
Reference in new issue
Block a user