Browse Source

LibWeb: Implement implicit submission of HTMLFormElement

Timothy Flynn 1 year ago
parent
commit
5d1657f57f

+ 28 - 0
Tests/LibWeb/Text/expected/HTML/form-implicit-submission.txt

@@ -0,0 +1,28 @@
+ wfh :^) PASS   wfh :^)    wfh :^) FAIL PASS FAIL   wfh :^) FAIL FAIL PASS  PASS  wfh :^)   wfh :^)  PASS  PASSwfh :^) FAIL   wfh :^) FAIL   wfh :^)   wfh :^) FAIL   wfh :^)    wfh :^)     defaultButton: click button=PASS
+defaultButton: submit
+defaultButton: handledEvent=true
+defaultButtonAsInput: click button=PASS
+defaultButtonAsInput: submit
+defaultButtonAsInput: handledEvent=true
+defaultButtonIsSecond: click button=PASS
+defaultButtonIsSecond: submit
+defaultButtonIsSecond: handledEvent=true
+defaultButtonIsLast: click button=PASS
+defaultButtonIsLast: submit
+defaultButtonIsLast: handledEvent=true
+defaultButtonIsBeforeForm: click button=PASS
+defaultButtonIsBeforeForm: submit
+defaultButtonIsBeforeForm: handledEvent=true
+defaultButtonIsAfterForm: click button=PASS
+defaultButtonIsAfterForm: submit
+defaultButtonIsAfterForm: handledEvent=true
+defaultButtonIsDynamicallyInserted: click button=PASS
+defaultButtonIsDynamicallyInserted: submit
+defaultButtonIsDynamicallyInserted: handledEvent=true
+defaultButtonIsDisabled: handledEvent=false
+noButton: submit
+noButton: handledEvent=true
+noDefaultButton: submit
+noDefaultButton: handledEvent=true
+excessiveBlockingElements1: handledEvent=false
+excessiveBlockingElements2: handledEvent=false

+ 94 - 0
Tests/LibWeb/Text/input/HTML/form-implicit-submission.html

@@ -0,0 +1,94 @@
+<form id="defaultButton">
+    <input />
+    <button>PASS</button>
+</form>
+<form id="defaultButtonAsInput">
+    <input />
+    <input type="submit" value="PASS" />
+</form>
+<form id="defaultButtonIsSecond">
+    <input />
+    <button type="button">FAIL</button>
+    <button>PASS</button>
+    <button type="button">FAIL</button>
+</form>
+<form id="defaultButtonIsLast">
+    <input />
+    <button type="button">FAIL</button>
+    <button type="button">FAIL</button>
+    <button>PASS</button>
+</form>
+<button form="defaultButtonIsBeforeForm">PASS</button>
+<form id="defaultButtonIsBeforeForm">
+    <input />
+</form>
+<form id="defaultButtonIsAfterForm">
+    <input />
+</form>
+<button form="defaultButtonIsAfterForm">PASS</button>
+<form id="defaultButtonIsDynamicallyInserted">
+    <input />
+    <button>FAIL</button>
+</form>
+<form id="defaultButtonIsDisabled">
+    <input />
+    <button disabled>FAIL</button>
+</form>
+<form id="noButton">
+    <input />
+</form>
+<form id="noDefaultButton">
+    <input />
+    <button type="button">FAIL</button>
+</form>
+<form id="excessiveBlockingElements1">
+    <input />
+    <input />
+</form>
+<form id="excessiveBlockingElements2">
+    <input />
+    <input type="time" />
+</form>
+<script src="../include.js"></script>
+<script>
+    let handledEvent = false;
+
+    const enterTextAndSubmitForm = form => {
+        const input = form.querySelector("input");
+
+        handledEvent = false;
+        internals.sendText(input, "wfh :^)");
+        internals.commitText();
+
+        println(`${form.id}: handledEvent=${handledEvent}`);
+    };
+
+    test(() => {
+        const button = document.createElement("button");
+        button.setAttribute("form", "defaultButtonIsDynamicallyInserted");
+        button.innerText = "PASS";
+
+        const dynamicForm = document.getElementById("defaultButtonIsDynamicallyInserted");
+        dynamicForm.insertBefore(button, dynamicForm.elements[0]);
+
+        document.querySelectorAll("form").forEach(form => {
+            form.addEventListener("submit", event => {
+                event.preventDefault();
+
+                println(`${form.id}: submit`);
+                handledEvent = true;
+            });
+
+            for (const element of form.elements) {
+                element.addEventListener("click", () => {
+                    const text = element.value || element.innerText;
+                    println(`${form.id}: click button=${text}`);
+
+                    handledEvent = true;
+                });
+            }
+
+            enterTextAndSubmitForm(form);
+        });
+    });
+</script>

