Skip to content

Commit 6cf0163

Browse files
committed
Document.activeElement, focus and blur
1 parent 7a5cade commit 6cf0163

File tree

3 files changed

+131
-0
lines changed

3 files changed

+131
-0
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<!DOCTYPE html>
2+
<script src="../testing.js"></script>
3+
<body>
4+
<input id="input1" type="text">
5+
<input id="input2" type="text">
6+
<button id="btn1">Button</button>
7+
</body>
8+
9+
<script id="activeElement_default">
10+
{
11+
const body = document.querySelector('body');
12+
body.focus();
13+
testing.expectEqual('BODY', document.activeElement.tagName);
14+
}
15+
</script>
16+
17+
<script id="focus_method">
18+
{
19+
const input1 = $('#input1');
20+
input1.focus();
21+
testing.expectEqual(input1, document.activeElement);
22+
}
23+
</script>
24+
25+
<script id="focus_events">
26+
{
27+
const input1 = $('#input1');
28+
const input2 = $('#input2');
29+
30+
// Explicitly blur any focused element
31+
if (document.activeElement) {
32+
document.activeElement.blur();
33+
}
34+
35+
let focusCount = 0;
36+
let blurCount = 0;
37+
38+
input1.addEventListener('focus', () => focusCount++);
39+
input1.addEventListener('blur', () => blurCount++);
40+
input2.addEventListener('focus', () => focusCount++);
41+
42+
input1.focus();
43+
testing.expectEqual(1, focusCount);
44+
testing.expectEqual(0, blurCount);
45+
46+
input2.focus();
47+
testing.expectEqual(2, focusCount);
48+
testing.expectEqual(1, blurCount);
49+
}
50+
</script>
51+
52+
<script id="blur_method">
53+
{
54+
const btn = $('#btn1');
55+
btn.focus();
56+
testing.expectEqual(btn, document.activeElement);
57+
58+
btn.blur();
59+
testing.expectEqual('BODY', document.activeElement.tagName);
60+
}
61+
</script>
62+
63+
<script id="focus_already_focused">
64+
{
65+
const input1 = $('#input1');
66+
67+
// Explicitly blur any focused element
68+
if (document.activeElement) {
69+
document.activeElement.blur();
70+
}
71+
72+
let focusCount = 0;
73+
input1.addEventListener('focus', () => focusCount++);
74+
75+
input1.focus();
76+
testing.expectEqual(1, focusCount);
77+
78+
input1.focus();
79+
testing.expectEqual(1, focusCount);
80+
}
81+
</script>

src/browser/webapi/Document.zig

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ _location: ?*Location = null,
2424
_ready_state: ReadyState = .loading,
2525
_current_script: ?*Element.Html.Script = null,
2626
_elements_by_id: std.StringHashMapUnmanaged(*Element) = .empty,
27+
_active_element: ?*Element = null,
2728

2829
pub const Type = union(enum) {
2930
generic,
@@ -155,6 +156,22 @@ pub fn getReadyState(self: *const Document) []const u8 {
155156
return @tagName(self._ready_state);
156157
}
157158

159+
pub fn getActiveElement(self: *Document) ?*Element {
160+
if (self._active_element) |el| {
161+
return el;
162+
}
163+
164+
// Default to body if it exists
165+
if (self.is(HTMLDocument)) |html_doc| {
166+
if (html_doc.getBody()) |body| {
167+
return body.asElement();
168+
}
169+
}
170+
171+
// Fallback to document element
172+
return self.getDocumentElement();
173+
}
174+
158175
const ReadyState = enum {
159176
loading,
160177
interactive,
@@ -182,6 +199,7 @@ pub const JsApi = struct {
182199
pub const documentElement = bridge.accessor(Document.getDocumentElement, null, .{});
183200
pub const readyState = bridge.accessor(Document.getReadyState, null, .{});
184201
pub const implementation = bridge.accessor(Document.getImplementation, null, .{});
202+
pub const activeElement = bridge.accessor(Document.getActiveElement, null, .{});
185203

186204
pub const createElement = bridge.function(Document.createElement, .{});
187205
pub const createElementNS = bridge.function(Document.createElementNS, .{});

src/browser/webapi/Element.zig

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ pub fn asNode(self: *Element) *Node {
8282
return self._proto;
8383
}
8484

85+
pub fn asEventTarget(self: *Element) *@import("EventTarget.zig") {
86+
return self._proto.asEventTarget();
87+
}
88+
8589
pub fn asConstNode(self: *const Element) *const Node {
8690
return self._proto;
8791
}
@@ -390,6 +394,32 @@ pub fn remove(self: *Element, page: *Page) void {
390394
page.removeNode(parent, node, .{ .will_be_reconnected = false });
391395
}
392396

397+
pub fn focus(self: *Element, page: *Page) !void {
398+
const Event = @import("Event.zig");
399+
400+
if (page.document._active_element) |old| {
401+
if (old == self) return;
402+
403+
const blur_event = try Event.init("blur", null, page);
404+
try page._event_manager.dispatch(old.asEventTarget(), blur_event);
405+
}
406+
407+
page.document._active_element = self;
408+
409+
const focus_event = try Event.init("focus", null, page);
410+
try page._event_manager.dispatch(self.asEventTarget(), focus_event);
411+
}
412+
413+
pub fn blur(self: *Element, page: *Page) !void {
414+
if (page.document._active_element != self) return;
415+
416+
page.document._active_element = null;
417+
418+
const Event = @import("Event.zig");
419+
const blur_event = try Event.init("blur", null, page);
420+
try page._event_manager.dispatch(self.asEventTarget(), blur_event);
421+
}
422+
393423
pub fn getChildren(self: *Element, page: *Page) !collections.NodeLive(.child_elements) {
394424
return collections.NodeLive(.child_elements).init(null, self.asNode(), {}, page);
395425
}
@@ -831,6 +861,8 @@ pub const JsApi = struct {
831861
pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{});
832862
pub const getElementsByClassName = bridge.function(Element.getElementsByClassName, .{});
833863
pub const children = bridge.accessor(Element.getChildren, null, .{});
864+
pub const focus = bridge.function(Element.focus, .{});
865+
pub const blur = bridge.function(Element.blur, .{});
834866
};
835867

836868
pub const Build = struct {

0 commit comments

Comments
 (0)