From 5f2c6c09cdbb1ec554d8a240c25d3867a3668a64 Mon Sep 17 00:00:00 2001 From: John Poge II <62263315+tipowol@users.noreply.github.com> Date: Fri, 21 Jul 2023 12:42:03 +0200 Subject: [PATCH] Implement :valid :invalid pseudo classes (#26729) Signed-off-by: Martin Robinson --- components/script/dom/element.rs | 2 + components/script/dom/htmlbuttonelement.rs | 8 +- components/script/dom/htmlfieldsetelement.rs | 22 +++- components/script/dom/htmlformelement.rs | 69 +++++++--- components/script/dom/htmlinputelement.rs | 37 ++++-- components/script/dom/htmloptgroupelement.rs | 41 +++++- components/script/dom/htmloptionelement.rs | 23 ++++ components/script/dom/htmlselectelement.rs | 12 +- components/script/dom/htmltextareaelement.rs | 44 +++++-- components/script/dom/validation.rs | 20 +-- components/script/dom/validitystate.rs | 121 +++++++++++++----- components/script/layout_dom/element.rs | 2 + components/style/servo/selector_parser.rs | 8 ++ .../dom/nodes/Element-closest.html.ini | 5 - .../infinite_backtracking.html.ini | 1 + ...-validity-dynamic-value-no-change.html.ini | 4 - .../input-pattern-dynamic-value.html.ini | 4 - .../constraints/radio-valueMissing.html.ini | 3 + ...mit-iframe-then-location-navigate.html.ini | 4 - .../text-plain.window.js.ini | 3 - .../form-requestsubmit.html.ini | 6 - .../pattern_attribute.html.ini | 12 -- .../forms/the-input-element/radio.html.ini | 3 + .../output-validity.html.ini | 3 - .../select-validity.html.ini | 3 + ...lid-invalid-fieldset-disconnected.html.ini | 6 - .../pseudo-classes/valid-invalid.html.ini | 87 ------------- .../input-pattern-dynamic-value.html.ini | 4 - .../form-double-submit-2.html.ini | 4 - .../form-double-submit.html.ini | 4 - 30 files changed, 324 insertions(+), 241 deletions(-) delete mode 100644 tests/wpt/meta-legacy-layout/html/semantics/forms/constraints/input-number-validity-dynamic-value-no-change.html.ini delete mode 100644 tests/wpt/meta-legacy-layout/html/semantics/forms/constraints/input-pattern-dynamic-value.html.ini create mode 100644 tests/wpt/meta-legacy-layout/html/semantics/forms/constraints/radio-valueMissing.html.ini delete mode 100644 tests/wpt/meta-legacy-layout/html/semantics/forms/form-submission-0/form-submit-iframe-then-location-navigate.html.ini delete mode 100644 tests/wpt/meta-legacy-layout/html/semantics/forms/the-form-element/form-requestsubmit.html.ini create mode 100644 tests/wpt/meta-legacy-layout/html/semantics/forms/the-input-element/radio.html.ini delete mode 100644 tests/wpt/meta-legacy-layout/html/semantics/forms/the-output-element/output-validity.html.ini create mode 100644 tests/wpt/meta-legacy-layout/html/semantics/forms/the-select-element/select-validity.html.ini delete mode 100644 tests/wpt/meta-legacy-layout/html/semantics/selectors/pseudo-classes/valid-invalid-fieldset-disconnected.html.ini delete mode 100644 tests/wpt/meta-legacy-layout/html/semantics/selectors/pseudo-classes/valid-invalid.html.ini delete mode 100644 tests/wpt/meta/html/semantics/forms/constraints/input-pattern-dynamic-value.html.ini delete mode 100644 tests/wpt/meta/html/semantics/forms/form-submission-0/form-double-submit-2.html.ini delete mode 100644 tests/wpt/meta/html/semantics/forms/form-submission-0/form-double-submit.html.ini diff --git a/components/script/dom/element.rs b/components/script/dom/element.rs index 2e94e635b26..7408147893f 100644 --- a/components/script/dom/element.rs +++ b/components/script/dom/element.rs @@ -3248,6 +3248,8 @@ impl<'a> SelectorsElement for DomRoot { NonTSPseudoClass::Enabled | NonTSPseudoClass::Disabled | NonTSPseudoClass::Checked | + NonTSPseudoClass::Valid | + NonTSPseudoClass::Invalid | NonTSPseudoClass::Indeterminate | NonTSPseudoClass::ReadWrite | NonTSPseudoClass::PlaceholderShown | diff --git a/components/script/dom/htmlbuttonelement.rs b/components/script/dom/htmlbuttonelement.rs index 6b22c542681..14f5d6abcaf 100755 --- a/components/script/dom/htmlbuttonelement.rs +++ b/components/script/dom/htmlbuttonelement.rs @@ -20,7 +20,7 @@ use crate::dom::htmlformelement::{FormSubmitter, ResetFrom, SubmittedFrom}; use crate::dom::node::{window_from_node, BindContext, Node, UnbindContext}; use crate::dom::nodelist::NodeList; use crate::dom::validation::{is_barred_by_datalist_ancestor, Validatable}; -use crate::dom::validitystate::ValidityState; +use crate::dom::validitystate::{ValidationFlags, ValidityState}; use crate::dom::virtualmethods::VirtualMethods; use dom_struct::dom_struct; use html5ever::{LocalName, Prefix}; @@ -242,6 +242,8 @@ impl VirtualMethods for HTMLButtonElement { }, } el.update_sequentially_focusable_status(); + self.validity_state() + .perform_validation_and_update(ValidationFlags::all()); }, &local_name!("type") => match mutation { AttributeMutation::Set(_) => { @@ -251,6 +253,8 @@ impl VirtualMethods for HTMLButtonElement { _ => ButtonType::Submit, }; self.button_type.set(value); + self.validity_state() + .perform_validation_and_update(ValidationFlags::all()); }, AttributeMutation::Removed => { self.button_type.set(ButtonType::Submit); @@ -258,6 +262,8 @@ impl VirtualMethods for HTMLButtonElement { }, &local_name!("form") => { self.form_attribute_mutated(mutation); + self.validity_state() + .perform_validation_and_update(ValidationFlags::empty()); }, _ => {}, } diff --git a/components/script/dom/htmlfieldsetelement.rs b/components/script/dom/htmlfieldsetelement.rs index f6629b5860b..1b7f325425a 100644 --- a/components/script/dom/htmlfieldsetelement.rs +++ b/components/script/dom/htmlfieldsetelement.rs @@ -38,7 +38,7 @@ impl HTMLFieldSetElement { ) -> HTMLFieldSetElement { HTMLFieldSetElement { htmlelement: HTMLElement::new_inherited_with_state( - ElementState::IN_ENABLED_STATE, + ElementState::IN_ENABLED_STATE | ElementState::IN_VALID_STATE, local_name, prefix, document, @@ -63,6 +63,26 @@ impl HTMLFieldSetElement { proto, ) } + + pub fn update_validity(&self) { + let has_invalid_child = self + .upcast::() + .traverse_preorder(ShadowIncluding::No) + .flat_map(DomRoot::downcast::) + .any(|element| { + if let Some(validatable) = element.as_maybe_validatable() { + validatable.is_instance_validatable() && + !validatable.validity_state().invalid_flags().is_empty() + } else { + false + } + }); + + self.upcast::() + .set_state(ElementState::IN_VALID_STATE, !has_invalid_child); + self.upcast::() + .set_state(ElementState::IN_INVALID_STATE, has_invalid_child); + } } impl HTMLFieldSetElementMethods for HTMLFieldSetElement { diff --git a/components/script/dom/htmlformelement.rs b/components/script/dom/htmlformelement.rs index 5d8d83d6783..7594026046c 100644 --- a/components/script/dom/htmlformelement.rs +++ b/components/script/dom/htmlformelement.rs @@ -72,6 +72,7 @@ use servo_rand::random; use std::borrow::ToOwned; use std::cell::Cell; use style::attr::AttrValue; +use style::element_state::ElementState; use style::str::split_html_space_chars; use crate::dom::bindings::codegen::UnionTypes::RadioNodeListOrElement; @@ -104,7 +105,12 @@ impl HTMLFormElement { document: &Document, ) -> HTMLFormElement { HTMLFormElement { - htmlelement: HTMLElement::new_inherited(local_name, prefix, document), + htmlelement: HTMLElement::new_inherited_with_state( + ElementState::IN_VALID_STATE, + local_name, + prefix, + document, + ), marked_for_reset: Cell::new(false), constructing_entry_list: Cell::new(false), elements: Default::default(), @@ -674,6 +680,23 @@ impl HTMLFormElement { result } + pub fn update_validity(&self) { + let controls = self.controls.borrow(); + + let is_any_invalid = controls + .iter() + .filter_map(|control| control.as_maybe_validatable()) + .any(|validatable| { + validatable.is_instance_validatable() && + !validatable.validity_state().invalid_flags().is_empty() + }); + + self.upcast::() + .set_state(ElementState::IN_VALID_STATE, !is_any_invalid); + self.upcast::() + .set_state(ElementState::IN_INVALID_STATE, is_any_invalid); + } + /// [Form submission](https://html.spec.whatwg.org/multipage/#concept-form-submit) pub fn submit(&self, submit_method_flag: SubmittedFrom, submitter: FormSubmitter) { // Step 1 @@ -1034,9 +1057,12 @@ impl HTMLFormElement { Some(v) => v, None => return None, }; + validatable + .validity_state() + .perform_validation_and_update(ValidationFlags::all()); if !validatable.is_instance_validatable() { None - } else if validatable.validate(ValidationFlags::all()).is_empty() { + } else if validatable.validity_state().invalid_flags().is_empty() { None } else { Some(DomRoot::from_ref(el)) @@ -1287,27 +1313,32 @@ impl HTMLFormElement { } fn add_control(&self, control: &T) { - let root = self.upcast::().root_element(); - let root = root.upcast::(); - - let mut controls = self.controls.borrow_mut(); - controls.insert_pre_order(control.to_element(), root); + { + let root = self.upcast::().root_element(); + let root = root.upcast::(); + let mut controls = self.controls.borrow_mut(); + controls.insert_pre_order(control.to_element(), root); + } + self.update_validity(); } fn remove_control(&self, control: &T) { - let control = control.to_element(); - let mut controls = self.controls.borrow_mut(); - controls - .iter() - .position(|c| &**c == control) - .map(|idx| controls.remove(idx)); + { + let control = control.to_element(); + let mut controls = self.controls.borrow_mut(); + controls + .iter() + .position(|c| &**c == control) + .map(|idx| controls.remove(idx)); - // https://html.spec.whatwg.org/multipage#forms.html#the-form-element:past-names-map-5 - // "If an element listed in a form element's past names map - // changes form owner, then its entries must be removed - // from that map." - let mut past_names_map = self.past_names_map.borrow_mut(); - past_names_map.retain(|_k, v| v.0 != control); + // https://html.spec.whatwg.org/multipage#forms.html#the-form-element:past-names-map-5 + // "If an element listed in a form element's past names map + // changes form owner, then its entries must be removed + // from that map." + let mut past_names_map = self.past_names_map.borrow_mut(); + past_names_map.retain(|_k, v| v.0 != control); + } + self.update_validity(); } } diff --git a/components/script/dom/htmlinputelement.rs b/components/script/dom/htmlinputelement.rs index 046f90dbad8..f0e16612c90 100755 --- a/components/script/dom/htmlinputelement.rs +++ b/components/script/dom/htmlinputelement.rs @@ -1296,6 +1296,8 @@ impl HTMLInputElementMethods for HTMLInputElement { }, } + self.validity_state() + .perform_validation_and_update(ValidationFlags::all()); self.upcast::().dirty(NodeDamage::OtherNodeDamage); Ok(()) } @@ -2340,7 +2342,6 @@ impl VirtualMethods for HTMLInputElement { } let new_value_mode = self.value_mode(); - match (&old_value_mode, old_idl_value.is_empty(), new_value_mode) { // Step 1 (&ValueMode::Value, false, ValueMode::Default) | @@ -2452,15 +2453,17 @@ impl VirtualMethods for HTMLInputElement { } self.update_placeholder_shown_state(); }, - &local_name!("readonly") if self.input_type().is_textual() => { - let el = self.upcast::(); - match mutation { - AttributeMutation::Set(_) => { - el.set_read_write_state(false); - }, - AttributeMutation::Removed => { - el.set_read_write_state(!el.disabled_state()); - }, + &local_name!("readonly") => { + if self.input_type().is_textual() { + let el = self.upcast::(); + match mutation { + AttributeMutation::Set(_) => { + el.set_read_write_state(false); + }, + AttributeMutation::Removed => { + el.set_read_write_state(!el.disabled_state()); + }, + } } }, &local_name!("form") => { @@ -2468,6 +2471,9 @@ impl VirtualMethods for HTMLInputElement { }, _ => {}, } + + self.validity_state() + .perform_validation_and_update(ValidationFlags::all()); } fn parse_plain_attribute(&self, name: &LocalName, value: DOMString) -> AttrValue { @@ -2494,6 +2500,9 @@ impl VirtualMethods for HTMLInputElement { } self.upcast::() .check_ancestors_disabled_state_for_form_control(); + + self.validity_state() + .perform_validation_and_update(ValidationFlags::all()); } fn unbind_from_tree(&self, context: &UnbindContext) { @@ -2509,6 +2518,9 @@ impl VirtualMethods for HTMLInputElement { } else { el.check_disabled_attribute(); } + + self.validity_state() + .perform_validation_and_update(ValidationFlags::all()); } // This represents behavior for which the UIEvents spec and the @@ -2608,6 +2620,9 @@ impl VirtualMethods for HTMLInputElement { event.mark_as_handled(); } } + + self.validity_state() + .perform_validation_and_update(ValidationFlags::all()); } // https://html.spec.whatwg.org/multipage/#the-input-element%3Aconcept-node-clone-ext @@ -2628,6 +2643,8 @@ impl VirtualMethods for HTMLInputElement { elem.textinput .borrow_mut() .set_content(self.textinput.borrow().get_content()); + elem.validity_state() + .perform_validation_and_update(ValidationFlags::all()); } } diff --git a/components/script/dom/htmloptgroupelement.rs b/components/script/dom/htmloptgroupelement.rs index 03e379055f6..0f737bcd521 100644 --- a/components/script/dom/htmloptgroupelement.rs +++ b/components/script/dom/htmloptgroupelement.rs @@ -10,7 +10,10 @@ use crate::dom::document::Document; use crate::dom::element::{AttributeMutation, Element}; use crate::dom::htmlelement::HTMLElement; use crate::dom::htmloptionelement::HTMLOptionElement; -use crate::dom::node::Node; +use crate::dom::htmlselectelement::HTMLSelectElement; +use crate::dom::node::{BindContext, Node, ShadowIncluding, UnbindContext}; +use crate::dom::validation::Validatable; +use crate::dom::validitystate::ValidationFlags; use crate::dom::virtualmethods::VirtualMethods; use dom_struct::dom_struct; use html5ever::{LocalName, Prefix}; @@ -53,6 +56,19 @@ impl HTMLOptGroupElement { proto, ) } + + fn update_select_validity(&self) { + if let Some(select) = self + .upcast::() + .ancestors() + .filter_map(DomRoot::downcast::) + .next() + { + select + .validity_state() + .perform_validation_and_update(ValidationFlags::all()); + } + } } impl HTMLOptGroupElementMethods for HTMLOptGroupElement { @@ -104,4 +120,27 @@ impl VirtualMethods for HTMLOptGroupElement { _ => {}, } } + + fn bind_to_tree(&self, context: &BindContext) { + if let Some(ref s) = self.super_type() { + s.bind_to_tree(context); + } + + self.update_select_validity(); + } + + fn unbind_from_tree(&self, context: &UnbindContext) { + self.super_type().unwrap().unbind_from_tree(context); + + if let Some(select) = context + .parent + .inclusive_ancestors(ShadowIncluding::No) + .filter_map(DomRoot::downcast::) + .next() + { + select + .validity_state() + .perform_validation_and_update(ValidationFlags::all()); + } + } } diff --git a/components/script/dom/htmloptionelement.rs b/components/script/dom/htmloptionelement.rs index 8e22d8edbaf..d11b221f0df 100644 --- a/components/script/dom/htmloptionelement.rs +++ b/components/script/dom/htmloptionelement.rs @@ -22,6 +22,8 @@ use crate::dom::htmlscriptelement::HTMLScriptElement; use crate::dom::htmlselectelement::HTMLSelectElement; use crate::dom::node::{BindContext, Node, ShadowIncluding, UnbindContext}; use crate::dom::text::Text; +use crate::dom::validation::Validatable; +use crate::dom::validitystate::ValidationFlags; use crate::dom::virtualmethods::VirtualMethods; use crate::dom::window::Window; use dom_struct::dom_struct; @@ -108,6 +110,7 @@ impl HTMLOptionElement { option.SetDefaultSelected(default_selected); option.set_selectedness(selected); + option.update_select_validity(); Ok(option) } @@ -167,6 +170,19 @@ impl HTMLOptionElement { }, } } + + fn update_select_validity(&self) { + if let Some(select) = self + .upcast::() + .ancestors() + .filter_map(DomRoot::downcast::) + .next() + { + select + .validity_state() + .perform_validation_and_update(ValidationFlags::all()); + } + } } // FIXME(ajeffrey): Provide a way of buffering DOMStrings other than using Strings @@ -264,6 +280,7 @@ impl HTMLOptionElementMethods for HTMLOptionElement { self.dirtiness.set(true); self.selectedness.set(selected); self.pick_if_selected_and_reset(); + self.update_select_validity(); } // https://html.spec.whatwg.org/multipage/#dom-option-index @@ -293,6 +310,7 @@ impl VirtualMethods for HTMLOptionElement { el.check_parent_disabled_state_for_option(); }, } + self.update_select_validity(); }, &local_name!("selected") => { match mutation { @@ -309,6 +327,7 @@ impl VirtualMethods for HTMLOptionElement { } }, } + self.update_select_validity(); }, _ => {}, } @@ -323,6 +342,7 @@ impl VirtualMethods for HTMLOptionElement { .check_parent_disabled_state_for_option(); self.pick_if_selected_and_reset(); + self.update_select_validity(); } fn unbind_from_tree(&self, context: &UnbindContext) { @@ -334,6 +354,9 @@ impl VirtualMethods for HTMLOptionElement { .filter_map(DomRoot::downcast::) .next() { + select + .validity_state() + .perform_validation_and_update(ValidationFlags::all()); select.ask_for_reset(); } diff --git a/components/script/dom/htmlselectelement.rs b/components/script/dom/htmlselectelement.rs index 00bd8b76f91..0cfda5bfaa6 100755 --- a/components/script/dom/htmlselectelement.rs +++ b/components/script/dom/htmlselectelement.rs @@ -76,7 +76,7 @@ impl HTMLSelectElement { ) -> HTMLSelectElement { HTMLSelectElement { htmlelement: HTMLElement::new_inherited_with_state( - ElementState::IN_ENABLED_STATE, + ElementState::IN_ENABLED_STATE | ElementState::IN_VALID_STATE, local_name, prefix, document, @@ -347,6 +347,9 @@ impl HTMLSelectElementMethods for HTMLSelectElement { for opt in opt_iter { opt.set_selectedness(false); } + + self.validity_state() + .perform_validation_and_update(ValidationFlags::VALUE_MISSING); } // https://html.spec.whatwg.org/multipage/#dom-select-selectedindex @@ -414,6 +417,10 @@ impl VirtualMethods for HTMLSelectElement { fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation) { self.super_type().unwrap().attribute_mutated(attr, mutation); match attr.local_name() { + &local_name!("required") => { + self.validity_state() + .perform_validation_and_update(ValidationFlags::VALUE_MISSING); + }, &local_name!("disabled") => { let el = self.upcast::(); match mutation { @@ -427,6 +434,9 @@ impl VirtualMethods for HTMLSelectElement { el.check_ancestors_disabled_state_for_form_control(); }, } + + self.validity_state() + .perform_validation_and_update(ValidationFlags::VALUE_MISSING); }, &local_name!("form") => { self.form_attribute_mutated(mutation); diff --git a/components/script/dom/htmltextareaelement.rs b/components/script/dom/htmltextareaelement.rs index f3656664239..db21db49b18 100755 --- a/components/script/dom/htmltextareaelement.rs +++ b/components/script/dom/htmltextareaelement.rs @@ -323,22 +323,26 @@ impl HTMLTextAreaElementMethods for HTMLTextAreaElement { // https://html.spec.whatwg.org/multipage/#dom-textarea-value fn SetValue(&self, value: DOMString) { - let mut textinput = self.textinput.borrow_mut(); + { + let mut textinput = self.textinput.borrow_mut(); - // Step 1 - let old_value = textinput.get_content(); + // Step 1 + let old_value = textinput.get_content(); - // Step 2 - textinput.set_content(value); + // Step 2 + textinput.set_content(value); - // Step 3 - self.value_dirty.set(true); + // Step 3 + self.value_dirty.set(true); - if old_value != textinput.get_content() { - // Step 4 - textinput.clear_selection_to_limit(Direction::Forward); + if old_value != textinput.get_content() { + // Step 4 + textinput.clear_selection_to_limit(Direction::Forward); + } } + self.validity_state() + .perform_validation_and_update(ValidationFlags::all()); self.upcast::().dirty(NodeDamage::OtherNodeDamage); } @@ -533,6 +537,9 @@ impl VirtualMethods for HTMLTextAreaElement { }, _ => {}, } + + self.validity_state() + .perform_validation_and_update(ValidationFlags::all()); } fn bind_to_tree(&self, context: &BindContext) { @@ -542,6 +549,9 @@ impl VirtualMethods for HTMLTextAreaElement { self.upcast::() .check_ancestors_disabled_state_for_form_control(); + + self.validity_state() + .perform_validation_and_update(ValidationFlags::all()); } fn parse_plain_attribute(&self, name: &LocalName, value: DOMString) -> AttrValue { @@ -574,6 +584,9 @@ impl VirtualMethods for HTMLTextAreaElement { } else { el.check_disabled_attribute(); } + + self.validity_state() + .perform_validation_and_update(ValidationFlags::all()); } // The cloning steps for textarea elements must propagate the raw value @@ -589,8 +602,12 @@ impl VirtualMethods for HTMLTextAreaElement { } let el = copy.downcast::().unwrap(); el.value_dirty.set(self.value_dirty.get()); - let mut textinput = el.textinput.borrow_mut(); - textinput.set_content(self.textinput.borrow().get_content()); + { + let mut textinput = el.textinput.borrow_mut(); + textinput.set_content(self.textinput.borrow().get_content()); + } + el.validity_state() + .perform_validation_and_update(ValidationFlags::all()); } fn children_changed(&self, mutation: &ChildrenMutation) { @@ -661,6 +678,9 @@ impl VirtualMethods for HTMLTextAreaElement { event.mark_as_handled(); } } + + self.validity_state() + .perform_validation_and_update(ValidationFlags::all()); } fn pop(&self) { diff --git a/components/script/dom/validation.rs b/components/script/dom/validation.rs index e6b36ec809d..a96d06894b4 100755 --- a/components/script/dom/validation.rs +++ b/components/script/dom/validation.rs @@ -28,23 +28,9 @@ pub trait Validatable { ValidationFlags::empty() } - // https://html.spec.whatwg.org/multipage/#concept-fv-valid - fn validate(&self, validate_flags: ValidationFlags) -> ValidationFlags { - let mut failed_flags = self.perform_validation(validate_flags); - - // https://html.spec.whatwg.org/multipage/#suffering-from-a-custom-error - if validate_flags.contains(ValidationFlags::CUSTOM_ERROR) { - if !self.validity_state().custom_error_message().is_empty() { - failed_flags.insert(ValidationFlags::CUSTOM_ERROR); - } - } - - failed_flags - } - // https://html.spec.whatwg.org/multipage/#check-validity-steps fn check_validity(&self) -> bool { - if self.is_instance_validatable() && !self.validate(ValidationFlags::all()).is_empty() { + if self.is_instance_validatable() && !self.validity_state().invalid_flags().is_empty() { self.as_element() .upcast::() .fire_cancelable_event(atom!("invalid")); @@ -61,7 +47,7 @@ pub trait Validatable { return true; } - let flags = self.validate(ValidationFlags::all()); + let flags = self.validity_state().invalid_flags(); if flags.is_empty() { return true; } @@ -90,7 +76,7 @@ pub trait Validatable { // https://html.spec.whatwg.org/multipage/#dom-cva-validationmessage fn validation_message(&self) -> DOMString { if self.is_instance_validatable() { - let flags = self.validate(ValidationFlags::all()); + let flags = self.validity_state().invalid_flags(); validation_message_for_flags(&self.validity_state(), flags) } else { DOMString::new() diff --git a/components/script/dom/validitystate.rs b/components/script/dom/validitystate.rs index b7f4da79530..2af3843c203 100755 --- a/components/script/dom/validitystate.rs +++ b/components/script/dom/validitystate.rs @@ -4,17 +4,24 @@ use crate::dom::bindings::cell::{DomRefCell, Ref}; use crate::dom::bindings::codegen::Bindings::ValidityStateBinding::ValidityStateMethods; +use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::reflector::{reflect_dom_object, Reflector}; use crate::dom::bindings::root::{Dom, DomRoot}; use crate::dom::bindings::str::DOMString; use crate::dom::element::Element; +use crate::dom::htmlfieldsetelement::HTMLFieldSetElement; +use crate::dom::htmlformelement::FormControlElementHelpers; +use crate::dom::node::Node; use crate::dom::window::Window; use dom_struct::dom_struct; use itertools::Itertools; +use std::cell::Cell; use std::fmt; +use style::element_state::ElementState; // https://html.spec.whatwg.org/multipage/#validity-states bitflags! { + #[derive(JSTraceable, MallocSizeOf)] pub struct ValidationFlags: u32 { const VALUE_MISSING = 0b0000000001; const TYPE_MISMATCH = 0b0000000010; @@ -64,6 +71,7 @@ pub struct ValidityState { reflector_: Reflector, element: Dom, custom_error_message: DomRefCell, + invalid_flags: Cell, } impl ValidityState { @@ -72,6 +80,7 @@ impl ValidityState { reflector_: Reflector::new(), element: Dom::from_ref(element), custom_error_message: DomRefCell::new(DOMString::new()), + invalid_flags: Cell::new(ValidationFlags::empty()), } } @@ -87,84 +96,130 @@ impl ValidityState { // https://html.spec.whatwg.org/multipage/#custom-validity-error-message pub fn set_custom_error_message(&self, error: DOMString) { *self.custom_error_message.borrow_mut() = error; + self.perform_validation_and_update(ValidationFlags::CUSTOM_ERROR); + } + + /// Given a set of [ValidationFlags], recalculate their value by performing + /// validation on this [ValidityState]'s associated element. Additionally, + /// if [ValidationFlags::CUSTOM_ERROR] is in `update_flags` and a custom + /// error has been set on this [ValidityState], the state will be updated + /// to reflect the existance of a custom error. + pub fn perform_validation_and_update(&self, update_flags: ValidationFlags) { + let mut invalid_flags = self.invalid_flags.get(); + invalid_flags.remove(update_flags); + + if let Some(validatable) = self.element.as_maybe_validatable() { + let new_flags = validatable.perform_validation(update_flags); + invalid_flags.insert(new_flags); + } + + // https://html.spec.whatwg.org/multipage/#suffering-from-a-custom-error + if update_flags.contains(ValidationFlags::CUSTOM_ERROR) && + !self.custom_error_message().is_empty() + { + invalid_flags.insert(ValidationFlags::CUSTOM_ERROR); + } + + self.invalid_flags.set(invalid_flags); + self.update_pseudo_classes(); + } + + pub fn invalid_flags(&self) -> ValidationFlags { + self.invalid_flags.get() + } + + fn update_pseudo_classes(&self) { + if let Some(validatable) = self.element.as_maybe_validatable() { + if validatable.is_instance_validatable() { + let is_valid = self.invalid_flags.get().is_empty(); + self.element + .set_state(ElementState::IN_VALID_STATE, is_valid); + self.element + .set_state(ElementState::IN_INVALID_STATE, !is_valid); + } else { + self.element.set_state(ElementState::IN_VALID_STATE, false); + self.element + .set_state(ElementState::IN_INVALID_STATE, false); + } + } + + if let Some(form_control) = self.element.as_maybe_form_control() { + if let Some(form_owner) = form_control.form_owner() { + form_owner.update_validity(); + } + } + + if let Some(fieldset) = self + .element + .upcast::() + .ancestors() + .filter_map(DomRoot::downcast::) + .next() + { + fieldset.update_validity(); + } } } impl ValidityStateMethods for ValidityState { // https://html.spec.whatwg.org/multipage/#dom-validitystate-valuemissing fn ValueMissing(&self) -> bool { - self.element.as_maybe_validatable().map_or(false, |e| { - !e.validate(ValidationFlags::VALUE_MISSING).is_empty() - }) + self.invalid_flags() + .contains(ValidationFlags::VALUE_MISSING) } // https://html.spec.whatwg.org/multipage/#dom-validitystate-typemismatch fn TypeMismatch(&self) -> bool { - self.element.as_maybe_validatable().map_or(false, |e| { - !e.validate(ValidationFlags::TYPE_MISMATCH).is_empty() - }) + self.invalid_flags() + .contains(ValidationFlags::TYPE_MISMATCH) } // https://html.spec.whatwg.org/multipage/#dom-validitystate-patternmismatch fn PatternMismatch(&self) -> bool { - self.element.as_maybe_validatable().map_or(false, |e| { - !e.validate(ValidationFlags::PATTERN_MISMATCH).is_empty() - }) + self.invalid_flags() + .contains(ValidationFlags::PATTERN_MISMATCH) } // https://html.spec.whatwg.org/multipage/#dom-validitystate-toolong fn TooLong(&self) -> bool { - self.element - .as_maybe_validatable() - .map_or(false, |e| !e.validate(ValidationFlags::TOO_LONG).is_empty()) + self.invalid_flags().contains(ValidationFlags::TOO_LONG) } // https://html.spec.whatwg.org/multipage/#dom-validitystate-tooshort fn TooShort(&self) -> bool { - self.element.as_maybe_validatable().map_or(false, |e| { - !e.validate(ValidationFlags::TOO_SHORT).is_empty() - }) + self.invalid_flags().contains(ValidationFlags::TOO_SHORT) } // https://html.spec.whatwg.org/multipage/#dom-validitystate-rangeunderflow fn RangeUnderflow(&self) -> bool { - self.element.as_maybe_validatable().map_or(false, |e| { - !e.validate(ValidationFlags::RANGE_UNDERFLOW).is_empty() - }) + self.invalid_flags() + .contains(ValidationFlags::RANGE_UNDERFLOW) } // https://html.spec.whatwg.org/multipage/#dom-validitystate-rangeoverflow fn RangeOverflow(&self) -> bool { - self.element.as_maybe_validatable().map_or(false, |e| { - !e.validate(ValidationFlags::RANGE_OVERFLOW).is_empty() - }) + self.invalid_flags() + .contains(ValidationFlags::RANGE_OVERFLOW) } // https://html.spec.whatwg.org/multipage/#dom-validitystate-stepmismatch fn StepMismatch(&self) -> bool { - self.element.as_maybe_validatable().map_or(false, |e| { - !e.validate(ValidationFlags::STEP_MISMATCH).is_empty() - }) + self.invalid_flags() + .contains(ValidationFlags::STEP_MISMATCH) } // https://html.spec.whatwg.org/multipage/#dom-validitystate-badinput fn BadInput(&self) -> bool { - self.element.as_maybe_validatable().map_or(false, |e| { - !e.validate(ValidationFlags::BAD_INPUT).is_empty() - }) + self.invalid_flags().contains(ValidationFlags::BAD_INPUT) } // https://html.spec.whatwg.org/multipage/#dom-validitystate-customerror fn CustomError(&self) -> bool { - self.element.as_maybe_validatable().map_or(false, |e| { - !e.validate(ValidationFlags::CUSTOM_ERROR).is_empty() - }) + self.invalid_flags().contains(ValidationFlags::CUSTOM_ERROR) } // https://html.spec.whatwg.org/multipage/#dom-validitystate-valid fn Valid(&self) -> bool { - self.element - .as_maybe_validatable() - .map_or(true, |e| e.validate(ValidationFlags::all()).is_empty()) + self.invalid_flags().is_empty() } } diff --git a/components/script/layout_dom/element.rs b/components/script/layout_dom/element.rs index ebd54df0c31..b85e72443c3 100644 --- a/components/script/layout_dom/element.rs +++ b/components/script/layout_dom/element.rs @@ -611,6 +611,8 @@ impl<'dom, LayoutDataType: LayoutDataTrait> ::selectors::Element NonTSPseudoClass::Enabled | NonTSPseudoClass::Disabled | NonTSPseudoClass::Checked | + NonTSPseudoClass::Valid | + NonTSPseudoClass::Invalid | NonTSPseudoClass::Indeterminate | NonTSPseudoClass::ReadWrite | NonTSPseudoClass::PlaceholderShown | diff --git a/components/style/servo/selector_parser.rs b/components/style/servo/selector_parser.rs index 7d5b8c5772a..82c6c1cda2f 100644 --- a/components/style/servo/selector_parser.rs +++ b/components/style/servo/selector_parser.rs @@ -281,6 +281,8 @@ pub enum NonTSPseudoClass { Active, AnyLink, Checked, + Valid, + Invalid, Defined, Disabled, Enabled, @@ -338,6 +340,8 @@ impl ToCss for NonTSPseudoClass { Active => ":active", AnyLink => ":any-link", Checked => ":checked", + Valid => ":valid", + Invalid => ":invalid", Defined => ":defined", Disabled => ":disabled", Enabled => ":enabled", @@ -371,6 +375,8 @@ impl NonTSPseudoClass { Enabled => ElementState::IN_ENABLED_STATE, Disabled => ElementState::IN_DISABLED_STATE, Checked => ElementState::IN_CHECKED_STATE, + Valid => ElementState::IN_VALID_STATE, + Invalid => ElementState::IN_INVALID_STATE, Indeterminate => ElementState::IN_INDETERMINATE_STATE, ReadOnly | ReadWrite => ElementState::IN_READWRITE_STATE, PlaceholderShown => ElementState::IN_PLACEHOLDER_SHOWN_STATE, @@ -425,6 +431,8 @@ impl<'a, 'i> ::selectors::Parser<'i> for SelectorParser<'a> { "active" => Active, "any-link" => AnyLink, "checked" => Checked, + "valid" => Valid, + "invalid" => Invalid, "defined" => Defined, "disabled" => Disabled, "enabled" => Enabled, diff --git a/tests/wpt/meta-legacy-layout/dom/nodes/Element-closest.html.ini b/tests/wpt/meta-legacy-layout/dom/nodes/Element-closest.html.ini index 73e8c643d68..a73d91f6ef4 100644 --- a/tests/wpt/meta-legacy-layout/dom/nodes/Element-closest.html.ini +++ b/tests/wpt/meta-legacy-layout/dom/nodes/Element-closest.html.ini @@ -1,9 +1,4 @@ [Element-closest.html] type: testharness - [Element.closest with context node 'test11' and selector ':invalid'] - bug: https://github.com/servo/servo/issues/10781 - expected: FAIL - [Element.closest with context node 'test4' and selector ':has(> :scope)'] expected: FAIL - diff --git a/tests/wpt/meta-legacy-layout/html/semantics/forms/constraints/infinite_backtracking.html.ini b/tests/wpt/meta-legacy-layout/html/semantics/forms/constraints/infinite_backtracking.html.ini index fd73c8d647c..f0776f9c17f 100644 --- a/tests/wpt/meta-legacy-layout/html/semantics/forms/constraints/infinite_backtracking.html.ini +++ b/tests/wpt/meta-legacy-layout/html/semantics/forms/constraints/infinite_backtracking.html.ini @@ -1,4 +1,5 @@ [infinite_backtracking.html] + expected: TIMEOUT [Infinite backtracking pattern terminates] expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/html/semantics/forms/constraints/input-number-validity-dynamic-value-no-change.html.ini b/tests/wpt/meta-legacy-layout/html/semantics/forms/constraints/input-number-validity-dynamic-value-no-change.html.ini deleted file mode 100644 index 849ad5c8b80..00000000000 --- a/tests/wpt/meta-legacy-layout/html/semantics/forms/constraints/input-number-validity-dynamic-value-no-change.html.ini +++ /dev/null @@ -1,4 +0,0 @@ -[input-number-validity-dynamic-value-no-change.html] - [number input number validation is updated correctly after value attribute change which doesn't change input value] - expected: FAIL - diff --git a/tests/wpt/meta-legacy-layout/html/semantics/forms/constraints/input-pattern-dynamic-value.html.ini b/tests/wpt/meta-legacy-layout/html/semantics/forms/constraints/input-pattern-dynamic-value.html.ini deleted file mode 100644 index 6d133b8ec69..00000000000 --- a/tests/wpt/meta-legacy-layout/html/semantics/forms/constraints/input-pattern-dynamic-value.html.ini +++ /dev/null @@ -1,4 +0,0 @@ -[input-pattern-dynamic-value.html] - [input validation is updated after pattern attribute change] - expected: FAIL - diff --git a/tests/wpt/meta-legacy-layout/html/semantics/forms/constraints/radio-valueMissing.html.ini b/tests/wpt/meta-legacy-layout/html/semantics/forms/constraints/radio-valueMissing.html.ini new file mode 100644 index 00000000000..056a42422db --- /dev/null +++ b/tests/wpt/meta-legacy-layout/html/semantics/forms/constraints/radio-valueMissing.html.ini @@ -0,0 +1,3 @@ +[radio-valueMissing.html] + [One of the radios is required and another one is checked] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/html/semantics/forms/form-submission-0/form-submit-iframe-then-location-navigate.html.ini b/tests/wpt/meta-legacy-layout/html/semantics/forms/form-submission-0/form-submit-iframe-then-location-navigate.html.ini deleted file mode 100644 index 74e9d711ff5..00000000000 --- a/tests/wpt/meta-legacy-layout/html/semantics/forms/form-submission-0/form-submit-iframe-then-location-navigate.html.ini +++ /dev/null @@ -1,4 +0,0 @@ -[form-submit-iframe-then-location-navigate.html] - expected: TIMEOUT - [Verifies that location navigations take precedence when following form submissions.] - expected: TIMEOUT diff --git a/tests/wpt/meta-legacy-layout/html/semantics/forms/form-submission-0/text-plain.window.js.ini b/tests/wpt/meta-legacy-layout/html/semantics/forms/form-submission-0/text-plain.window.js.ini index 2ed4e055249..b0956237833 100644 --- a/tests/wpt/meta-legacy-layout/html/semantics/forms/form-submission-0/text-plain.window.js.ini +++ b/tests/wpt/meta-legacy-layout/html/semantics/forms/form-submission-0/text-plain.window.js.ini @@ -5,9 +5,6 @@ [text/plain: Basic File test (normal form)] expected: FAIL - [text/plain: 0x00 in name (normal form)] - expected: FAIL - [text/plain: 0x00 in value (normal form)] expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/html/semantics/forms/the-form-element/form-requestsubmit.html.ini b/tests/wpt/meta-legacy-layout/html/semantics/forms/the-form-element/form-requestsubmit.html.ini deleted file mode 100644 index 4f05392b2e0..00000000000 --- a/tests/wpt/meta-legacy-layout/html/semantics/forms/the-form-element/form-requestsubmit.html.ini +++ /dev/null @@ -1,6 +0,0 @@ -[form-requestsubmit.html] - [The value of the submitter should be appended, and form* attributes of the submitter should be handled.] - expected: FAIL - - [requestSubmit() should trigger interactive form validation] - expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/html/semantics/forms/the-input-element/pattern_attribute.html.ini b/tests/wpt/meta-legacy-layout/html/semantics/forms/the-input-element/pattern_attribute.html.ini index 23bcf08fd46..55a105a7c68 100644 --- a/tests/wpt/meta-legacy-layout/html/semantics/forms/the-input-element/pattern_attribute.html.ini +++ b/tests/wpt/meta-legacy-layout/html/semantics/forms/the-input-element/pattern_attribute.html.ini @@ -2,18 +2,6 @@ [pattern attribute support on input element] expected: FAIL - [basic support] - expected: FAIL - - [ is Unicode code point-aware] - expected: FAIL - - [ supports Unicode property escape syntax] - expected: FAIL - - [ supports Unicode property escape syntax for properties of strings] - expected: FAIL - [ supports set difference syntax] expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/html/semantics/forms/the-input-element/radio.html.ini b/tests/wpt/meta-legacy-layout/html/semantics/forms/the-input-element/radio.html.ini new file mode 100644 index 00000000000..e6320078ac5 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/html/semantics/forms/the-input-element/radio.html.ini @@ -0,0 +1,3 @@ +[radio.html] + [Radio buttons in an orphan tree should make a group] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/html/semantics/forms/the-output-element/output-validity.html.ini b/tests/wpt/meta-legacy-layout/html/semantics/forms/the-output-element/output-validity.html.ini deleted file mode 100644 index 28d4f01369f..00000000000 --- a/tests/wpt/meta-legacy-layout/html/semantics/forms/the-output-element/output-validity.html.ini +++ /dev/null @@ -1,3 +0,0 @@ -[output-validity.html] - [:valid and :invalid pseudo-class on output element] - expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/html/semantics/forms/the-select-element/select-validity.html.ini b/tests/wpt/meta-legacy-layout/html/semantics/forms/the-select-element/select-validity.html.ini new file mode 100644 index 00000000000..348da577e86 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/html/semantics/forms/the-select-element/select-validity.html.ini @@ -0,0 +1,3 @@ +[select-validity.html] + [Remove and add back the placeholder label option] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/html/semantics/selectors/pseudo-classes/valid-invalid-fieldset-disconnected.html.ini b/tests/wpt/meta-legacy-layout/html/semantics/selectors/pseudo-classes/valid-invalid-fieldset-disconnected.html.ini deleted file mode 100644 index 56209882404..00000000000 --- a/tests/wpt/meta-legacy-layout/html/semantics/selectors/pseudo-classes/valid-invalid-fieldset-disconnected.html.ini +++ /dev/null @@ -1,6 +0,0 @@ -[valid-invalid-fieldset-disconnected.html] - [ element becomes invalid inside disconnected
] - expected: FAIL - - [