Let input JS event be dispatched by keydown instead of keypress (#37078)

1. Let `input` JS event be dispatched by `keydown` instead of
`keypress`, according to spec
2. Fire `input` event for Backspace and Delete. But do so only when
something is actually deleted

Testing: Manually tested and compared with other browsers.
Fixes: #37051
cc @xiaochengh

Signed-off-by: Euclid Ye <yezhizhenjiakang@gmail.com>
This commit is contained in:
Euclid Ye 2025-05-28 16:51:05 +08:00 committed by GitHub
parent 8ebf344e5e
commit 45072ae2e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 51 additions and 28 deletions

View file

@ -2374,6 +2374,9 @@ impl Document {
let mut cancel_state = event.get_cancel_state(); let mut cancel_state = event.get_cancel_state();
// https://w3c.github.io/uievents/#keys-cancelable-keys // https://w3c.github.io/uievents/#keys-cancelable-keys
// it MUST prevent the respective beforeinput and input
// (and keypress if supported) events from being generated
// TODO: keypress should be deprecated and superceded by beforeinput
if keyboard_event.state == KeyState::Down && if keyboard_event.state == KeyState::Down &&
is_character_value_key(&(keyboard_event.key)) && is_character_value_key(&(keyboard_event.key)) &&
!keyboard_event.is_composing && !keyboard_event.is_composing &&

View file

@ -2883,6 +2883,17 @@ impl VirtualMethods for HTMLInputElement {
self.implicit_submission(can_gc); self.implicit_submission(can_gc);
}, },
DispatchInput => { DispatchInput => {
if event.IsTrusted() {
self.owner_global()
.task_manager()
.user_interaction_task_source()
.queue_event(
self.upcast(),
atom!("input"),
EventBubbles::Bubbles,
EventCancelable::NotCancelable,
);
}
self.value_dirty.set(true); self.value_dirty.set(true);
self.update_placeholder_shown_state(); self.update_placeholder_shown_state();
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage); self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
@ -2899,17 +2910,9 @@ impl VirtualMethods for HTMLInputElement {
!event.DefaultPrevented() && !event.DefaultPrevented() &&
self.input_type().is_textual_or_password() self.input_type().is_textual_or_password()
{ {
if event.IsTrusted() { // keypress should be deprecated and replaced by beforeinput.
self.owner_global() // keypress was supposed to fire "blur" and "focus" events
.task_manager() // but already done in `document.rs`
.user_interaction_task_source()
.queue_event(
self.upcast(),
atom!("input"),
EventBubbles::Bubbles,
EventCancelable::NotCancelable,
);
}
} else if (event.type_() == atom!("compositionstart") || } else if (event.type_() == atom!("compositionstart") ||
event.type_() == atom!("compositionupdate") || event.type_() == atom!("compositionupdate") ||
event.type_() == atom!("compositionend")) && event.type_() == atom!("compositionend")) &&

View file

@ -643,6 +643,17 @@ impl VirtualMethods for HTMLTextAreaElement {
match action { match action {
KeyReaction::TriggerDefaultAction => (), KeyReaction::TriggerDefaultAction => (),
KeyReaction::DispatchInput => { KeyReaction::DispatchInput => {
if event.IsTrusted() {
self.owner_global()
.task_manager()
.user_interaction_task_source()
.queue_event(
self.upcast(),
atom!("input"),
EventBubbles::Bubbles,
EventCancelable::NotCancelable,
);
}
self.value_dirty.set(true); self.value_dirty.set(true);
self.update_placeholder_shown_state(); self.update_placeholder_shown_state();
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage); self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
@ -656,17 +667,9 @@ impl VirtualMethods for HTMLTextAreaElement {
} }
} }
} else if event.type_() == atom!("keypress") && !event.DefaultPrevented() { } else if event.type_() == atom!("keypress") && !event.DefaultPrevented() {
if event.IsTrusted() { // keypress should be deprecated and replaced by beforeinput.
self.owner_global() // keypress was supposed to fire "blur" and "focus" events
.task_manager() // but already done in `document.rs`
.user_interaction_task_source()
.queue_event(
self.upcast(),
atom!("input"),
EventBubbles::Bubbles,
EventCancelable::NotCancelable,
);
}
} else if event.type_() == atom!("compositionstart") || } else if event.type_() == atom!("compositionstart") ||
event.type_() == atom!("compositionupdate") || event.type_() == atom!("compositionupdate") ||
event.type_() == atom!("compositionend") event.type_() == atom!("compositionend")

View file

@ -319,11 +319,18 @@ impl<T: ClipboardProvider> TextInput<T> {
} }
/// Remove a character at the current editing point /// Remove a character at the current editing point
pub fn delete_char(&mut self, dir: Direction) { ///
/// Returns true if any character was deleted
pub fn delete_char(&mut self, dir: Direction) -> bool {
if self.selection_origin.is_none() || self.selection_origin == Some(self.edit_point) { if self.selection_origin.is_none() || self.selection_origin == Some(self.edit_point) {
self.adjust_horizontal_by_one(dir, Selection::Selected); self.adjust_horizontal_by_one(dir, Selection::Selected);
} }
self.replace_selection(DOMString::new()); if self.selection_start() == self.selection_end() {
false
} else {
self.replace_selection(DOMString::new());
true
}
} }
/// Insert a character at the current editing point /// Insert a character at the current editing point
@ -895,6 +902,7 @@ impl<T: ClipboardProvider> TextInput<T> {
KeyReaction::RedrawSelection KeyReaction::RedrawSelection
}) })
.shortcut(CMD_OR_CONTROL, 'X', || { .shortcut(CMD_OR_CONTROL, 'X', || {
// FIXME: this is unreachable because ClipboardEvent is fired instead of keydown
if let Some(text) = self.get_selection_text() { if let Some(text) = self.get_selection_text() {
self.clipboard_provider.set_text(text); self.clipboard_provider.set_text(text);
self.delete_char(Direction::Backward); self.delete_char(Direction::Backward);
@ -914,12 +922,18 @@ impl<T: ClipboardProvider> TextInput<T> {
KeyReaction::DispatchInput KeyReaction::DispatchInput
}) })
.shortcut(Modifiers::empty(), Key::Delete, || { .shortcut(Modifiers::empty(), Key::Delete, || {
self.delete_char(Direction::Forward); if self.delete_char(Direction::Forward) {
KeyReaction::DispatchInput KeyReaction::DispatchInput
} else {
KeyReaction::Nothing
}
}) })
.shortcut(Modifiers::empty(), Key::Backspace, || { .shortcut(Modifiers::empty(), Key::Backspace, || {
self.delete_char(Direction::Backward); if self.delete_char(Direction::Backward) {
KeyReaction::DispatchInput KeyReaction::DispatchInput
} else {
KeyReaction::Nothing
}
}) })
.optional_shortcut(macos, Modifiers::META, Key::ArrowLeft, || { .optional_shortcut(macos, Modifiers::META, Key::ArrowLeft, || {
self.adjust_horizontal_to_line_end(Direction::Backward, maybe_select); self.adjust_horizontal_to_line_end(Direction::Backward, maybe_select);