mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 16:52:43 +00:00 
			
		
		
		
	LibWeb: Implement implicit submission of HTMLFormElement
This commit is contained in:
		
							parent
							
								
									a17074422e
								
							
						
					
					
						commit
						5d1657f57f
					
				
					 5 changed files with 229 additions and 4 deletions
				
			
		
							
								
								
									
										28
									
								
								Tests/LibWeb/Text/expected/HTML/form-implicit-submission.txt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								Tests/LibWeb/Text/expected/HTML/form-implicit-submission.txt
									
										
									
									
									
										Normal 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 | ||||
							
								
								
									
										94
									
								
								Tests/LibWeb/Text/input/HTML/form-implicit-submission.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								Tests/LibWeb/Text/input/HTML/form-implicit-submission.html
									
										
									
									
									
										Normal 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> | ||||
|  | @ -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; | ||||
| } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -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
 | ||||
|  |  | |||
|  | @ -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)) { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy Flynn
						Timothy Flynn