LibWeb: Implement CSS validity pseudo-classes

This commit is contained in:
Psychpsyo 2025-02-02 20:41:23 +01:00 committed by Sam Atkins
parent 39e17e83f9
commit bad7324307
Notes: github-actions[bot] 2025-02-05 12:52:56 +00:00
15 changed files with 745 additions and 29 deletions

View file

@ -53,6 +53,9 @@
"indeterminate": {
"argument": ""
},
"invalid": {
"argument": ""
},
"is": {
"argument": "<forgiving-selector-list>"
},
@ -137,6 +140,15 @@
"target-within": {
"argument": ""
},
"user-invalid": {
"argument": ""
},
"user-valid": {
"argument": ""
},
"valid": {
"argument": ""
},
"visited": {
"argument": ""
},

View file

@ -21,6 +21,7 @@
#include <LibWeb/HTML/HTMLDetailsElement.h>
#include <LibWeb/HTML/HTMLDialogElement.h>
#include <LibWeb/HTML/HTMLFieldSetElement.h>
#include <LibWeb/HTML/HTMLFormElement.h>
#include <LibWeb/HTML/HTMLHtmlElement.h>
#include <LibWeb/HTML/HTMLInputElement.h>
#include <LibWeb/HTML/HTMLMediaElement.h>
@ -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<HTML::HTMLElement>(element) && element.has_attribute(HTML::AttributeNames::popover)) {
auto& html_element = static_cast<HTML::HTMLElement const&>(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<Web::HTML::FormAssociatedElement>(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<Web::HTML::HTMLFormElement>(element)) {
bool has_invalid_elements = false;
element.for_each_in_subtree([&](auto& node) {
if (auto form_associated_element = as_if<Web::HTML::FormAssociatedElement>(&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<Web::HTML::HTMLFieldSetElement>(element)) {
bool has_invalid_children = false;
element.for_each_in_subtree([&](auto& node) {
if (auto form_associated_element = as_if<Web::HTML::FormAssociatedElement>(&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<Web::HTML::FormAssociatedElement>(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<Web::HTML::HTMLFormElement>(element)) {
bool has_invalid_elements = false;
element.for_each_in_subtree([&](auto& node) {
if (auto form_associated_element = as_if<Web::HTML::FormAssociatedElement>(&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<Web::HTML::HTMLFieldSetElement>(element)) {
bool has_invalid_children = false;
element.for_each_in_subtree([&](auto& node) {
if (auto form_associated_element = as_if<Web::HTML::FormAssociatedElement>(&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<Web::HTML::HTMLInputElement>(element)) {
user_validity = input_element->user_validity();
} else if (auto select_element = as_if<Web::HTML::HTMLSelectElement>(element)) {
user_validity = select_element->user_validity();
} else if (auto text_area_element = as_if<Web::HTML::HTMLTextAreaElement>(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<Web::HTML::FormAssociatedElement>(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<Web::HTML::HTMLInputElement>(element)) {
user_validity = input_element->user_validity();
} else if (auto select_element = as_if<Web::HTML::HTMLSelectElement>(element)) {
user_validity = select_element->user_validity();
} else if (auto text_area_element = as_if<Web::HTML::HTMLTextAreaElement>(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<Web::HTML::FormAssociatedElement>(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).

View file

@ -40,6 +40,8 @@ static void fire_a_focus_event(GC::Ptr<DOM::EventTarget> focus_event_target, GC:
// https://html.spec.whatwg.org/multipage/interaction.html#focus-update-steps
static void run_focus_update_steps(Vector<GC::Root<DOM::Node>> old_chain, Vector<GC::Root<DOM::Node>> 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<GC::Root<DOM::Node>> 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<HTMLInputElement>(*entry)) {
auto& input_element = static_cast<HTMLInputElement&>(*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();
}
}

View file

@ -15,6 +15,7 @@
#include <LibWeb/DOM/SelectionchangeEventDispatching.h>
#include <LibWeb/HTML/FormAssociatedElement.h>
#include <LibWeb/HTML/HTMLButtonElement.h>
#include <LibWeb/HTML/HTMLDataListElement.h>
#include <LibWeb/HTML/HTMLFieldSetElement.h>
#include <LibWeb/HTML/HTMLFormElement.h>
#include <LibWeb/HTML/HTMLInputElement.h>
@ -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<HTMLButtonElement>(html_element) || is<HTMLInputElement>(html_element) || is<HTMLSelectElement>(html_element) || is<HTMLTextAreaElement>(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<HTMLFieldSetElement>(); fieldset_ancestor; fieldset_ancestor = fieldset_ancestor->first_ancestor_of_type<HTMLFieldSetElement>()) {
if (fieldset_ancestor->has_attribute(HTML::AttributeNames::disabled)) {
auto* first_legend_element_child = fieldset_ancestor->first_child_of_type<HTMLLegendElement>();
@ -202,6 +203,98 @@ WebIDL::ExceptionOr<void> 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<HTMLDataListElement>())
return false;
if (is<HTMLInputElement>(html_element)) {
auto const& input_element = as<HTMLInputElement>(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<HTMLButtonElement>(html_element)) {
auto const& button_element = as<HTMLButtonElement>(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<HTMLTextAreaElement>(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()
{

View file

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

View file

@ -125,18 +125,31 @@ WebIDL::ExceptionOr<void> HTMLFormElement::submit_form(GC::Ref<HTMLElement> 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<HTMLInputElement>(*element)) {
(&as<HTMLInputElement>(*element))->set_user_validity(false);
} else if (is<HTMLSelectElement>(*element)) {
(&as<HTMLSelectElement>(*element))->set_user_validity(false);
} else if (is<HTMLTextAreaElement>(*element)) {
(&as<HTMLTextAreaElement>(*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<HTMLElement> 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<void> HTMLFormElement::submit_form(GC::Ref<HTMLElement> 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 {};

View file

@ -468,7 +468,8 @@ void HTMLInputElement::did_pick_color(Optional<Color> 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;
}
}

View file

@ -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<String> placeholder_value() const;
@ -220,6 +224,15 @@ public:
virtual void did_edit_text_node() override;
virtual GC::Ptr<DOM::Text> 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 };

View file

@ -241,16 +241,21 @@ Vector<GC::Root<HTMLOptionElement>> 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()));
}
}

View file

@ -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<SelectItem> m_select_items;
GC::Ptr<DOM::Element> m_inner_text_element;
GC::Ptr<DOM::Element> m_chevron_icon_element;
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#user-validity
bool m_user_validity { false };
};
}

View file

@ -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();
}
}

View file

@ -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<DOM::Text> 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;

View file

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

View file

@ -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);
}

View file

@ -0,0 +1,146 @@
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
<title>Selector: pseudo-classes (:valid, :invalid)</title>
<link rel="author" title="Denis Ah-Kang" href="mailto:denis@w3.org" id=link1>
<link rel=help href="https://html.spec.whatwg.org/multipage/#pseudo-classes" id=link2>
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<script src="utils.js"></script>
<style>
#styleTests form, #styleTests fieldset, #failExample { background-color:red; }
#styleTests > :valid, #validExample { background-color:green; }
#styleTests > :invalid, #invalidExample { background-color:lime; }
</style>
</head>
<body>
<div id="log"></div>
<div id='simpleConstraints'>
<input type=text id=text1 value="foobar" required>
<input type=text id=text2 required>
</div>
<div id='FormSelection'>
<form id=form1>
<input type=text id=text3 value="foobar" required>
</form>
<form id=form2>
<input type=text id=text4 required>
</form>
</div>
<div id='FieldSetSelection'>
<fieldset id=fieldset1>
<input type=text id=text5 value="foobar" required>
</fieldset>
<fieldset id=fieldset2>
<input type=text id=text6 required>
</fieldset>
</div>
<div id='patternConstraints'>
<input type=text id=text7 value="AAA" pattern="[0-9][A-Z]{3}">
<input type=text id=text8 value="0AAA" pattern="[0-9][A-Z]{3}">
</div>
<div id='numberConstraints'>
<input type=number id=number1 value=0 min=1>
<input type=number id=number2 value=1 min=1>
</div>
<div id='styleTests'>
<form>
</form>
<form>
<input type=text min=8 value=4>
</form>
<form>
<input type=number min=8 value=4>
</form>
<fieldset>
</fieldset>
<fieldset>
<input type=text min=8 value=4>
</fieldset>
<fieldset>
<input type=number min=8 value=4>
</fieldset>
<div id='validExample'></div>
<div id='invalidExample'></div>
<div id='failExample'></div>
</div>
<script>
testSelectorIdsMatch("#simpleConstraints :valid", ["text1"], "':valid' matches elements that satisfy their constraints");
testSelectorIdsMatch("#FormSelection :valid", ["form1", "text3"], "':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");
testSelectorIdsMatch("#FieldSetSelection :valid", ["fieldset1", "text5"], "':valid' matches fieldset elements that have no descendant elements that themselves are candidates for constraint validation but do not satisfy their constraints");
testSelectorIdsMatch("#patternConstraints :valid", [ "text8" ], "':valid' matches elements that satisfy their pattern constraints");
testSelectorIdsMatch("#numberConstraints :valid", [ "number2" ], "':valid' matches elements that satisfy their number constraints");
testSelectorIdsMatch("#simpleConstraints :invalid", ["text2"], "':invalid' matches elements that do not satisfy their simple text constraints");
testSelectorIdsMatch("#FormSelection :invalid", ["form2", "text4"], "':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");
testSelectorIdsMatch("#FieldSetSelection :invalid", ["fieldset2", "text6"], "':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");
testSelectorIdsMatch("#patternConstraints :invalid", ["text7"], "':invalid' matches elements that do not satisfy their pattern constraints");
testSelectorIdsMatch("#numberConstraints :invalid", ["number1"], "':invalid' matches elements that do not satisfy their number constraints");
document.getElementById("text7").value="0BBB";
testSelectorIdsMatch("#patternConstraints :valid", [ "text7", "text8" ], "':valid' matches new elements that satisfy their constraints");
testSelectorIdsMatch("#patternConstraints :invalid", [], "':invalid' doesn't match new elements that satisfy their constraints");
document.getElementById("text8").value="BBB";
testSelectorIdsMatch("#patternConstraints :valid", ["text7"], "':valid' doesn't match new elements that do not satisfy their constraints");
testSelectorIdsMatch("#patternConstraints :invalid", ["text8"], "':invalid' matches new elements that do not satisfy their constraints");
function getBGColor(elem) {
return getComputedStyle(elem).backgroundColor;
}
function testStyles(type) {
var elems = document.querySelectorAll("#styleTests " + type),
empty = elems[0],
valid = elems[1],
invalid = elems[2],
validInput = valid.querySelector("input"),
invalidInput = invalid.querySelector("input"),
expectedValidBGColor = getBGColor(document.getElementById("validExample")),
expectedInvalidBGColor = getBGColor(document.getElementById("invalidExample")),
expectedFailBGColor = getBGColor(document.getElementById("failExample"));
test(function() {
assert_equals(getBGColor(empty), expectedValidBGColor, "wrong background-color");
}, 'empty ' + type + ' correctly styled on page-load');
test(function() {
assert_equals(getBGColor(valid), expectedValidBGColor, "wrong background-color");
}, 'valid ' + type + ' correctly styled on page-load');
test(function() {
assert_equals(getBGColor(invalid), expectedInvalidBGColor, "wrong background-color");
}, 'invalid ' + type + ' correctly styled on page-load');
test(function() {
empty.appendChild(validInput.cloneNode());
assert_equals(getBGColor(empty), expectedValidBGColor, "wrong background-color");
}, 'programmatically adding valid to empty ' + type + ' results in correct style');
test(function() {
empty.appendChild(invalidInput.cloneNode());
assert_equals(getBGColor(empty), expectedInvalidBGColor, "wrong background-color");
}, 'programmatically adding invalid to empty ' + type + ' results in correct style');
validInput.type = "number";
invalidInput.type = "text";
test(function() {
assert_equals(getBGColor(valid), expectedInvalidBGColor, "wrong background-color");
}, 'programmatically-invalidated ' + type + ' correctly styled');
test(function() {
assert_equals(getBGColor(invalid), expectedValidBGColor, "wrong background-color");
}, 'programmatically-validated ' + type + ' correctly styled');
}
test(testStyles.bind(undefined, "form"), ":valid/:invalid styling for <form>");
test(testStyles.bind(undefined, "fieldset"), ":valid/:invalid styling for <fieldset>");
</script>
</body>
</html>