diff --git a/src/browser/tests/element/html/input.html b/src/browser/tests/element/html/input.html
index a56d553ca..b3b70dc95 100644
--- a/src/browser/tests/element/html/input.html
+++ b/src/browser/tests/element/html/input.html
@@ -609,5 +609,13 @@
email.select();
testing.expectEqual(1, selects);
testing.expectError('InvalidStateError', () => email.setSelectionRange(0, 1));
+
+ const date = document.createElement('input');
+ date.type = 'date';
+ date.value = '2026-09-28';
+ let date_selects = 0;
+ date.addEventListener('select', () => date_selects++);
+ date.select();
+ testing.expectEqual(0, date_selects);
}
diff --git a/src/browser/webapi/element/html/Input.zig b/src/browser/webapi/element/html/Input.zig
index cb955f367..418848f5b 100644
--- a/src/browser/webapi/element/html/Input.zig
+++ b/src/browser/webapi/element/html/Input.zig
@@ -628,9 +628,10 @@ const entry = text_entry.TextEntry(Input);
pub const select = entry.select;
pub const innerInsert = entry.innerInsert;
pub const acceptsTextEntry = entry.acceptsTextEntry;
-pub const tracksSelection = entry.tracksSelection;
+const tracksSelection = entry.tracksSelection;
pub const innerDelete = entry.innerDelete;
pub const moveCaret = entry.moveCaret;
+pub const caretToEnd = entry.caretToEnd;
pub const CaretMove = entry.CaretMove;
pub const getSelectionDirection = entry.getSelectionDirection;
pub const setSelectionStart = entry.setSelectionStart;
diff --git a/src/browser/webapi/element/html/TextArea.zig b/src/browser/webapi/element/html/TextArea.zig
index a4aa87934..42431e682 100644
--- a/src/browser/webapi/element/html/TextArea.zig
+++ b/src/browser/webapi/element/html/TextArea.zig
@@ -143,6 +143,7 @@ pub const innerInsert = entry.innerInsert;
pub const acceptsTextEntry = entry.acceptsTextEntry;
pub const innerDelete = entry.innerDelete;
pub const moveCaret = entry.moveCaret;
+pub const caretToEnd = entry.caretToEnd;
pub const CaretMove = entry.CaretMove;
pub const getSelectionDirection = entry.getSelectionDirection;
pub const setSelectionStart = entry.setSelectionStart;
diff --git a/src/browser/webapi/element/text_entry.zig b/src/browser/webapi/element/text_entry.zig
index f1458a391..72f466880 100644
--- a/src/browser/webapi/element/text_entry.zig
+++ b/src/browser/webapi/element/text_entry.zig
@@ -77,8 +77,10 @@ pub fn TextEntry(comptime T: type) type {
// fully selected, replace the content.
const new_value = try arena.dupe(u8, str);
try self.setUserValue(new_value, frame);
- self._selection_start = @intCast(new_value.len);
- self._selection_end = @intCast(new_value.len);
+ // the sanitized value can be shorter than what was inserted
+ const new_len: u32 = @intCast(self.getValue().len);
+ self._selection_start = new_len;
+ self._selection_end = new_len;
self._selection_direction = .none;
try dispatchSelectionChangeEvent(self, frame);
},
@@ -95,9 +97,9 @@ pub fn TextEntry(comptime T: type) type {
);
try self.setUserValue(new_value, frame);
- const new_pos = range[0] + str.len;
- self._selection_start = @intCast(new_pos);
- self._selection_end = @intCast(new_pos);
+ const new_pos: u32 = @intCast(@min(range[0] + str.len, self.getValue().len));
+ self._selection_start = new_pos;
+ self._selection_end = new_pos;
self._selection_direction = .none;
try dispatchSelectionChangeEvent(self, frame);
},
@@ -171,6 +173,16 @@ pub fn TextEntry(comptime T: type) type {
try dispatchInputEvent(self, null, if (forward) "deleteContentForward" else "deleteContentBackward", frame);
}
+ // Collapses the selection to the end of the value. Unlike
+ // setSelectionRange(), applies to email and number too.
+ pub fn caretToEnd(self: *T, frame: *Frame) !void {
+ if (tracksSelection(self) == false) {
+ return;
+ }
+ const len: u32 = @intCast(self.getValue().len);
+ try applySelectionRange(self, len, len, .none, frame);
+ }
+
pub const CaretMove = enum { backward, forward, line_start, line_end };
// Default action of the caret movement keys (ArrowLeft, ArrowRight,
diff --git a/src/server/bidi/http_command.zig b/src/server/bidi/http_command.zig
index 9a00fe4ae..a59e80921 100644
--- a/src/server/bidi/http_command.zig
+++ b/src/server/bidi/http_command.zig
@@ -689,9 +689,9 @@ fn elementSendKeys(cmd: *BiDi.Command, p: SendKeys) !void {
return cmd.sendError("element not interactable", "element can't be focused");
}
if (element.is(Html.Input)) |input_element| {
- try caretToEnd(input_element, frame);
+ try input_element.caretToEnd(frame);
} else if (element.is(Html.TextArea)) |textarea| {
- try caretToEnd(textarea, frame);
+ try textarea.caretToEnd(frame);
}
}
@@ -702,14 +702,6 @@ fn elementSendKeys(cmd: *BiDi.Command, p: SendKeys) !void {
return browsing_context.answerAfterNavigation(cmd, ctx, frame);
}
-fn caretToEnd(ctl: anytype, frame: *Frame) !void {
- if (ctl.selectionAvailable() == false) {
- return;
- }
- const len: u32 = @intCast(ctl.getValue().len);
- try ctl.setSelectionRange(len, len, null, frame);
-}
-
fn requireInteractable(cmd: *BiDi.Command, element: *Node.Element, frame: *Frame) !bool {
element.scrollIntoView(null, frame);
if (element.checkVisibility(.{}, frame)) {
diff --git a/src/server/cdp/domains/input.zig b/src/server/cdp/domains/input.zig
index e2d3e3ccb..c590d7649 100644
--- a/src/server/cdp/domains/input.zig
+++ b/src/server/cdp/domains/input.zig
@@ -229,6 +229,11 @@ test "cdp.input: insertText replaces select()ed value of email and number inputs
_ = try ls.local.compileAndRun("inp.type = 'number'; inp.value = '12'; inp.select();", null);
try ctx.processMessage(.{ .id = 2, .method = "Input.insertText", .params = .{ .text = "345" } });
try testing.expect((try ls.local.compileAndRun("inp.value === '345'", null)).isTrue());
+
+ // The caret lands at the end of the sanitized value, not of the inserted text.
+ _ = try ls.local.compileAndRun("inp.type = 'text'; inp.value = 'ab'; inp.select();", null);
+ try ctx.processMessage(.{ .id = 3, .method = "Input.insertText", .params = .{ .text = "c\nd" } });
+ try testing.expect((try ls.local.compileAndRun("inp.value === 'cd' && inp.selectionStart === 2", null)).isTrue());
}
test "cdp.input: dispatchMouseEvent mouseMoved fires hover events" {