diff --git a/Libraries/LibWeb/CSS/PseudoClasses.json b/Libraries/LibWeb/CSS/PseudoClasses.json index 0fb9d81e4b4..a799dba1c8b 100644 --- a/Libraries/LibWeb/CSS/PseudoClasses.json +++ b/Libraries/LibWeb/CSS/PseudoClasses.json @@ -53,6 +53,9 @@ "indeterminate": { "argument": "" }, + "invalid": { + "argument": "" + }, "is": { "argument": "" }, @@ -137,6 +140,15 @@ "target-within": { "argument": "" }, + "user-invalid": { + "argument": "" + }, + "user-valid": { + "argument": "" + }, + "valid": { + "argument": "" + }, "visited": { "argument": "" }, diff --git a/Libraries/LibWeb/CSS/SelectorEngine.cpp b/Libraries/LibWeb/CSS/SelectorEngine.cpp index 6fee18ac11e..7d458eb5410 100644 --- a/Libraries/LibWeb/CSS/SelectorEngine.cpp +++ b/Libraries/LibWeb/CSS/SelectorEngine.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -750,7 +751,7 @@ static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoCla return false; } case CSS::PseudoClass::PopoverOpen: { - // https://html.spec.whatwg.org/#selector-popover-open + // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-popover-open // The :popover-open pseudo-class is defined to match any HTML element whose popover attribute is not in the no popover state and whose popover visibility state is showing. if (is(element) && element.has_attribute(HTML::AttributeNames::popover)) { auto& html_element = static_cast(element); @@ -759,6 +760,134 @@ static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoCla return false; } + case CSS::PseudoClass::Valid: { + // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-valid + // The :valid pseudo-class must match any element falling into one of the following categories: + + // - elements that are candidates for constraint validation and that satisfy their constraints + if (auto form_associated_element = as_if(element)) + if (form_associated_element->is_candidate_for_constraint_validation() && form_associated_element->satisfies_its_constraints()) + return true; + + // - form elements that are not the form owner of any elements that themselves are candidates for constraint validation but do not satisfy their constraints + if (auto form_element = as_if(element)) { + bool has_invalid_elements = false; + element.for_each_in_subtree([&](auto& node) { + if (auto form_associated_element = as_if(&node)) { + if (form_associated_element->form() == form_element && form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints()) { + has_invalid_elements = true; + return TraversalDecision::Break; + } + } + return TraversalDecision::Continue; + }); + if (!has_invalid_elements) + return true; + } + + // - fieldset elements that have no descendant elements that themselves are candidates for constraint validation but do not satisfy their constraints + if (is(element)) { + bool has_invalid_children = false; + element.for_each_in_subtree([&](auto& node) { + if (auto form_associated_element = as_if(&node)) { + if (form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints()) { + has_invalid_children = true; + return TraversalDecision::Break; + } + } + return TraversalDecision::Continue; + }); + if (!has_invalid_children) + return true; + } + + return false; + } + case CSS::PseudoClass::Invalid: { + // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-invalid + // The :invalid pseudo-class must match any element falling into one of the following categories: + + // - elements that are candidates for constraint validation but that do not satisfy their constraints + if (auto form_associated_element = as_if(element)) + if (form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints()) + return true; + + // - form elements that are the form owner of one or more elements that themselves are candidates for constraint validation but do not satisfy their constraints + if (auto form_element = as_if(element)) { + bool has_invalid_elements = false; + element.for_each_in_subtree([&](auto& node) { + if (auto form_associated_element = as_if(&node)) { + if (form_associated_element->form() == form_element && form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints()) { + has_invalid_elements = true; + return TraversalDecision::Break; + } + } + return TraversalDecision::Continue; + }); + if (has_invalid_elements) + return true; + } + + // - fieldset elements that have of one or more descendant elements that themselves are candidates for constraint validation but do not satisfy their constraints + if (is(element)) { + bool has_invalid_children = false; + element.for_each_in_subtree([&](auto& node) { + if (auto form_associated_element = as_if(&node)) { + if (form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints()) { + has_invalid_children = true; + return TraversalDecision::Break; + } + } + return TraversalDecision::Continue; + }); + if (has_invalid_children) + return true; + } + + return false; + } + case CSS::PseudoClass::UserValid: { + // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-user-valid + // The :user-valid pseudo-class must match input, textarea, and select elements whose user validity is true, + bool user_validity = false; + if (auto input_element = as_if(element)) { + user_validity = input_element->user_validity(); + } else if (auto select_element = as_if(element)) { + user_validity = select_element->user_validity(); + } else if (auto text_area_element = as_if(element)) { + user_validity = text_area_element->user_validity(); + } + if (!user_validity) + return false; + + // are candidates for constraint validation, and that satisfy their constraints. + auto& form_associated_element = as(element); + if (form_associated_element.is_candidate_for_constraint_validation() && form_associated_element.satisfies_its_constraints()) + return true; + + return false; + } + case CSS::PseudoClass::UserInvalid: { + // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-user-invalid + // The :user-invalid pseudo-class must match input, textarea, and select elements whose user validity is true, + bool user_validity = false; + if (auto input_element = as_if(element)) { + user_validity = input_element->user_validity(); + } else if (auto select_element = as_if(element)) { + user_validity = select_element->user_validity(); + } else if (auto text_area_element = as_if(element)) { + user_validity = text_area_element->user_validity(); + } + if (!user_validity) + return false; + + // are candidates for constraint validation but do not satisfy their constraints. + auto& form_associated_element = as(element); + if (form_associated_element.is_candidate_for_constraint_validation() && !form_associated_element.satisfies_its_constraints()) + return true; + + return false; + } } return false; @@ -923,7 +1052,7 @@ static bool fast_matches_simple_selector(CSS::Selector::SimpleSelector const& si case CSS::Selector::SimpleSelector::Type::Universal: return matches_namespace(simple_selector.qualified_name(), element, context.style_sheet_for_rule); case CSS::Selector::SimpleSelector::Type::TagName: - // https://html.spec.whatwg.org/#case-sensitivity-of-selectors + // https://html.spec.whatwg.org/multipage/semantics-other.html#case-sensitivity-of-selectors // When comparing a CSS element type selector to the names of HTML elements in HTML documents, the CSS element type selector must first be converted to ASCII lowercase. The // same selector when compared to other elements must be compared according to its original case. In both cases, to match the values must be identical to each other (and therefore // the comparison is case sensitive). diff --git a/Libraries/LibWeb/HTML/Focus.cpp b/Libraries/LibWeb/HTML/Focus.cpp index d55645a217e..79dd1fd8c44 100644 --- a/Libraries/LibWeb/HTML/Focus.cpp +++ b/Libraries/LibWeb/HTML/Focus.cpp @@ -40,6 +40,8 @@ static void fire_a_focus_event(GC::Ptr focus_event_target, GC: // https://html.spec.whatwg.org/multipage/interaction.html#focus-update-steps static void run_focus_update_steps(Vector> old_chain, Vector> new_chain, DOM::Node* new_focus_target) { + // The focus update steps, given an old chain, a new chain, and a new focus target respectively, are as follows: + // 1. If the last entry in old chain and the last entry in new chain are the same, // pop the last entry from old chain and the last entry from new chain and redo this step. while (!old_chain.is_empty() @@ -51,18 +53,21 @@ static void run_focus_update_steps(Vector> old_chain, Vector // 2. For each entry entry in old chain, in order, run these substeps: for (auto& entry : old_chain) { - // 1. If entry is an input element, and the change event applies to the element, and the element does not have - // a defined activation behavior, and the user has changed the element's value or its list of selected files - // while the control was focused without committing that change (such that it is different to what it was - // when the control was first focused), then fire an event named change at the element, with the bubbles - // attribute initialized to true. + // 1. If entry is an input element if (is(*entry)) { auto& input_element = static_cast(*entry); // FIXME: Spec issue: It doesn't make sense to check if the element has a defined activation behavior, as // that is always true. Instead, we check if it has an *input* activation behavior. // https://github.com/whatwg/html/issues/9973 - if (input_element.change_event_applies() && !input_element.has_input_activation_behavior()) { + // and the change event applies to the element, and the element does not have a defined activation behavior, and the user has changed the + // element's value or its list of selected files while the control was focused without committing that change (such that it is different to what it was when the control was first + // focused), then: + if (input_element.change_event_applies() && !input_element.has_input_activation_behavior() && input_element.has_uncommitted_changes()) { + // 1. Set entry's user validity to true. + input_element.set_user_validity(true); + + // 2. Fire an event named change at the element, with the bubbles attribute initialized to true. input_element.commit_pending_changes(); } } diff --git a/Libraries/LibWeb/HTML/FormAssociatedElement.cpp b/Libraries/LibWeb/HTML/FormAssociatedElement.cpp index 170e86204e0..eaa322edf80 100644 --- a/Libraries/LibWeb/HTML/FormAssociatedElement.cpp +++ b/Libraries/LibWeb/HTML/FormAssociatedElement.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -52,13 +53,13 @@ bool FormAssociatedElement::enabled() const // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-fe-disabled auto const& html_element = form_associated_element_to_html_element(); - // A form control is disabled if any of the following conditions are met: - // 1. The element is a button, input, select, textarea, or form-associated custom element, and the disabled attribute is specified on this element (regardless of its value). + // A form control is disabled if any of the following are true: + // - The element is a button, input, select, textarea, or form-associated custom element, and the disabled attribute is specified on this element (regardless of its value); or // FIXME: This doesn't check for form-associated custom elements. if ((is(html_element) || is(html_element) || is(html_element) || is(html_element)) && html_element.has_attribute(HTML::AttributeNames::disabled)) return false; - // 2. The element is a descendant of a fieldset element whose disabled attribute is specified, and is not a descendant of that fieldset element's first legend element child, if any. + // - The element is a descendant of a fieldset element whose disabled attribute is specified, and is not a descendant of that fieldset element's first legend element child, if any. for (auto* fieldset_ancestor = html_element.first_ancestor_of_type(); fieldset_ancestor; fieldset_ancestor = fieldset_ancestor->first_ancestor_of_type()) { if (fieldset_ancestor->has_attribute(HTML::AttributeNames::disabled)) { auto* first_legend_element_child = fieldset_ancestor->first_child_of_type(); @@ -202,6 +203,98 @@ WebIDL::ExceptionOr FormAssociatedElement::set_form_action(String const& v return html_element.set_attribute(HTML::AttributeNames::formaction, value); } +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#candidate-for-constraint-validation +bool FormAssociatedElement::is_candidate_for_constraint_validation() const +{ + // A submittable element is a candidate for constraint validation except when a condition has barred the element from constraint validation. + if (!is_submittable()) + return false; + + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#enabling-and-disabling-form-controls%3A-the-disabled-attribute%3Abarred-from-constraint-validation + // If an element is disabled, it is barred from constraint validation. + if (!enabled()) + return false; + + auto const& html_element = form_associated_element_to_html_element(); + + // https://html.spec.whatwg.org/multipage/form-elements.html#the-datalist-element%3Abarred-from-constraint-validation + // If an element has a datalist element ancestor, it is barred from constraint validation. + if (html_element.first_ancestor_of_type()) + return false; + + if (is(html_element)) { + auto const& input_element = as(html_element); + + // https://html.spec.whatwg.org/multipage/input.html#hidden-state-(type%3Dhidden)%3Abarred-from-constraint-validation + // If an input element's type attribute is in the Hidden state, it is barred from constraint validation. + if (input_element.type_state() == HTMLInputElement::TypeAttributeState::Hidden) + return false; + + // https://html.spec.whatwg.org/multipage/input.html#reset-button-state-(type%3Dreset)%3Abarred-from-constraint-validation + // When an input element's type attribute is in the Reset Button state, the rules in this section apply. + // The element is barred from constraint validation. + if (input_element.type_state() == HTMLInputElement::TypeAttributeState::ResetButton) + return false; + + // https://html.spec.whatwg.org/multipage/input.html#button-state-(type%3Dbutton)%3Abarred-from-constraint-validation + // When an input element's type attribute is in the Button state, the rules in this section apply. + // The element is barred from constraint validation. + if (input_element.type_state() == HTMLInputElement::TypeAttributeState::Button) + return false; + + // https://html.spec.whatwg.org/multipage/input.html#the-readonly-attribute%3Abarred-from-constraint-validation + // If the readonly attribute is specified on an input element, the element is barred from constraint validation. + if (input_element.has_attribute(HTML::AttributeNames::readonly)) + return false; + } + + if (is(html_element)) { + auto const& button_element = as(html_element); + + // https://html.spec.whatwg.org/multipage/form-elements.html#the-button-element%3Abarred-from-constraint-validation + // If the type attribute is in the Reset Button state or the Button state, the element is barred from constraint validation. + if (button_element.type_state() == HTMLButtonElement::TypeAttributeState::Button || button_element.type_state() == HTMLButtonElement::TypeAttributeState::Reset) + return false; + } + + if (is(html_element)) { + // https://html.spec.whatwg.org/multipage/form-elements.html#the-textarea-element%3Abarred-from-constraint-validation + // If the readonly attribute is specified on a textarea element, the element is barred from constraint validation. + if (html_element.has_attribute(HTML::AttributeNames::readonly)) + return false; + } + + return true; +} + +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-fv-valid +bool FormAssociatedElement::satisfies_its_constraints() const +{ + return !( + suffering_from_being_missing() || suffering_from_a_type_mismatch() || suffering_from_a_pattern_mismatch() || suffering_from_being_too_long() || suffering_from_being_too_short() || suffering_from_an_underflow() || suffering_from_an_overflow() || suffering_from_a_step_mismatch() || suffering_from_bad_input() || suffering_from_a_custom_error()); +} + +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#limiting-user-input-length%3A-the-maxlength-attribute%3Asuffering-from-being-too-long +bool FormAssociatedElement::suffering_from_being_too_long() const +{ + // FIXME: Implement this. + return false; +} + +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#setting-minimum-input-length-requirements%3A-the-minlength-attribute%3Asuffering-from-being-too-short +bool FormAssociatedElement::suffering_from_being_too_short() const +{ + // FIXME: Implement this. + return false; +} + +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#suffering-from-a-custom-error +bool FormAssociatedElement::suffering_from_a_custom_error() const +{ + // FIXME: Implement this. + return false; +} + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value void FormAssociatedTextControlElement::relevant_value_was_changed() { diff --git a/Libraries/LibWeb/HTML/FormAssociatedElement.h b/Libraries/LibWeb/HTML/FormAssociatedElement.h index 7173e10d46d..5b17111a06f 100644 --- a/Libraries/LibWeb/HTML/FormAssociatedElement.h +++ b/Libraries/LibWeb/HTML/FormAssociatedElement.h @@ -91,6 +91,24 @@ public: // https://html.spec.whatwg.org/multipage/forms.html#concept-submit-button virtual bool is_submit_button() const { return false; } + // https://html.spec.whatwg.org/#candidate-for-constraint-validation + bool is_candidate_for_constraint_validation() const; + + // https://html.spec.whatwg.org/#concept-fv-valid + bool satisfies_its_constraints() const; + + // https://html.spec.whatwg.org/#definitions + virtual bool suffering_from_being_missing() const { return false; } + virtual bool suffering_from_a_type_mismatch() const { return false; } + virtual bool suffering_from_a_pattern_mismatch() const { return false; } + bool suffering_from_being_too_long() const; + bool suffering_from_being_too_short() const; + virtual bool suffering_from_an_underflow() const { return false; } + virtual bool suffering_from_an_overflow() const { return false; } + virtual bool suffering_from_a_step_mismatch() const { return false; } + virtual bool suffering_from_bad_input() const { return false; } + bool suffering_from_a_custom_error() const; + virtual String value() const { return String {}; } virtual HTMLElement& form_associated_element_to_html_element() = 0; diff --git a/Libraries/LibWeb/HTML/HTMLFormElement.cpp b/Libraries/LibWeb/HTML/HTMLFormElement.cpp index fae2052a923..8b6db2f250a 100644 --- a/Libraries/LibWeb/HTML/HTMLFormElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLFormElement.cpp @@ -125,18 +125,31 @@ WebIDL::ExceptionOr HTMLFormElement::submit_form(GC::Ref subm // 2. Set form's firing submission events to true. m_firing_submission_events = true; - // FIXME: 3. If the submitter element's no-validate state is false, then interactively validate the constraints + // 3. For each element field in the list of submittable elements whose form owner is form, set field's user validity to true. + for (auto& element : get_submittable_elements()) { + // NOTE: Only input, select and textarea elements have a user validity flag. + // See https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#user-validity + if (is(*element)) { + (&as(*element))->set_user_validity(false); + } else if (is(*element)) { + (&as(*element))->set_user_validity(false); + } else if (is(*element)) { + (&as(*element))->set_user_validity(false); + } + } + + // FIXME: 4. If the submitter element's no-validate state is false, then interactively validate the constraints // of form and examine the result. If the result is negative (i.e., the constraint validation concluded // that there were invalid fields and probably informed the user of this), then: // 1. Set form's firing submission events to false. // 2. Return. - // 4. Let submitterButton be null if submitter is form. Otherwise, let submitterButton be submitter. + // 5. Let submitterButton be null if submitter is form. Otherwise, let submitterButton be submitter. GC::Ptr submitter_button; if (submitter != this) submitter_button = submitter; - // 5. Let shouldContinue be the result of firing an event named submit at form using SubmitEvent, with the + // 6. Let shouldContinue be the result of firing an event named submit at form using SubmitEvent, with the // submitter attribute initialized to submitterButton, the bubbles attribute initialized to true, and the // cancelable attribute initialized to true. SubmitEventInit event_init {}; @@ -146,14 +159,14 @@ WebIDL::ExceptionOr HTMLFormElement::submit_form(GC::Ref subm submit_event->set_cancelable(true); bool should_continue = dispatch_event(*submit_event); - // 6. Set form's firing submission events to false. + // 7. Set form's firing submission events to false. m_firing_submission_events = false; - // 7. If shouldContinue is false, then return. + // 8. If shouldContinue is false, then return. if (!should_continue) return {}; - // 8. If form cannot navigate, then return. + // 9. If form cannot navigate, then return. // Spec Note: Cannot navigate is run again as dispatching the submit event could have changed the outcome. if (cannot_navigate()) return {}; diff --git a/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Libraries/LibWeb/HTML/HTMLInputElement.cpp index ffedd98e8f5..c80701faa69 100644 --- a/Libraries/LibWeb/HTML/HTMLInputElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLInputElement.cpp @@ -468,7 +468,8 @@ void HTMLInputElement::did_pick_color(Optional picked_color, ColorPickerU if (state == ColorPickerUpdateState::Closed) { queue_an_element_task(HTML::Task::Source::UserInteraction, [this] { // given the input element - // FIXME: to set its user interacted to true + // to set its user validity to true + m_user_validity = true; // and fire an event named change at the input element, with the bubbles attribute initialized to true. auto change_event = DOM::Event::create(realm(), HTML::EventNames::change); change_event->set_bubbles(true); @@ -1214,15 +1215,16 @@ void HTMLInputElement::user_interaction_did_change_input_value() // For input elements without a defined input activation behavior, but to which these events apply, // and for which the user interface involves both interactive manipulation and an explicit commit action, // then when the user changes the element's value, the user agent must queue an element task on the user interaction task source - // given the input element to fire an event named input at the input element, with the bubbles and composed attributes initialized to true, - // and any time the user commits the change, the user agent must queue an element task on the user interaction task source given the input - // element to set its user validity to true and fire an event named change at the input element, with the bubbles attribute initialized to true. + // given the input element to fire an event named input at the input element, with the bubbles and composed attributes initialized to true queue_an_element_task(HTML::Task::Source::UserInteraction, [this] { auto input_event = DOM::Event::create(realm(), HTML::EventNames::input); input_event->set_bubbles(true); input_event->set_composed(true); dispatch_event(*input_event); }); + // and any time the user commits the change, the user agent must queue an element task on the user interaction task source given the input + // element to set its user validity to true and fire an event named change at the input element, with the bubbles attribute initialized to true. + // FIXME: Does this need to happen here? } void HTMLInputElement::update_slider_shadow_tree_elements() @@ -1583,7 +1585,8 @@ String HTMLInputElement::value_sanitization_algorithm(String const& value) const // https://html.spec.whatwg.org/multipage/input.html#the-input-element:concept-form-reset-control void HTMLInputElement::reset_algorithm() { - // The reset algorithm for input elements is to set the dirty value flag and dirty checkedness flag back to false, + // The reset algorithm for input elements is to set its user validity, dirty value flag, and dirty checkedness flag back to false, + m_user_validity = false; m_dirty_value = false; m_dirty_checkedness = false; @@ -2765,4 +2768,169 @@ bool HTMLInputElement::is_focusable() const return m_type != TypeAttributeState::Hidden && enabled(); } +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#suffering-from-being-missing +bool HTMLInputElement::suffering_from_being_missing() const +{ + switch (type_state()) { + case TypeAttributeState::Checkbox: + // https://html.spec.whatwg.org/multipage/input.html#checkbox-state-(type%3Dcheckbox)%3Asuffering-from-being-missing + // If the element is required and its checkedness is false, then the element is suffering from being missing. + if (has_attribute(HTML::AttributeNames::required) && !checked()) + return true; + break; + case TypeAttributeState::RadioButton: + // https://html.spec.whatwg.org/multipage/input.html#radio-button-state-(type%3Dradio)%3Asuffering-from-being-missing + // If an element in the radio button group is required, and all of the input elements in the radio button group have a checkedness that is false, then the element + // is suffering from being missing. + // FIXME: Implement this. + break; + case TypeAttributeState::FileUpload: + // https://html.spec.whatwg.org/multipage/input.html#file-upload-state-(type%3Dfile)%3Asuffering-from-being-missing + // If the element is required and the list of selected files is empty, then the element is suffering from being missing. + // FIXME: Implement this. + break; + default: + break; + } + + // https://html.spec.whatwg.org/multipage/input.html#the-required-attribute%3Asuffering-from-being-missing + // If the element is required, and its value IDL attribute applies and is in the mode value, and the element is mutable, and the element's value is the empty + // string, then the element is suffering from being missing. + if (has_attribute(HTML::AttributeNames::required) && value_attribute_mode() == ValueAttributeMode::Value && is_mutable() && m_value.is_empty()) + return true; + + return false; +} + +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#suffering-from-a-type-mismatch +bool HTMLInputElement::suffering_from_a_type_mismatch() const +{ + switch (type_state()) { + case TypeAttributeState::URL: + // https://html.spec.whatwg.org/multipage/input.html#url-state-(type%3Durl)%3Asuffering-from-a-type-mismatch + // While the value of the element is neither the empty string nor a valid absolute URL, the element is suffering from a type mismatch. + // FIXME: Implement this. + break; + case TypeAttributeState::Email: + // https://html.spec.whatwg.org/multipage/input.html#email-state-(type%3Demail)%3Asuffering-from-a-type-mismatch + // While the value of the element is neither the empty string nor a single valid email address, the element is suffering from a type mismatch. + // FIXME: Implement this. + break; + default: + break; + } + return false; +} + +// https://html.spec.whatwg.org/multipage/input.html#the-pattern-attribute%3Asuffering-from-a-pattern-mismatch +bool HTMLInputElement::suffering_from_a_pattern_mismatch() const +{ + // If the element's value is not the empty string, and either the element's multiple attribute is not specified or it does not apply to the input element given its + // type attribute's current state, and the element has a compiled pattern regular expression but that regular expression does not match the element's value, then the element is + // suffering from a pattern mismatch. + // FIXME: Implement this. + return false; +} + +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#suffering-from-an-underflow +bool HTMLInputElement::suffering_from_an_underflow() const +{ + // https://html.spec.whatwg.org/multipage/input.html#the-min-and-max-attributes%3Asuffering-from-an-underflow-2 + // When the element has a minimum and does not have a reversed range, and the result of applying the algorithm to convert a string to a number to the string + // given by the element's value is a number, and the number obtained from that algorithm is less than the minimum, the element is suffering from an underflow. + // FIXME: Implement this. + + // https://html.spec.whatwg.org/multipage/input.html#the-min-and-max-attributes%3Asuffering-from-an-underflow-3 + // When an element has a reversed range, and the result of applying the algorithm to convert a string to a number to the string given by the element's value is a + // number, and the number obtained from that algorithm is more than the maximum and less than the minimum, the element is simultaneously suffering from an underflow and + // suffering from an overflow. + // FIXME: Implement this. + return false; +} + +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#suffering-from-an-overflow +bool HTMLInputElement::suffering_from_an_overflow() const +{ + // https://html.spec.whatwg.org/multipage/input.html#the-min-and-max-attributes%3Asuffering-from-an-overflow-2 + // When the element has a maximum and does not have a reversed range, and the result of applying the algorithm to convert a string to a number to the string + // given by the element's value is a number, and the number obtained from that algorithm is more than the maximum, the element is suffering from an overflow. + + // https://html.spec.whatwg.org/multipage/input.html#the-min-and-max-attributes%3Asuffering-from-an-underflow-3 + // When an element has a reversed range, and the result of applying the algorithm to convert a string to a number to the string given by the element's value is a + // number, and the number obtained from that algorithm is more than the maximum and less than the minimum, the element is simultaneously suffering from an underflow and + // suffering from an overflow. + // FIXME: Implement this. + return false; +} + +// https://html.spec.whatwg.org/multipage/input.html#the-step-attribute%3Asuffering-from-a-step-mismatch +bool HTMLInputElement::suffering_from_a_step_mismatch() const +{ + // When the element has an allowed value step, and the result of applying the algorithm to convert a string to a number to the string given by the element's + // value is a number, and that number subtracted from the step base is not an integral multiple of the allowed value step, the element is suffering from a step mismatch. + // FIXME: Implement this. + return false; +} + +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#suffering-from-bad-input +bool HTMLInputElement::suffering_from_bad_input() const +{ + switch (type_state()) { + case TypeAttributeState::Email: + // https://html.spec.whatwg.org/multipage/input.html#email-state-(type%3Demail)%3Asuffering-from-bad-input + // While the user interface is representing input that the user agent cannot convert to punycode, the control is suffering from bad input. + // FIXME: Implement this. + + // https://html.spec.whatwg.org/multipage/input.html#email-state-(type%3Demail)%3Asuffering-from-bad-input-2 + // While the user interface describes a situation where an individual value contains a U+002C COMMA (,) or is representing input that the user agent + // cannot convert to punycode, the control is suffering from bad input. + // FIXME: Implement this. + break; + case TypeAttributeState::Date: + // https://html.spec.whatwg.org/multipage/input.html#date-state-(type%3Ddate)%3Asuffering-from-bad-input + // While the user interface describes input that the user agent cannot convert to a valid date string, the control is suffering from bad input. + // FIXME: Implement this. + break; + case TypeAttributeState::Month: + // https://html.spec.whatwg.org/multipage/input.html#month-state-(type%3Dmonth)%3Asuffering-from-bad-input + // While the user interface describes input that the user agent cannot convert to a valid month string, the control is suffering from bad input. + // FIXME: Implement this. + break; + case TypeAttributeState::Week: + // https://html.spec.whatwg.org/multipage/input.html#week-state-(type%3Dweek)%3Asuffering-from-bad-input + // While the user interface describes input that the user agent cannot convert to a valid week string, the control is suffering from bad input. + // FIXME: Implement this. + break; + case TypeAttributeState::Time: + // https://html.spec.whatwg.org/multipage/#time-state-(type=time):suffering-from-bad-input + // While the user interface describes input that the user agent cannot convert to a valid time string, the control is suffering from bad input. + // FIXME: Implement this. + break; + case TypeAttributeState::LocalDateAndTime: + // https://html.spec.whatwg.org/multipage/input.html#local-date-and-time-state-(type%3Ddatetime-local)%3Asuffering-from-bad-input + // While the user interface describes input that the user agent cannot convert to a valid normalized local date and time string, the control is suffering from bad + // input. + // FIXME: Implement this. + break; + case TypeAttributeState::Number: + // https://html.spec.whatwg.org/multipage/input.html#number-state-(type%3Dnumber)%3Asuffering-from-bad-input + // While the user interface describes input that the user agent cannot convert to a valid floating-point number, the control is suffering from bad input. + // FIXME: Implement this. + break; + case TypeAttributeState::Range: + // https://html.spec.whatwg.org/multipage/input.html#range-state-(type%3Drange)%3Asuffering-from-bad-input + // While the user interface describes input that the user agent cannot convert to a valid floating-point number, the control is suffering from bad input. + // FIXME: Implement this. + break; + case TypeAttributeState::Color: + // https://html.spec.whatwg.org/multipage/input.html#color-state-(type%3Dcolor)%3Asuffering-from-bad-input + // While the element's value is not the empty string and parsing it returns failure, the control is suffering from bad input. + // FIXME: Implement this. + break; + default: + break; + } + return false; +} + } diff --git a/Libraries/LibWeb/HTML/HTMLInputElement.h b/Libraries/LibWeb/HTML/HTMLInputElement.h index e2178a63830..36df2fa706c 100644 --- a/Libraries/LibWeb/HTML/HTMLInputElement.h +++ b/Libraries/LibWeb/HTML/HTMLInputElement.h @@ -84,7 +84,11 @@ public: virtual void set_dirty_value_flag(bool flag) override { m_dirty_value = flag; } + bool user_validity() const { return m_user_validity; } + void set_user_validity(bool flag) { m_user_validity = flag; } + void commit_pending_changes(); + bool has_uncommitted_changes() { return m_has_uncommitted_changes; } String placeholder() const; Optional placeholder_value() const; @@ -220,6 +224,15 @@ public: virtual void did_edit_text_node() override; virtual GC::Ptr form_associated_element_to_text_node() override { return m_text_node; } + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#definitions + virtual bool suffering_from_being_missing() const override; + virtual bool suffering_from_a_type_mismatch() const override; + virtual bool suffering_from_a_pattern_mismatch() const override; + virtual bool suffering_from_an_underflow() const override; + virtual bool suffering_from_an_overflow() const override; + virtual bool suffering_from_a_step_mismatch() const override; + virtual bool suffering_from_bad_input() const override; + private: HTMLInputElement(DOM::Document&, DOM::QualifiedName); @@ -338,6 +351,9 @@ private: // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-fe-dirty bool m_dirty_value { false }; + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#user-validity + bool m_user_validity { false }; + // https://html.spec.whatwg.org/multipage/input.html#the-input-element:legacy-pre-activation-behavior bool m_before_legacy_pre_activation_behavior_checked { false }; bool m_before_legacy_pre_activation_behavior_indeterminate { false }; diff --git a/Libraries/LibWeb/HTML/HTMLSelectElement.cpp b/Libraries/LibWeb/HTML/HTMLSelectElement.cpp index 8fa27dc6677..f46a3304c66 100644 --- a/Libraries/LibWeb/HTML/HTMLSelectElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLSelectElement.cpp @@ -241,16 +241,21 @@ Vector> HTMLSelectElement::list_of_options() const void HTMLSelectElement::reset_algorithm() { update_cached_list_of_options(); + // The reset algorithm for a select element selectElement is: - // The reset algorithm for select elements is to go through all the option elements in the element's list of options, + // 1. Set selectElement's user validity to false. + m_user_validity = false; + + // 2. For each optionElement of selectElement's list of options: for (auto const& option_element : m_cached_list_of_options) { - // set their selectedness to true if the option element has a selected attribute, and false otherwise, + // 1. If optionElement has a selected attribute, then set optionElement's selectedness to true; otherwise set it to false. option_element->set_selected_internal(option_element->has_attribute(AttributeNames::selected)); - // set their dirtiness to false, + // 2. Set optionElement's dirtiness to false. option_element->m_dirty = false; - // and then have the option elements ask for a reset. - option_element->ask_for_a_reset(); } + + // 3. Run the selectedness setting algorithm given selectElement. + update_selectedness(); } // https://html.spec.whatwg.org/multipage/form-elements.html#dom-select-selectedindex @@ -379,7 +384,8 @@ void HTMLSelectElement::queue_input_and_change_events() { // When the user agent is to send select update notifications, queue an element task on the user interaction task source given the select element to run these steps: queue_an_element_task(HTML::Task::Source::UserInteraction, [this] { - // FIXME: 1. Set the select element's user interacted to true. + // 1. Set the select element's user validity to true. + m_user_validity = true; // 2. Fire an event named input at the select element, with the bubbles and composed attributes initialized to true. auto input_event = DOM::Event::create(realm(), HTML::EventNames::input); @@ -691,4 +697,28 @@ bool HTMLSelectElement::is_focusable() const return enabled(); } +// https://html.spec.whatwg.org/multipage/form-elements.html#placeholder-label-option +HTMLOptionElement* HTMLSelectElement::placeholder_label_option() const +{ + // If a select element has a required attribute specified, does not have a multiple attribute specified, and has a display size of 1; + if (has_attribute(HTML::AttributeNames::required) && !has_attribute(HTML::AttributeNames::multiple) && display_size() == 1) { + // and if the value of the first option element in the + // select element's list of options (if any) is the empty string, and that option element's parent node is the select element (and not an optgroup element), then that option is the + // select element's placeholder label option. + auto first_option_element = list_of_options()[0]; + if (first_option_element->value().is_empty() && first_option_element->parent() == this) + return first_option_element; + } + return {}; +} + +// https://html.spec.whatwg.org/multipage/form-elements.html#the-select-element%3Asuffering-from-being-missing +bool HTMLSelectElement::suffering_from_being_missing() const +{ + // If the element has its required attribute specified, and either none of the option elements in the select element's list of options have their selectedness + // set to true, or the only option element in the select element's list of options with its selectedness set to true is the placeholder label option, then the element is suffering from being + // missing. + return has_attribute(HTML::AttributeNames::required) && (m_selected_options->length() == 0 || (m_selected_options->length() == 1 && m_selected_options->item(0) == placeholder_label_option())); +} + } diff --git a/Libraries/LibWeb/HTML/HTMLSelectElement.h b/Libraries/LibWeb/HTML/HTMLSelectElement.h index 5390fa402f1..b34925578d4 100644 --- a/Libraries/LibWeb/HTML/HTMLSelectElement.h +++ b/Libraries/LibWeb/HTML/HTMLSelectElement.h @@ -100,6 +100,15 @@ public: bool can_skip_selectedness_update_for_inserted_option(HTMLOptionElement const&) const; + bool user_validity() const { return m_user_validity; } + void set_user_validity(bool flag) { m_user_validity = flag; } + + // https://html.spec.whatwg.org/multipage/form-elements.html#placeholder-label-option + HTMLOptionElement* placeholder_label_option() const; + + // https://html.spec.whatwg.org/multipage/form-elements.html#the-select-element%3Asuffering-from-being-missing + virtual bool suffering_from_being_missing() const override; + private: HTMLSelectElement(DOM::Document&, DOM::QualifiedName); @@ -132,6 +141,9 @@ private: Vector m_select_items; GC::Ptr m_inner_text_element; GC::Ptr m_chevron_icon_element; + + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#user-validity + bool m_user_validity { false }; }; } diff --git a/Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp b/Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp index d21f23f05d3..88c10a63358 100644 --- a/Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp @@ -110,7 +110,8 @@ i32 HTMLTextAreaElement::default_tab_index_value() const // https://html.spec.whatwg.org/multipage/form-elements.html#the-textarea-element:concept-form-reset-control void HTMLTextAreaElement::reset_algorithm() { - // The reset algorithm for textarea elements is to set the dirty value flag back to false, + // The reset algorithm for textarea elements is to set the user validity to false, dirty value flag back to false, + m_user_validity = false; m_dirty_value = false; // and set the raw value of element to its child text content. set_raw_value(child_text_content()); @@ -486,4 +487,12 @@ bool HTMLTextAreaElement::is_focusable() const return enabled(); } +// https://html.spec.whatwg.org/multipage/form-elements.html#the-textarea-element%3Asuffering-from-being-missing +bool HTMLTextAreaElement::suffering_from_being_missing() const +{ + // If the element has its required attribute specified, and the element is mutable, and the element's value is the empty string, then the element is suffering from + // being missing. + return has_attribute(HTML::AttributeNames::required) && is_mutable() && value().is_empty(); +} + } diff --git a/Libraries/LibWeb/HTML/HTMLTextAreaElement.h b/Libraries/LibWeb/HTML/HTMLTextAreaElement.h index ca45cd7d24a..a98b075724f 100644 --- a/Libraries/LibWeb/HTML/HTMLTextAreaElement.h +++ b/Libraries/LibWeb/HTML/HTMLTextAreaElement.h @@ -90,6 +90,9 @@ public: virtual void set_dirty_value_flag(bool flag) override { m_dirty_value = flag; } + bool user_validity() const { return m_user_validity; } + void set_user_validity(bool flag) { m_user_validity = flag; } + u32 text_length() const; bool check_validity(); @@ -126,6 +129,9 @@ public: virtual void did_edit_text_node() override; virtual GC::Ptr form_associated_element_to_text_node() override { return m_text_node; } + // https://html.spec.whatwg.org/multipage/form-elements.html#the-textarea-element%3Asuffering-from-being-missing + virtual bool suffering_from_being_missing() const override; + private: HTMLTextAreaElement(DOM::Document&, DOM::QualifiedName); @@ -157,6 +163,9 @@ private: // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-fe-dirty bool m_dirty_value { false }; + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#user-validity + bool m_user_validity { false }; + // https://html.spec.whatwg.org/multipage/form-elements.html#concept-textarea-raw-value String m_raw_value; diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/semantics/selectors/pseudo-classes/valid-invalid.txt b/Tests/LibWeb/Text/expected/wpt-import/html/semantics/selectors/pseudo-classes/valid-invalid.txt new file mode 100644 index 00000000000..e0a9239feee --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/html/semantics/selectors/pseudo-classes/valid-invalid.txt @@ -0,0 +1,36 @@ +Harness status: OK + +Found 30 tests + +18 Pass +12 Fail +Pass ':valid' matches elements that satisfy their constraints +Pass ':valid' matches form elements that are not the form owner of any elements that themselves are candidates for constraint validation but do not satisfy their constraints +Pass ':valid' matches fieldset elements that have no descendant elements that themselves are candidates for constraint validation but do not satisfy their constraints +Fail ':valid' matches elements that satisfy their pattern constraints +Fail ':valid' matches elements that satisfy their number constraints +Pass ':invalid' matches elements that do not satisfy their simple text constraints +Pass ':invalid' matches form elements that are the form owner of one or more elements that themselves are candidates for constraint validation but do not satisfy their constraints +Pass ':invalid' matches fieldset elements that have of one or more descendant elements that themselves are candidates for constraint validation but do not satisfy their constraints +Fail ':invalid' matches elements that do not satisfy their pattern constraints +Fail ':invalid' matches elements that do not satisfy their number constraints +Pass ':valid' matches new elements that satisfy their constraints +Pass ':invalid' doesn't match new elements that satisfy their constraints +Fail ':valid' doesn't match new elements that do not satisfy their constraints +Fail ':invalid' matches new elements that do not satisfy their constraints +Pass :valid/:invalid styling for
+Pass empty form correctly styled on page-load +Pass valid form correctly styled on page-load +Fail invalid form correctly styled on page-load +Pass programmatically adding valid to empty form results in correct style +Fail programmatically adding invalid to empty form results in correct style +Fail programmatically-invalidated form correctly styled +Pass programmatically-validated form correctly styled +Pass :valid/:invalid styling for
+Pass empty fieldset correctly styled on page-load +Pass valid fieldset correctly styled on page-load +Fail invalid fieldset correctly styled on page-load +Pass programmatically adding valid to empty fieldset results in correct style +Fail programmatically adding invalid to empty fieldset results in correct style +Fail programmatically-invalidated fieldset correctly styled +Pass programmatically-validated fieldset correctly styled \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/html/semantics/selectors/pseudo-classes/utils.js b/Tests/LibWeb/Text/input/wpt-import/html/semantics/selectors/pseudo-classes/utils.js new file mode 100644 index 00000000000..7a2fb77f105 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/semantics/selectors/pseudo-classes/utils.js @@ -0,0 +1,20 @@ +function getElementsByIds(ids) { + var result = []; + ids.forEach(function(id) { + result.push(document.getElementById(id)); + }); + return result; +} + +function testSelectorIdsMatch(selector, ids, testName) { + test(function(){ + var elements = document.querySelectorAll(selector); + assert_array_equals([...elements], getElementsByIds(ids)); + }, testName); +} + +function testSelectorElementsMatch(selector, elements, testName) { + test(function(){ + assert_array_equals([...document.querySelectorAll(selector)], elements); + }, testName); +} diff --git a/Tests/LibWeb/Text/input/wpt-import/html/semantics/selectors/pseudo-classes/valid-invalid.html b/Tests/LibWeb/Text/input/wpt-import/html/semantics/selectors/pseudo-classes/valid-invalid.html new file mode 100644 index 00000000000..882d6c9ef28 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/semantics/selectors/pseudo-classes/valid-invalid.html @@ -0,0 +1,146 @@ + + + + +Selector: pseudo-classes (:valid, :invalid) + + + + + + + + +
+
+ + +
+
+ + + +
+ +
+
+
+
+ +
+
+ +
+
+
+ + +
+
+ + +
+
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+
+ + +