+ 90 - 0
Userland/Libraries/LibWeb/HTML/HTMLFormElement.cpp

@@ -62,6 +62,34 @@ void HTMLFormElement::visit_edges(Cell::Visitor& visitor)
         visitor.visit(element);
         visitor.visit(element);
 }
 }
 
 
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#implicit-submission
+WebIDL::ExceptionOr<void> HTMLFormElement::implicitly_submit_form()
+{
+    // If the user agent supports letting the user submit a form implicitly (for example, on some platforms hitting the
+    // "enter" key while a text control is focused implicitly submits the form), then doing so for a form, whose default
+    // button has activation behavior and is not disabled, must cause the user agent to fire a click event at that
+    // default button.
+    if (auto* default_button = this->default_button()) {
+        auto& default_button_element = default_button->form_associated_element_to_html_element();
+
+        if (default_button_element.has_activation_behavior() && default_button->enabled())
+            default_button_element.click();
+
+        return {};
+    }
+
+    // If the form has no submit button, then the implicit submission mechanism must perform the following steps:
+
+    // 1. If the form has more than one field that blocks implicit submission, then return.
+    if (number_of_fields_blocking_implicit_submission() > 1)
+        return {};
+
+    // 2. Submit the form element from the form element itself with userInvolvement set to "activation".
+    TRY(submit_form(*this, { .user_involvement = UserNavigationInvolvement::Activation }));
+
+    return {};
+}
+
 // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-form-submit
 // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-form-submit
 WebIDL::ExceptionOr<void> HTMLFormElement::submit_form(JS::NonnullGCPtr<HTMLElement> submitter, SubmitFormOptions options)
 WebIDL::ExceptionOr<void> HTMLFormElement::submit_form(JS::NonnullGCPtr<HTMLElement> submitter, SubmitFormOptions options)
 {
 {
@@ -1012,4 +1040,66 @@ WebIDL::ExceptionOr<JS::Value> HTMLFormElement::named_item_value(FlyString const
     return node;
     return node;
 }
 }
 
 
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#default-button
+FormAssociatedElement* HTMLFormElement::default_button()
+{
+    // A form element's default button is the first submit button in tree order whose form owner is that form element.
+    FormAssociatedElement* default_button = nullptr;
+
+    root().for_each_in_subtree([&](auto& node) {
+        auto* form_associated_element = dynamic_cast<FormAssociatedElement*>(&node);
+        if (!form_associated_element)
+            return IterationDecision::Continue;
+
+        if (form_associated_element->form() == this && form_associated_element->is_submit_button()) {
+            default_button = form_associated_element;
+            return IterationDecision::Break;
+        }
+
+        return IterationDecision::Continue;
+    });
+
+    return default_button;
+}
+
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#field-that-blocks-implicit-submission
+size_t HTMLFormElement::number_of_fields_blocking_implicit_submission() const
+{
+    // For the purpose of the previous paragraph, an element is a field that blocks implicit submission of a form
+    // element if it is an input element whose form owner is that form element and whose type attribute is in one of
+    // the following states: Text, Search, Telephone, URL, Email, Password, Date, Month, Week, Time,
+    // Local Date and Time, Number.
+    size_t count = 0;
+
+    for (auto element : m_associated_elements) {
+        if (!is<HTMLInputElement>(*element))
+            continue;
+
+        auto const& input = static_cast<HTMLInputElement&>(*element);
+        using enum HTMLInputElement::TypeAttributeState;
+
+        switch (input.type_state()) {
+        case Text:
+        case Search:
+        case Telephone:
+        case URL:
+        case Email:
+        case Password:
+        case Date:
+        case Month:
+        case Week:
+        case Time:
+        case LocalDateAndTime:
+        case Number:
+            ++count;
+            break;
+
+        default:
+            break;
+        }
+    };
+
+    return count;
+}
+
 }
 }

+ 4 - 0
Userland/Libraries/LibWeb/HTML/HTMLFormElement.h

@@ -59,6 +59,7 @@ public:
         UserNavigationInvolvement user_involvement = { UserNavigationInvolvement::None };
         UserNavigationInvolvement user_involvement = { UserNavigationInvolvement::None };
     };
     };
     WebIDL::ExceptionOr<void> submit_form(JS::NonnullGCPtr<HTMLElement> submitter, SubmitFormOptions);
     WebIDL::ExceptionOr<void> submit_form(JS::NonnullGCPtr<HTMLElement> submitter, SubmitFormOptions);
