events: gate KeyboardEvent.keyCode/charCode on isTrusted, add Enter charCode

Address review feedback on the legacy KeyboardEvent.keyCode and
KeyboardEvent.charCode getters:

* `getKeyCode` and `getCharCode` now early-return 0 when the event is
  not trusted, matching Chrome's behavior. Synthetic events created via
  `new KeyboardEvent(...)` from script have `isTrusted === false` and
  therefore expose 0 for both legacy attributes; only events dispatched
  by the user agent itself surface the legacy mapping.
* `getCharCode` now uses `_type_string.eql(comptime .wrap("keypress"))`
  to match the idiom used elsewhere in the project.
* The charCode mapping is moved into a pure `Key.charCode()` helper that
  mirrors `Key.keyCode()`, including a `.Enter => 13` arm so a trusted
  `keypress` for Enter exposes `\r` (U+000D) per spec.

The JS test fixture is consolidated into a single block asserting the
Chrome-correct behavior for synthetic events. The full per-key mapping
table is now exercised via two pure-function Zig unit tests on
`Key.keyCode()` and `Key.charCode()`.
This commit is contained in:
Navid EMAD
2026-04-28 07:12:29 +02:00
parent 88b38d240d
commit 73a007f88e
2 changed files with 117 additions and 78 deletions

View File

@@ -132,85 +132,33 @@
}
</script>
<script id=keyCodeLetter>
<script id=keyCodeAndCharCodeAreZeroForSyntheticEvents>
{
// Letters: keyCode is the uppercase ASCII regardless of case.
testing.expectEqual(65, new KeyboardEvent('keydown', {key: 'a'}).keyCode);
testing.expectEqual(65, new KeyboardEvent('keydown', {key: 'A'}).keyCode);
testing.expectEqual(84, new KeyboardEvent('keydown', {key: 't'}).keyCode);
testing.expectEqual(84, new KeyboardEvent('keydown', {key: 'T'}).keyCode);
testing.expectEqual(90, new KeyboardEvent('keydown', {key: 'z'}).keyCode);
testing.expectEqual(90, new KeyboardEvent('keydown', {key: 'Z'}).keyCode);
}
</script>
// Per Chrome behavior, both legacy `keyCode` and `charCode` return 0 for
// events constructed via `new KeyboardEvent(...)` because such events
// have `isTrusted === false`. The legacy mapping (a -> 65, Enter -> 13,
// etc.) is only exposed for events dispatched by the user agent itself.
// The full mapping table is exercised via Zig unit tests in
// `webapi/event/KeyboardEvent.zig`.
<script id=keyCodeDigit>
{
testing.expectEqual(48, new KeyboardEvent('keydown', {key: '0'}).keyCode);
testing.expectEqual(53, new KeyboardEvent('keydown', {key: '5'}).keyCode);
testing.expectEqual(57, new KeyboardEvent('keydown', {key: '9'}).keyCode);
}
</script>
<script id=keyCodeSpace>
{
testing.expectEqual(32, new KeyboardEvent('keydown', {key: ' '}).keyCode);
}
</script>
<script id=keyCodeNamed>
{
// Modifier keys
testing.expectEqual(16, new KeyboardEvent('keydown', {key: 'Shift'}).keyCode);
testing.expectEqual(17, new KeyboardEvent('keydown', {key: 'Control'}).keyCode);
testing.expectEqual(18, new KeyboardEvent('keydown', {key: 'Alt'}).keyCode);
testing.expectEqual(91, new KeyboardEvent('keydown', {key: 'Meta'}).keyCode);
testing.expectEqual(20, new KeyboardEvent('keydown', {key: 'CapsLock'}).keyCode);
// Whitespace
testing.expectEqual(13, new KeyboardEvent('keydown', {key: 'Enter'}).keyCode);
testing.expectEqual(9, new KeyboardEvent('keydown', {key: 'Tab'}).keyCode);
// Navigation
testing.expectEqual(37, new KeyboardEvent('keydown', {key: 'ArrowLeft'}).keyCode);
testing.expectEqual(38, new KeyboardEvent('keydown', {key: 'ArrowUp'}).keyCode);
testing.expectEqual(39, new KeyboardEvent('keydown', {key: 'ArrowRight'}).keyCode);
testing.expectEqual(40, new KeyboardEvent('keydown', {key: 'ArrowDown'}).keyCode);
testing.expectEqual(33, new KeyboardEvent('keydown', {key: 'PageUp'}).keyCode);
testing.expectEqual(34, new KeyboardEvent('keydown', {key: 'PageDown'}).keyCode);
testing.expectEqual(35, new KeyboardEvent('keydown', {key: 'End'}).keyCode);
testing.expectEqual(36, new KeyboardEvent('keydown', {key: 'Home'}).keyCode);
// Editing
testing.expectEqual(8, new KeyboardEvent('keydown', {key: 'Backspace'}).keyCode);
testing.expectEqual(46, new KeyboardEvent('keydown', {key: 'Delete'}).keyCode);
testing.expectEqual(45, new KeyboardEvent('keydown', {key: 'Insert'}).keyCode);
// UI
testing.expectEqual(27, new KeyboardEvent('keydown', {key: 'Escape'}).keyCode);
testing.expectEqual(19, new KeyboardEvent('keydown', {key: 'Pause'}).keyCode);
testing.expectEqual(93, new KeyboardEvent('keydown', {key: 'ContextMenu'}).keyCode);
// Function keys
testing.expectEqual(112, new KeyboardEvent('keydown', {key: 'F1'}).keyCode);
testing.expectEqual(123, new KeyboardEvent('keydown', {key: 'F12'}).keyCode);
}
</script>
<script id=keyCodeUnknown>
{
// Keys without a defined fixed virtual key code return 0.
// Letters
testing.expectEqual(0, new KeyboardEvent('keydown', {key: 'a'}).keyCode);
testing.expectEqual(0, new KeyboardEvent('keydown', {key: 'Z'}).keyCode);
// Digits
testing.expectEqual(0, new KeyboardEvent('keydown', {key: '5'}).keyCode);
// Space
testing.expectEqual(0, new KeyboardEvent('keydown', {key: ' '}).keyCode);
// Named keys
testing.expectEqual(0, new KeyboardEvent('keydown', {key: 'Enter'}).keyCode);
testing.expectEqual(0, new KeyboardEvent('keydown', {key: 'F1'}).keyCode);
// Unknown key
testing.expectEqual(0, new KeyboardEvent('keydown', {key: 'Dead'}).keyCode);
testing.expectEqual(0, new KeyboardEvent('keydown', {key: 'Unidentified'}).keyCode);
testing.expectEqual(0, new KeyboardEvent('keydown', {key: ''}).keyCode);
}
</script>
<script id=charCode>
{
// charCode is meaningful only for keypress events; 0 elsewhere.
testing.expectEqual(97, new KeyboardEvent('keypress', {key: 'a'}).charCode);
testing.expectEqual(65, new KeyboardEvent('keypress', {key: 'A'}).charCode);
testing.expectEqual(48, new KeyboardEvent('keypress', {key: '0'}).charCode);
testing.expectEqual(32, new KeyboardEvent('keypress', {key: ' '}).charCode);
// charCode follows the same rule across event types.
testing.expectEqual(0, new KeyboardEvent('keypress', {key: 'a'}).charCode);
testing.expectEqual(0, new KeyboardEvent('keypress', {key: 'Enter'}).charCode);
testing.expectEqual(0, new KeyboardEvent('keydown', {key: 'a'}).charCode);
testing.expectEqual(0, new KeyboardEvent('keyup', {key: 'a'}).charCode);
testing.expectEqual(0, new KeyboardEvent('keypress', {key: 'Enter'}).charCode);
}
</script>

