Skip to content

Commit 18e6f9b

Browse files
committed
Detached node can't have focus.
Refactor isNodeAttached because of the "law of three."
1 parent 211ce20 commit 18e6f9b

File tree

4 files changed

+40
-26
lines changed

4 files changed

+40
-26
lines changed

src/browser/dom/document.zig

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ pub const Document = struct {
4242
pub const prototype = *Node;
4343
pub const subtype = .node;
4444

45+
active_element: ?*parser.Element = null,
46+
4547
pub fn constructor(page: *const Page) !*parser.DocumentHTML {
4648
const doc = try parser.documentCreateDocument(
4749
try parser.documentHTMLGetTitle(page.window.document),
@@ -243,9 +245,17 @@ pub const Document = struct {
243245
return try TreeWalker.init(root, what_to_show, filter);
244246
}
245247

246-
pub fn get_activeElement(_: *parser.Document, page: *const Page) !?ElementUnion {
247-
const el = (try page.activeElement()) orelse return null;
248-
return try Element.toInterface(el);
248+
pub fn get_activeElement(doc: *parser.Document, page: *Page) !?ElementUnion {
249+
const self = try page.getOrCreateNodeWrapper(Document, @ptrCast(doc));
250+
if (self.active_element) |ae| {
251+
return try Element.toInterface(ae);
252+
}
253+
254+
if (try parser.documentHTMLBody(page.window.document)) |body| {
255+
return try Element.toInterface(@ptrCast(body));
256+
}
257+
258+
return get_documentElement(doc);
249259
}
250260
};
251261

src/browser/dom/element.zig

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -368,8 +368,7 @@ pub const Element = struct {
368368
// Returns a 0 DOMRect object if the element is eventually detached from the main window
369369
pub fn _getBoundingClientRect(self: *parser.Element, page: *Page) !DOMRect {
370370
// 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.
371-
const root = try parser.nodeGetRootNode(parser.elementToNode(self));
372-
if (root != parser.documentToNode(parser.documentHTMLToDocument(page.window.document))) {
371+
if (!try page.isNodeAttached(parser.elementToNode(self))) {
373372
return DOMRect{ .x = 0, .y = 0, .width = 0, .height = 0 };
374373
}
375374
return page.renderer.getRect(self);
@@ -379,8 +378,7 @@ pub const Element = struct {
379378
// We do not render so it only always return the element's bounding rect.
380379
// Returns an empty array if the element is eventually detached from the main window
381380
pub fn _getClientRects(self: *parser.Element, page: *Page) ![]DOMRect {
382-
const root = try parser.nodeGetRootNode(parser.elementToNode(self));
383-
if (root != parser.documentToNode(parser.documentHTMLToDocument(page.window.document))) {
381+
if (!try page.isNodeAttached(parser.elementToNode(self))) {
384382
return &.{};
385383
}
386384
const heap_ptr = try page.call_arena.create(DOMRect);

src/browser/html/elements.zig

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,12 +149,25 @@ pub const HTMLElement = struct {
149149
_ = try parser.elementDispatchEvent(@ptrCast(e), @ptrCast(event));
150150
}
151151

152-
pub fn _focus(e: *parser.ElementHTML, page: *Page) void {
152+
const FocusOpts = struct {
153+
preventScroll: bool,
154+
focusVisible: bool,
155+
};
156+
pub fn _focus(e: *parser.ElementHTML, _: ?FocusOpts, page: *Page) !void {
157+
if (!try page.isNodeAttached(@ptrCast(e))) {
158+
return;
159+
}
160+
161+
const root_node = try parser.nodeGetRootNode(@ptrCast(e));
162+
163+
const Document = @import("../dom/document.zig").Document;
164+
const document = try page.getOrCreateNodeWrapper(Document, @ptrCast(root_node));
165+
153166
// TODO: some elements can't be focused, like if they're disabled
154167
// but there doesn't seem to be a generic way to check this. For example
155168
// we could look for the "disabled" attribute, but that's only meaningful
156169
// on certain types, and libdom's vtable doesn't seem to expose this.
157-
page.active_element = @ptrCast(e);
170+
document.active_element = @ptrCast(e);
158171
}
159172
};
160173

@@ -1189,4 +1202,11 @@ test "Browser.HTML.Element" {
11891202
.{ "a.href = 'about'", null },
11901203
.{ "a.href", "https://lightpanda.io/opensource-browser/about" },
11911204
}, .{});
1205+
1206+
// detached node cannot be focused
1207+
try runner.testCases(&.{
1208+
.{ "const focused = document.activeElement", null },
1209+
.{ "document.createElement('a').focus()", null },
1210+
.{ "document.activeElement === focused", "true" },
1211+
}, .{});
11921212
}

src/browser/page.zig

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,6 @@ pub const Page = struct {
9595
// indicates intention to navigate to another page on the next loop execution.
9696
delayed_navigation: bool = false,
9797

98-
active_element: ?*parser.Element = null,
99-
10098
pub fn init(self: *Page, arena: Allocator, session: *Session) !void {
10199
const browser = session.browser;
102100
self.* = .{
@@ -645,21 +643,9 @@ pub const Page = struct {
645643
try self.navigateFromWebAPI(action, opts);
646644
}
647645

648-
pub fn activeElement(self: *const Page) !?*parser.Element {
649-
if (self.active_element) |ae| {
650-
return ae;
651-
}
652-
653-
if (try parser.documentHTMLBody(self.window.document)) |body| {
654-
return @ptrCast(body);
655-
}
656-
657-
const doc = self.window.document;
658-
if (try parser.documentGetDocumentElement(@ptrCast(doc))) |de| {
659-
return @ptrCast(de);
660-
}
661-
662-
return null;
646+
pub fn isNodeAttached(self: *const Page, node: *parser.Node) !bool {
647+
const root = parser.documentToNode(parser.documentHTMLToDocument(self.window.document));
648+
return root == try parser.nodeGetRootNode(node);
663649
}
664650

665651
fn elementSubmitForm(self: *Page, element: *parser.Element) !void {

0 commit comments

Comments
 (0)