Merge pull request #742 from lightpanda-io/focus_and_active_element

Focus and active element
This commit is contained in:
Karl Seguin
2025-06-04 08:33:20 +08:00
committed by GitHub
4 changed files with 56 additions and 4 deletions

View File

@@ -42,6 +42,8 @@ pub const Document = struct {
pub const prototype = *Node;
pub const subtype = .node;
active_element: ?*parser.Element = null,
pub fn constructor(page: *const Page) !*parser.DocumentHTML {
const doc = try parser.documentCreateDocument(
try parser.documentHTMLGetTitle(page.window.document),
@@ -242,6 +244,19 @@ pub const Document = struct {
pub fn _createTreeWalker(_: *parser.Document, root: *parser.Node, what_to_show: ?u32, filter: ?TreeWalker.TreeWalkerOpts) !TreeWalker {
return try TreeWalker.init(root, what_to_show, filter);
}
pub fn get_activeElement(doc: *parser.Document, page: *Page) !?ElementUnion {
const self = try page.getOrCreateNodeWrapper(Document, @ptrCast(doc));
if (self.active_element) |ae| {
return try Element.toInterface(ae);
}
if (try parser.documentHTMLBody(page.window.document)) |body| {
return try Element.toInterface(@ptrCast(body));
}
return get_documentElement(doc);
}
};
const testing = @import("../../testing.zig");
@@ -412,6 +427,12 @@ test "Browser.DOM.Document" {
},
}, .{});
try runner.testCases(&.{
.{ "document.activeElement === document.body", "true" },
.{ "document.getElementById('link').focus()", "undefined" },
.{ "document.activeElement === document.getElementById('link')", "true" },
}, .{});
// this test breaks the doc structure, keep it at the end of the test
// suite.
try runner.testCases(&.{

View File

@@ -368,8 +368,7 @@ pub const Element = struct {
// Returns a 0 DOMRect object if the element is eventually detached from the main window
pub fn _getBoundingClientRect(self: *parser.Element, page: *Page) !DOMRect {
// Since we are lazy rendering we need to do this check. We could store the renderer in a viewport such that it could cache these, but it would require tracking changes.
const root = try parser.nodeGetRootNode(parser.elementToNode(self));
if (root != parser.documentToNode(parser.documentHTMLToDocument(page.window.document))) {
if (!try page.isNodeAttached(parser.elementToNode(self))) {
return DOMRect{ .x = 0, .y = 0, .width = 0, .height = 0 };
}
return page.renderer.getRect(self);
@@ -379,8 +378,7 @@ pub const Element = struct {
// We do not render so it only always return the element's bounding rect.
// Returns an empty array if the element is eventually detached from the main window
pub fn _getClientRects(self: *parser.Element, page: *Page) ![]DOMRect {
const root = try parser.nodeGetRootNode(parser.elementToNode(self));
if (root != parser.documentToNode(parser.documentHTMLToDocument(page.window.document))) {
if (!try page.isNodeAttached(parser.elementToNode(self))) {
return &.{};
}
const heap_ptr = try page.call_arena.create(DOMRect);

View File

@@ -148,6 +148,27 @@ pub const HTMLElement = struct {
});
_ = try parser.elementDispatchEvent(@ptrCast(e), @ptrCast(event));
}
const FocusOpts = struct {
preventScroll: bool,
focusVisible: bool,
};
pub fn _focus(e: *parser.ElementHTML, _: ?FocusOpts, page: *Page) !void {
if (!try page.isNodeAttached(@ptrCast(e))) {
return;
}
const root_node = try parser.nodeGetRootNode(@ptrCast(e));
const Document = @import("../dom/document.zig").Document;
const document = try page.getOrCreateNodeWrapper(Document, @ptrCast(root_node));
// TODO: some elements can't be focused, like if they're disabled
// but there doesn't seem to be a generic way to check this. For example
// we could look for the "disabled" attribute, but that's only meaningful
// on certain types, and libdom's vtable doesn't seem to expose this.
document.active_element = @ptrCast(e);
}
};
// Deprecated HTMLElements in Chrome (2023/03/15)
@@ -1181,4 +1202,11 @@ test "Browser.HTML.Element" {
.{ "a.href = 'about'", null },
.{ "a.href", "https://lightpanda.io/opensource-browser/about" },
}, .{});
// detached node cannot be focused
try runner.testCases(&.{
.{ "const focused = document.activeElement", null },
.{ "document.createElement('a').focus()", null },
.{ "document.activeElement === focused", "true" },
}, .{});
}

View File

@@ -643,6 +643,11 @@ pub const Page = struct {
try self.navigateFromWebAPI(action, opts);
}
pub fn isNodeAttached(self: *const Page, node: *parser.Node) !bool {
const root = parser.documentToNode(parser.documentHTMLToDocument(self.window.document));
return root == try parser.nodeGetRootNode(node);
}
fn elementSubmitForm(self: *Page, element: *parser.Element) !void {
const form = (try self.formForElement(element)) orelse return;
return self.submitForm(@ptrCast(form), @ptrCast(element));