LibWeb: Implement implicit submission of HTMLFormElement

This commit is contained in:
Timothy Flynn 2024-01-31 13:26:01 -05:00 committed by Andrew Kaster
parent a17074422e
commit 5d1657f57f
5 changed files with 229 additions and 4 deletions

View file

@ -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

View file

@ -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>

View file

@ -62,6 +62,34 @@ void HTMLFormElement::visit_edges(Cell::Visitor& visitor)
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
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;
}
// 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;
}
}

View file

@ -59,6 +59,7 @@ public:
UserNavigationInvolvement user_involvement = { UserNavigationInvolvement::None };
};
WebIDL::ExceptionOr<void> submit_form(JS::NonnullGCPtr<HTMLElement> submitter, SubmitFormOptions);
WebIDL::ExceptionOr<void> implicitly_submit_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);
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 };
// https://html.spec.whatwg.org/multipage/forms.html#locked-for-reset

View file

@ -11,6 +11,7 @@
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/Focus.h>
#include <LibWeb/HTML/HTMLAnchorElement.h>
#include <LibWeb/HTML/HTMLFormElement.h>
#include <LibWeb/HTML/HTMLIFrameElement.h>
#include <LibWeb/HTML/HTMLImageElement.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()));
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.
if (!should_ignore_keydown_event(code_point, modifiers)) {