From 45072ae2e05c814cde5ea4dcb7f5c73896de86aa Mon Sep 17 00:00:00 2001 From: Euclid Ye Date: Wed, 28 May 2025 16:51:05 +0800 Subject: [PATCH] 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 --- components/script/dom/document.rs | 3 +++ components/script/dom/htmlinputelement.rs | 25 ++++++++++--------- components/script/dom/htmltextareaelement.rs | 25 ++++++++++--------- components/script/textinput.rs | 26 +++++++++++++++----- 4 files changed, 51 insertions(+), 28 deletions(-) diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs index 6056f1f1e5a..fd4faf0ee43 100644 --- a/components/script/dom/document.rs +++ b/components/script/dom/document.rs @@ -2374,6 +2374,9 @@ impl Document { let mut cancel_state = event.get_cancel_state(); // 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 && is_character_value_key(&(keyboard_event.key)) && !keyboard_event.is_composing && diff --git a/components/script/dom/htmlinputelement.rs b/components/script/dom/htmlinputelement.rs index c6a3e2227f5..e914ad7f5d1 100644 --- a/components/script/dom/htmlinputelement.rs +++ b/components/script/dom/htmlinputelement.rs @@ -2883,6 +2883,17 @@ impl VirtualMethods for HTMLInputElement { self.implicit_submission(can_gc); }, 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.update_placeholder_shown_state(); self.upcast::().dirty(NodeDamage::OtherNodeDamage); @@ -2899,17 +2910,9 @@ impl VirtualMethods for HTMLInputElement { !event.DefaultPrevented() && self.input_type().is_textual_or_password() { - if event.IsTrusted() { - self.owner_global() - .task_manager() - .user_interaction_task_source() - .queue_event( - self.upcast(), - atom!("input"), - EventBubbles::Bubbles, - EventCancelable::NotCancelable, - ); - } + // keypress should be deprecated and replaced by beforeinput. + // keypress was supposed to fire "blur" and "focus" events + // but already done in `document.rs` } else if (event.type_() == atom!("compositionstart") || event.type_() == atom!("compositionupdate") || event.type_() == atom!("compositionend")) && diff --git a/components/script/dom/htmltextareaelement.rs b/components/script/dom/htmltextareaelement.rs index a7435b40fdd..0219c7928f2 100644 --- a/components/script/dom/htmltextareaelement.rs +++ b/components/script/dom/htmltextareaelement.rs @@ -643,6 +643,17 @@ impl VirtualMethods for HTMLTextAreaElement { match action { KeyReaction::TriggerDefaultAction => (), 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.update_placeholder_shown_state(); self.upcast::().dirty(NodeDamage::OtherNodeDamage); @@ -656,17 +667,9 @@ impl VirtualMethods for HTMLTextAreaElement { } } } else if event.type_() == atom!("keypress") && !event.DefaultPrevented() { - if event.IsTrusted() { - self.owner_global() - .task_manager() - .user_interaction_task_source() - .queue_event( - self.upcast(), - atom!("input"), - EventBubbles::Bubbles, - EventCancelable::NotCancelable, - ); - } + // keypress should be deprecated and replaced by beforeinput. + // keypress was supposed to fire "blur" and "focus" events + // but already done in `document.rs` } else if event.type_() == atom!("compositionstart") || event.type_() == atom!("compositionupdate") || event.type_() == atom!("compositionend") diff --git a/components/script/textinput.rs b/components/script/textinput.rs index 3cee1451528..73f6159a9a0 100644 --- a/components/script/textinput.rs +++ b/components/script/textinput.rs @@ -319,11 +319,18 @@ impl TextInput { } /// 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) { 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 @@ -895,6 +902,7 @@ impl TextInput { KeyReaction::RedrawSelection }) .shortcut(CMD_OR_CONTROL, 'X', || { + // FIXME: this is unreachable because ClipboardEvent is fired instead of keydown if let Some(text) = self.get_selection_text() { self.clipboard_provider.set_text(text); self.delete_char(Direction::Backward); @@ -914,12 +922,18 @@ impl TextInput { KeyReaction::DispatchInput }) .shortcut(Modifiers::empty(), Key::Delete, || { - self.delete_char(Direction::Forward); - KeyReaction::DispatchInput + if self.delete_char(Direction::Forward) { + KeyReaction::DispatchInput + } else { + KeyReaction::Nothing + } }) .shortcut(Modifiers::empty(), Key::Backspace, || { - self.delete_char(Direction::Backward); - KeyReaction::DispatchInput + if self.delete_char(Direction::Backward) { + KeyReaction::DispatchInput + } else { + KeyReaction::Nothing + } }) .optional_shortcut(macos, Modifiers::META, Key::ArrowLeft, || { self.adjust_horizontal_to_line_end(Direction::Backward, maybe_select);