diff --git a/Tests/LibWeb/Text/expected/input-commit.txt b/Tests/LibWeb/Text/expected/input-commit.txt
new file mode 100644
index 0000000000..cc7af5f15f
--- /dev/null
+++ b/Tests/LibWeb/Text/expected/input-commit.txt
@@ -0,0 +1 @@
+wfh :^) wfh :^)
diff --git a/Tests/LibWeb/Text/input/input-commit.html b/Tests/LibWeb/Text/input/input-commit.html
new file mode 100644
index 0000000000..a3140eae07
--- /dev/null
+++ b/Tests/LibWeb/Text/input/input-commit.html
@@ -0,0 +1,15 @@
+
+
+
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp
index e3113f8d3e..7c397dcd68 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp
+++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp
@@ -276,8 +276,6 @@ WebIDL::ExceptionOr HTMLInputElement::run_input_activation_behavior()
TRY(form->submit_form(*this));
} else if (type_state() == TypeAttributeState::FileUpload || type_state() == TypeAttributeState::Color) {
show_the_picker_if_applicable(*this);
- } else {
- dispatch_event(DOM::Event::create(realm(), EventNames::change));
}
return {};
@@ -289,6 +287,8 @@ void HTMLInputElement::did_edit_text_node(Badge)
m_value = value_sanitization_algorithm(m_text_node->data());
m_dirty_value = true;
+ m_has_uncommitted_changes = true;
+
update_placeholder_visibility();
// NOTE: This is a bit ad-hoc, but basically implements part of "4.10.5.5 Common event behaviors"
@@ -406,6 +406,33 @@ WebIDL::ExceptionOr HTMLInputElement::set_value(String const& value)
return {};
}
+void HTMLInputElement::commit_pending_changes()
+{
+ // The change event fires when the value is committed, if that makes sense for the control,
+ // or else when the control loses focus
+ switch (type_state()) {
+ case TypeAttributeState::Email:
+ case TypeAttributeState::Password:
+ case TypeAttributeState::Search:
+ case TypeAttributeState::Telephone:
+ case TypeAttributeState::Text:
+ case TypeAttributeState::URL:
+ if (!m_has_uncommitted_changes)
+ return;
+ break;
+
+ default:
+ break;
+ }
+
+ m_has_uncommitted_changes = false;
+
+ queue_an_element_task(HTML::Task::Source::UserInteraction, [this] {
+ auto change_event = DOM::Event::create(realm(), HTML::EventNames::change, { .bubbles = true });
+ dispatch_event(change_event);
+ });
+}
+
void HTMLInputElement::update_placeholder_visibility()
{
if (!m_placeholder_element)
@@ -615,13 +642,7 @@ void HTMLInputElement::did_receive_focus()
void HTMLInputElement::did_lose_focus()
{
- // The change event fires when the value is committed, if that makes sense for the control,
- // or else when the control loses focus
- queue_an_element_task(HTML::Task::Source::UserInteraction, [this] {
- auto change_event = DOM::Event::create(realm(), HTML::EventNames::change);
- change_event->set_bubbles(true);
- dispatch_event(change_event);
- });
+ commit_pending_changes();
}
void HTMLInputElement::attribute_changed(FlyString const& name, Optional const& value)
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h
index 2c381a9c51..ccb22794d2 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h
+++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h
@@ -69,6 +69,8 @@ public:
virtual String value() const override;
WebIDL::ExceptionOr set_value(String const&);
+ void commit_pending_changes();
+
Optional placeholder_value() const;
bool checked() const { return m_checked; }
@@ -215,6 +217,8 @@ private:
TypeAttributeState m_type { TypeAttributeState::Text };
String m_value;
+
+ bool m_has_uncommitted_changes { false };
};
}
diff --git a/Userland/Libraries/LibWeb/Internals/Internals.cpp b/Userland/Libraries/LibWeb/Internals/Internals.cpp
index d8965a1889..75fcd3ffad 100644
--- a/Userland/Libraries/LibWeb/Internals/Internals.cpp
+++ b/Userland/Libraries/LibWeb/Internals/Internals.cpp
@@ -11,6 +11,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -62,6 +63,27 @@ JS::Object* Internals::hit_test(double x, double y)
return nullptr;
}
+void Internals::send_text(HTML::HTMLElement& target, String const& text)
+{
+ auto* page = global_object().browsing_context()->page();
+ if (!page)
+ return;
+
+ target.focus();
+
+ for (auto code_point : text.code_points())
+ page->handle_keydown(code_point_to_key_code(code_point), 0, code_point);
+}
+
+void Internals::commit_text()
+{
+ auto* page = global_object().browsing_context()->page();
+ if (!page)
+ return;
+
+ page->handle_keydown(Key_Return, 0, 0);
+}
+
WebIDL::ExceptionOr Internals::dispatch_user_activated_event(DOM::EventTarget& target, DOM::Event& event)
{
event.set_is_trusted(true);
diff --git a/Userland/Libraries/LibWeb/Internals/Internals.h b/Userland/Libraries/LibWeb/Internals/Internals.h
index b5c1a7fa62..c3f21ac44e 100644
--- a/Userland/Libraries/LibWeb/Internals/Internals.h
+++ b/Userland/Libraries/LibWeb/Internals/Internals.h
@@ -22,6 +22,9 @@ public:
void gc();
JS::Object* hit_test(double x, double y);
+ void send_text(HTML::HTMLElement&, String const&);
+ void commit_text();
+
WebIDL::ExceptionOr dispatch_user_activated_event(DOM::EventTarget&, DOM::Event& event);
private:
diff --git a/Userland/Libraries/LibWeb/Internals/Internals.idl b/Userland/Libraries/LibWeb/Internals/Internals.idl
index ea7f624cdf..69f3733173 100644
--- a/Userland/Libraries/LibWeb/Internals/Internals.idl
+++ b/Userland/Libraries/LibWeb/Internals/Internals.idl
@@ -1,10 +1,15 @@
#import
+#import
[Exposed=Nobody] interface Internals {
undefined signalTextTestIsDone();
undefined gc();
object hitTest(double x, double y);
+
+ undefined sendText(HTMLElement target, DOMString text);
+ undefined commitText();
+
boolean dispatchUserActivatedEvent(EventTarget target, Event event);
};
diff --git a/Userland/Libraries/LibWeb/Page/EventHandler.cpp b/Userland/Libraries/LibWeb/Page/EventHandler.cpp
index 88cdbca3e4..87596dff91 100644
--- a/Userland/Libraries/LibWeb/Page/EventHandler.cpp
+++ b/Userland/Libraries/LibWeb/Page/EventHandler.cpp
@@ -13,6 +13,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -769,6 +770,8 @@ bool EventHandler::handle_keydown(KeyCode key, unsigned modifiers, u32 code_poin
}
if (m_browsing_context->cursor_position() && m_browsing_context->cursor_position()->node()->is_editable()) {
+ auto& node = verify_cast(*m_browsing_context->cursor_position()->node());
+
if (key == KeyCode::Key_Backspace) {
if (!m_browsing_context->decrement_cursor_position_offset()) {
// FIXME: Move to the previous node and delete the last character there.
@@ -799,15 +802,18 @@ bool EventHandler::handle_keydown(KeyCode key, unsigned modifiers, u32 code_poin
return true;
}
if (key == KeyCode::Key_Home) {
- auto& node = verify_cast(*m_browsing_context->cursor_position()->node());
m_browsing_context->set_cursor_position(DOM::Position::create(realm, node, 0));
return true;
}
if (key == KeyCode::Key_End) {
- auto& node = verify_cast(*m_browsing_context->cursor_position()->node());
m_browsing_context->set_cursor_position(DOM::Position::create(realm, node, (unsigned)node.data().bytes().size()));
return true;
}
+ if (key == KeyCode::Key_Return && is(node.editable_text_node_owner())) {
+ auto& input_element = static_cast(*node.editable_text_node_owner());
+ input_element.commit_pending_changes();
+ return true;
+ }
if (!should_ignore_keydown_event(code_point)) {
m_edit_event_handler->handle_insert(JS::NonnullGCPtr { *m_browsing_context->cursor_position() }, code_point);
m_browsing_context->increment_cursor_position_offset();