diff --git a/src/browser/events/event.zig b/src/browser/events/event.zig
index cc82ab57..9e3c36df 100644
--- a/src/browser/events/event.zig
+++ b/src/browser/events/event.zig
@@ -33,11 +33,20 @@ const AbortSignal = @import("../html/AbortController.zig").AbortSignal;
const CustomEvent = @import("custom_event.zig").CustomEvent;
const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
const MouseEvent = @import("mouse_event.zig").MouseEvent;
+const KeyboardEvent = @import("keyboard_event.zig").KeyboardEvent;
const ErrorEvent = @import("../html/error_event.zig").ErrorEvent;
const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
// Event interfaces
-pub const Interfaces = .{ Event, CustomEvent, ProgressEvent, MouseEvent, ErrorEvent, MessageEvent };
+pub const Interfaces = .{
+ Event,
+ CustomEvent,
+ ProgressEvent,
+ MouseEvent,
+ KeyboardEvent,
+ ErrorEvent,
+ MessageEvent,
+};
pub const Union = generate.Union(Interfaces);
diff --git a/src/browser/events/keyboard_event.zig b/src/browser/events/keyboard_event.zig
new file mode 100644
index 00000000..2a5a27c9
--- /dev/null
+++ b/src/browser/events/keyboard_event.zig
@@ -0,0 +1,80 @@
+const std = @import("std");
+const log = @import("../../log.zig");
+
+const netsurf = @import("../netsurf.zig");
+const Event = @import("event.zig").Event;
+const JsObject = @import("../env.zig").JsObject;
+
+const c = @cImport({
+ @cInclude("dom/dom.h");
+ @cInclude("core/pi.h");
+ @cInclude("dom/bindings/hubbub/parser.h");
+ @cInclude("events/event_target.h");
+ @cInclude("events/event.h");
+ @cInclude("events/mouse_event.h");
+ @cInclude("events/keyboard_event.h");
+ @cInclude("utils/validate.h");
+ @cInclude("html/html_element.h");
+ @cInclude("html/html_document.h");
+});
+
+// TODO: We currently don't have a UIEvent interface so we skip it in the prototype chain.
+// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent
+const UIEvent = Event;
+
+// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
+pub const KeyboardEvent = struct {
+ pub const Self = netsurf.KeyboardEvent;
+ pub const prototype = *UIEvent;
+
+ pub const KeyLocationCode = enum(u16) {
+ standard = 0x00,
+ left = 0x01,
+ right = 0x02,
+ numpad = 0x03,
+ mobile = 0x04, // Non-standard, deprecated.
+ joystick = 0x05, // Non-standard, deprecated.
+ };
+
+ pub const ConstructorOptions = struct {
+ key: []const u8 = "",
+ code: []const u8 = "",
+ location: KeyLocationCode = .standard,
+ char_code: u32 = 0,
+ key_code: u32 = 0,
+ which: u32 = 0,
+ repeat: bool = false,
+ ctrl_key: bool = false,
+ shift_key: bool = false,
+ alt_key: bool = false,
+ meta_key: bool = false,
+ is_composing: bool = false,
+ };
+
+ pub fn constructor(event_type: []const u8, maybe_options: ?ConstructorOptions) !*netsurf.KeyboardEvent {
+ const options = maybe_options orelse ConstructorOptions{};
+
+ const event = try netsurf.keyboardEventCreate();
+ try netsurf.keyboardEventInit(
+ event,
+ event_type,
+ .{
+ .bubbles = false,
+ .cancelable = false,
+ .key = options.key,
+ .code = options.code,
+ .alt = options.alt_key,
+ .ctrl = options.ctrl_key,
+ .meta = options.meta_key,
+ .shift = options.shift_key,
+ },
+ );
+
+ return event;
+ }
+};
+
+const testing = @import("../../testing.zig");
+test "Browser: Events.Keyboard" {
+ try testing.htmlRunner("events/keyboard.html");
+}
diff --git a/src/browser/netsurf.zig b/src/browser/netsurf.zig
index a663037b..d8ef79e3 100644
--- a/src/browser/netsurf.zig
+++ b/src/browser/netsurf.zig
@@ -388,7 +388,7 @@ pub const DOMError = error{
const DOMException = c.dom_exception;
-fn DOMErr(except: DOMException) DOMError!void {
+pub fn DOMErr(except: DOMException) DOMError!void {
return switch (except) {
c.DOM_NO_ERR => return,
c.DOM_INDEX_SIZE_ERR => DOMError.IndexSize,
diff --git a/src/tests/events/keyboard.html b/src/tests/events/keyboard.html
new file mode 100644
index 00000000..b3941fdd
--- /dev/null
+++ b/src/tests/events/keyboard.html
@@ -0,0 +1,22 @@
+
+
+
+