Implement :valid :invalid pseudo classes (#26729)

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
John Poge II 2023-07-21 12:42:03 +02:00 committed by GitHub
parent 2b67392fd5
commit 5f2c6c09cd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 324 additions and 241 deletions

View file

@ -3248,6 +3248,8 @@ impl<'a> SelectorsElement for DomRoot<Element> {
NonTSPseudoClass::Enabled |
NonTSPseudoClass::Disabled |
NonTSPseudoClass::Checked |
NonTSPseudoClass::Valid |
NonTSPseudoClass::Invalid |
NonTSPseudoClass::Indeterminate |
NonTSPseudoClass::ReadWrite |
NonTSPseudoClass::PlaceholderShown |

View file

@ -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());
},
_ => {},
}

View file

@ -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::<Node>()
.traverse_preorder(ShadowIncluding::No)
.flat_map(DomRoot::downcast::<Element>)
.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::<Element>()
.set_state(ElementState::IN_VALID_STATE, !has_invalid_child);
self.upcast::<Element>()
.set_state(ElementState::IN_INVALID_STATE, has_invalid_child);
}
}
impl HTMLFieldSetElementMethods for HTMLFieldSetElement {

View file

@ -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::<Element>()
.set_state(ElementState::IN_VALID_STATE, !is_any_invalid);
self.upcast::<Element>()
.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,14 +1313,17 @@ impl HTMLFormElement {
}
fn add_control<T: ?Sized + FormControl>(&self, control: &T) {
{
let root = self.upcast::<Element>().root_element();
let root = root.upcast::<Node>();
let mut controls = self.controls.borrow_mut();
controls.insert_pre_order(control.to_element(), root);
}
self.update_validity();
}
fn remove_control<T: ?Sized + FormControl>(&self, control: &T) {
{
let control = control.to_element();
let mut controls = self.controls.borrow_mut();
controls
@ -1309,6 +1338,8 @@ impl HTMLFormElement {
let mut past_names_map = self.past_names_map.borrow_mut();
past_names_map.retain(|_k, v| v.0 != control);
}
self.update_validity();
}
}
#[derive(Clone, JSTraceable, MallocSizeOf)]

View file

@ -1296,6 +1296,8 @@ impl HTMLInputElementMethods for HTMLInputElement {
},
}
self.validity_state()
.perform_validation_and_update(ValidationFlags::all());
self.upcast::<Node>().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,7 +2453,8 @@ impl VirtualMethods for HTMLInputElement {
}
self.update_placeholder_shown_state();
},
&local_name!("readonly") if self.input_type().is_textual() => {
&local_name!("readonly") => {
if self.input_type().is_textual() {
let el = self.upcast::<Element>();
match mutation {
AttributeMutation::Set(_) => {
@ -2462,12 +2464,16 @@ impl VirtualMethods for HTMLInputElement {
el.set_read_write_state(!el.disabled_state());
},
}
}
},
&local_name!("form") => {
self.form_attribute_mutated(mutation);
},
_ => {},
}
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::<Element>()
.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());
}
}

View file

@ -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::<Node>()
.ancestors()
.filter_map(DomRoot::downcast::<HTMLSelectElement>)
.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::<HTMLSelectElement>)
.next()
{
select
.validity_state()
.perform_validation_and_update(ValidationFlags::all());
}
}
}

View file

@ -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::<Node>()
.ancestors()
.filter_map(DomRoot::downcast::<HTMLSelectElement>)
.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::<HTMLSelectElement>)
.next()
{
select
.validity_state()
.perform_validation_and_update(ValidationFlags::all());
select.ask_for_reset();
}

View file

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

View file

@ -323,6 +323,7 @@ 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();
// Step 1
@ -338,7 +339,10 @@ impl HTMLTextAreaElementMethods for HTMLTextAreaElement {
// Step 4
textinput.clear_selection_to_limit(Direction::Forward);
}
}
self.validity_state()
.perform_validation_and_update(ValidationFlags::all());
self.upcast::<Node>().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::<Element>()
.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,9 +602,13 @@ impl VirtualMethods for HTMLTextAreaElement {
}
let el = copy.downcast::<HTMLTextAreaElement>().unwrap();
el.value_dirty.set(self.value_dirty.get());
{
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) {
if let Some(ref s) = self.super_type() {
@ -661,6 +678,9 @@ impl VirtualMethods for HTMLTextAreaElement {
event.mark_as_handled();
}
}
self.validity_state()
.perform_validation_and_update(ValidationFlags::all());
}
fn pop(&self) {

View file

@ -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::<EventTarget>()
.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()

View file

@ -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<Element>,
custom_error_message: DomRefCell<DOMString>,
invalid_flags: Cell<ValidationFlags>,
}
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::<Node>()
.ancestors()
.filter_map(DomRoot::downcast::<HTMLFieldSetElement>)
.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()
}
}