+    WebIDL::ExceptionOr<void> implicitly_submit_form();
 
 
     void reset_form();
     void reset_form();
 
 
@@ -117,6 +118,9 @@ private:
     ErrorOr<void> mail_as_body(AK::URL parsed_action, Vector<XHR::FormDataEntry> entry_list, EncodingTypeAttributeState encoding_type, String encoding, JS::NonnullGCPtr<Navigable> target_navigable, Bindings::NavigationHistoryBehavior history_handling, UserNavigationInvolvement user_involvement);
     ErrorOr<void> mail_as_body(AK::URL parsed_action, Vector<XHR::FormDataEntry> entry_list, EncodingTypeAttributeState encoding_type, String encoding, JS::NonnullGCPtr<Navigable> target_navigable, Bindings::NavigationHistoryBehavior history_handling, UserNavigationInvolvement user_involvement);
     void plan_to_navigate_to(AK::URL url, Variant<Empty, String, POSTResource> post_resource, JS::NonnullGCPtr<Navigable> target_navigable, Bindings::NavigationHistoryBehavior history_handling, UserNavigationInvolvement user_involvement);
     void plan_to_navigate_to(AK::URL url, Variant<Empty, String, POSTResource> post_resource, JS::NonnullGCPtr<Navigable> target_navigable, Bindings::NavigationHistoryBehavior history_handling, UserNavigationInvolvement user_involvement);
 
 
+    FormAssociatedElement* default_button();
+    size_t number_of_fields_blocking_implicit_submission() const;
+
     bool m_firing_submission_events { false };
     bool m_firing_submission_events { false };
 
 
     // https://html.spec.whatwg.org/multipage/forms.html#locked-for-reset
     // https://html.spec.whatwg.org/multipage/forms.html#locked-for-reset

+ 13 - 4
Userland/Libraries/LibWeb/Page/EventHandler.cpp

@@ -11,6 +11,7 @@
 #include <LibWeb/HTML/BrowsingContext.h>
 #include <LibWeb/HTML/BrowsingContext.h>
 #include <LibWeb/HTML/Focus.h>
 #include <LibWeb/HTML/Focus.h>
 #include <LibWeb/HTML/HTMLAnchorElement.h>
 #include <LibWeb/HTML/HTMLAnchorElement.h>
+#include <LibWeb/HTML/HTMLFormElement.h>
 #include <LibWeb/HTML/HTMLIFrameElement.h>
 #include <LibWeb/HTML/HTMLIFrameElement.h>
 #include <LibWeb/HTML/HTMLImageElement.h>
 #include <LibWeb/HTML/HTMLImageElement.h>
 #include <LibWeb/HTML/HTMLInputElement.h>
 #include <LibWeb/HTML/HTMLInputElement.h>
@@ -816,10 +817,18 @@ bool EventHandler::handle_keydown(KeyCode key, u32 modifiers, u32 code_point)
             m_browsing_context->set_cursor_position(DOM::Position::create(realm, node, (unsigned)node.data().bytes().size()));
             m_browsing_context->set_cursor_position(DOM::Position::create(realm, node, (unsigned)node.data().bytes().size()));
             return true;
             return true;
         }
         }
-        if (key == KeyCode::Key_Return && is<HTML::HTMLInputElement>(node.editable_text_node_owner())) {
-            auto& input_element = static_cast<HTML::HTMLInputElement&>(*node.editable_text_node_owner());
-            input_element.commit_pending_changes();
-            return true;
+        if (key == KeyCode::Key_Return) {
+            if (is<HTML::HTMLInputElement>(node.editable_text_node_owner())) {
+                auto& input_element = static_cast<HTML::HTMLInputElement&>(*node.editable_text_node_owner());
+
+                if (auto* form = input_element.form()) {
+                    form->implicitly_submit_form().release_value_but_fixme_should_propagate_errors();
+                    return true;
+                }
+
+                input_element.commit_pending_changes();
+                return true;
+            }
         }
         }
         // FIXME: Text editing shortcut keys (copy/paste etc.) should be handled here.
         // FIXME: Text editing shortcut keys (copy/paste etc.) should be handled here.
         if (!should_ignore_keydown_event(code_point, modifiers)) {
         if (!should_ignore_keydown_event(code_point, modifiers)) {