View File

@@ -248,6 +248,18 @@ pub const Key = union(enum) {
else => 0,
};
}
/// Legacy `KeyboardEvent.charCode` value per UI Events spec § Annex C
/// (https://www.w3.org/TR/uievents/#legacy-key-attributes). Returns the
/// Unicode code point of the character produced by the key. Only
/// meaningful inside a `keypress` event — callers must gate accordingly.
pub fn charCode(self: Key) u32 {
return switch (self) {
.Enter => 13,
.standard => |s| if (s.len > 0) s[0] else 0,
else => 0,
};
}
};
pub const Location = enum(i32) {
@@ -362,17 +374,22 @@ pub fn getShiftKey(self: *const KeyboardEvent) bool {
// charCode is the Unicode code point of the character produced by the key,
// and is only meaningful on `keypress` events. For `keydown` and `keyup` it
// is 0. (Deprecated, but read by legacy event handlers.)
//
// Chrome returns 0 for synthetic events (those created via
// `new KeyboardEvent(...)` rather than dispatched by the user agent), so we
// gate on `_is_trusted` to match.
pub fn getCharCode(self: *const KeyboardEvent) u32 {
const event = self._proto._proto;
if (!std.mem.eql(u8, event._type_string.str(), "keypress")) return 0;
return switch (self._key) {
.standard => |s| if (s.len > 0) s[0] else 0,
else => 0,
};
if (event._is_trusted == false) return 0;
if (event._type_string.eql(comptime .wrap("keypress")) == false) return 0;
return self._key.charCode();
}
// https://www.w3.org/TR/uievents/#dom-keyboardevent-keycode
//
// As with `charCode`, Chrome returns 0 for synthetic events.
pub fn getKeyCode(self: *const KeyboardEvent) u32 {
if (self._proto._proto._is_trusted == false) return 0;
return self._key.keyCode();
}
@@ -462,3 +479,77 @@ const testing = @import("../../../testing.zig");
test "WebApi: KeyboardEvent" {
try testing.htmlRunner("event/keyboard.html", .{});
}
test "KeyboardEvent: Key.keyCode mapping" {
// Letters: uppercase ASCII regardless of case.
try testing.expectEqual(@as(u32, 65), Key.keyCode(.{ .standard = "a" }));
try testing.expectEqual(@as(u32, 65), Key.keyCode(.{ .standard = "A" }));
try testing.expectEqual(@as(u32, 84), Key.keyCode(.{ .standard = "T" }));
try testing.expectEqual(@as(u32, 90), Key.keyCode(.{ .standard = "z" }));
// Digits.
try testing.expectEqual(@as(u32, 48), Key.keyCode(.{ .standard = "0" }));
try testing.expectEqual(@as(u32, 53), Key.keyCode(.{ .standard = "5" }));
try testing.expectEqual(@as(u32, 57), Key.keyCode(.{ .standard = "9" }));
// Space.
try testing.expectEqual(@as(u32, 32), Key.keyCode(.{ .standard = " " }));
// Modifier keys.
try testing.expectEqual(@as(u32, 16), Key.keyCode(.Shift));
try testing.expectEqual(@as(u32, 17), Key.keyCode(.Control));
try testing.expectEqual(@as(u32, 18), Key.keyCode(.Alt));
try testing.expectEqual(@as(u32, 91), Key.keyCode(.Meta));
try testing.expectEqual(@as(u32, 20), Key.keyCode(.CapsLock));
// Whitespace keys.
try testing.expectEqual(@as(u32, 13), Key.keyCode(.Enter));
try testing.expectEqual(@as(u32, 9), Key.keyCode(.Tab));
// Navigation keys.
try testing.expectEqual(@as(u32, 37), Key.keyCode(.ArrowLeft));
try testing.expectEqual(@as(u32, 38), Key.keyCode(.ArrowUp));
try testing.expectEqual(@as(u32, 39), Key.keyCode(.ArrowRight));
try testing.expectEqual(@as(u32, 40), Key.keyCode(.ArrowDown));
try testing.expectEqual(@as(u32, 33), Key.keyCode(.PageUp));
try testing.expectEqual(@as(u32, 34), Key.keyCode(.PageDown));
try testing.expectEqual(@as(u32, 35), Key.keyCode(.End));
try testing.expectEqual(@as(u32, 36), Key.keyCode(.Home));
// Editing keys.
try testing.expectEqual(@as(u32, 8), Key.keyCode(.Backspace));
try testing.expectEqual(@as(u32, 46), Key.keyCode(.Delete));
try testing.expectEqual(@as(u32, 45), Key.keyCode(.Insert));
// UI keys.
try testing.expectEqual(@as(u32, 27), Key.keyCode(.Escape));
try testing.expectEqual(@as(u32, 19), Key.keyCode(.Pause));
try testing.expectEqual(@as(u32, 93), Key.keyCode(.ContextMenu));
// Function keys.
try testing.expectEqual(@as(u32, 112), Key.keyCode(.F1));
try testing.expectEqual(@as(u32, 123), Key.keyCode(.F12));
// Keys without a defined fixed virtual key code.
try testing.expectEqual(@as(u32, 0), Key.keyCode(.Dead));
try testing.expectEqual(@as(u32, 0), Key.keyCode(.Unidentified));
try testing.expectEqual(@as(u32, 0), Key.keyCode(.{ .standard = "" }));
}
test "KeyboardEvent: Key.charCode mapping" {
// Printable characters: Unicode code point of the first byte.
try testing.expectEqual(@as(u32, 97), Key.charCode(.{ .standard = "a" }));
try testing.expectEqual(@as(u32, 65), Key.charCode(.{ .standard = "A" }));
try testing.expectEqual(@as(u32, 48), Key.charCode(.{ .standard = "0" }));
try testing.expectEqual(@as(u32, 32), Key.charCode(.{ .standard = " " }));
// Enter is the one named key that produces a charCode (\r = 13).
try testing.expectEqual(@as(u32, 13), Key.charCode(.Enter));
// Other named keys and the empty standard key produce no character.
try testing.expectEqual(@as(u32, 0), Key.charCode(.Tab));
try testing.expectEqual(@as(u32, 0), Key.charCode(.Escape));
try testing.expectEqual(@as(u32, 0), Key.charCode(.ArrowLeft));
try testing.expectEqual(@as(u32, 0), Key.charCode(.Shift));
try testing.expectEqual(@as(u32, 0), Key.charCode(.{ .standard = "" }));
}