View file

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

View file

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

View file

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

View file

@ -1,4 +1,5 @@
[infinite_backtracking.html]
expected: TIMEOUT
[Infinite backtracking pattern terminates]
expected: FAIL

View file

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

View file

@ -1,4 +0,0 @@
[input-pattern-dynamic-value.html]
[input validation is updated after pattern attribute change]
expected: FAIL

View file

@ -0,0 +1,3 @@
[radio-valueMissing.html]
[One of the radios is required and another one is checked]
expected: FAIL

View file

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

View file

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

View file

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

View file

@ -2,18 +2,6 @@
[pattern attribute support on input element]
expected: FAIL
[basic <input pattern> support]
expected: FAIL
[<input pattern> is Unicode code point-aware]
expected: FAIL
[<input pattern> supports Unicode property escape syntax]
expected: FAIL
[<input pattern> supports Unicode property escape syntax for properties of strings]
expected: FAIL
[<input pattern> supports set difference syntax]
expected: FAIL

View file

@ -0,0 +1,3 @@
[radio.html]
[Radio buttons in an orphan tree should make a group]
expected: FAIL

View file

@ -1,3 +0,0 @@
[output-validity.html]
[:valid and :invalid pseudo-class on output element]
expected: FAIL

View file

@ -0,0 +1,3 @@
[select-validity.html]
[Remove and add back the placeholder label option]
expected: FAIL

View file

@ -1,6 +0,0 @@
[valid-invalid-fieldset-disconnected.html]
[<input> element becomes invalid inside disconnected <fieldset>]
expected: FAIL
[<select> element becomes valid inside disconnected <fieldset>]
expected: FAIL

View file

@ -1,87 +0,0 @@
[valid-invalid.html]
type: testharness
bug: https://github.com/servo/servo/issues/10781
[':valid' matches elements that satisfy their constraints]
expected: FAIL
[':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]
expected: FAIL
[':valid' matches fieldset elements that have no descendant elements that themselves are candidates for constraint validation but do not satisfy their constraints]
expected: FAIL
[':valid' matches elements that satisfy their pattern constraints]
expected: FAIL
[':valid' matches elements that satisfy their number constraints]
expected: FAIL
[':invalid' matches elements that do not satisfy their simple text constraints]
expected: FAIL
[':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]
expected: FAIL
[':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]
expected: FAIL
[':invalid' matches elements that do not satisfy their pattern constraints]
expected: FAIL
[':invalid' matches elements that do not satisfy their number constraints]
expected: FAIL
[':valid' matches new elements that satisfy their constraints]
expected: FAIL
[':invalid' doesn't match new elements that satisfy their constraints]
expected: FAIL
[':valid' doesn't match new elements that do not satisfy their constraints]
expected: FAIL
[':invalid' matches new elements that do not satisfy their constraints]
expected: FAIL
[empty form correctly styled on page-load]
expected: FAIL
[valid form correctly styled on page-load]
expected: FAIL
[invalid form correctly styled on page-load]
expected: FAIL
[programmatically adding valid to empty form results in correct style]
expected: FAIL
[programmatically adding invalid to empty form results in correct style]
expected: FAIL
[programmatically-invalidated form correctly styled]
expected: FAIL
[programmatically-validated form correctly styled]
expected: FAIL
[empty fieldset correctly styled on page-load]
expected: FAIL
[valid fieldset correctly styled on page-load]
expected: FAIL
[invalid fieldset correctly styled on page-load]
expected: FAIL
[programmatically adding valid to empty fieldset results in correct style]
expected: FAIL
[programmatically adding invalid to empty fieldset results in correct style]
expected: FAIL
[programmatically-invalidated fieldset correctly styled]
expected: FAIL
[programmatically-validated fieldset correctly styled]
expected: FAIL

View file

@ -1,4 +0,0 @@
[input-pattern-dynamic-value.html]
[input validation is updated after pattern attribute change]
expected: FAIL

View file

@ -1,4 +0,0 @@
[form-double-submit-2.html]
[preventDefault should allow onclick submit() to succeed]
expected: FAIL

View file

@ -1,4 +0,0 @@
[form-double-submit.html]
[default submit action should supersede onclick submit()]
expected: FAIL