LibWeb: Implement :required/:optional pseudo-classes

This commit is contained in:
Gingeh 2025-04-30 10:23:46 +10:00 committed by Sam Atkins
commit 7acc0f4a42
Notes: github-actions[bot] 2025-05-24 09:32:37 +00:00
13 changed files with 300 additions and 2 deletions

View file

@ -116,6 +116,9 @@
"optimal-value": { "optimal-value": {
"argument": "" "argument": ""
}, },
"optional": {
"argument": ""
},
"popover-open": { "popover-open": {
"argument": "" "argument": ""
}, },
@ -134,6 +137,9 @@
"read-write": { "read-write": {
"argument": "" "argument": ""
}, },
"required": {
"argument": ""
},
"root": { "root": {
"argument": "" "argument": ""
}, },

View file

@ -988,6 +988,55 @@ static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoCla
return false; return false;
} }
case CSS::PseudoClass::Required: {
// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-required
// The :required pseudo-class must match any element falling into one of the following categories:
// - input elements that are required
if (auto const* input_element = as_if<Web::HTML::HTMLInputElement>(element)) {
if (input_element->required_applies() && input_element->has_attribute(HTML::AttributeNames::required))
return true;
}
// - select elements that have a required attribute
else if (auto const* select_element = as_if<Web::HTML::HTMLSelectElement>(element)) {
if (select_element->has_attribute(HTML::AttributeNames::required))
return true;
}
// - textarea elements that have a required attribute
else if (auto const* textarea_element = as_if<Web::HTML::HTMLTextAreaElement>(element)) {
if (textarea_element->has_attribute(HTML::AttributeNames::required))
return true;
}
return false;
}
case CSS::PseudoClass::Optional: {
// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-optional
// The :optional pseudo-class must match any element falling into one of the following categories:
// - input elements to which the required attribute applies that are not required
if (auto const* input_element = as_if<Web::HTML::HTMLInputElement>(element)) {
if (input_element->required_applies() && !input_element->has_attribute(HTML::AttributeNames::required))
return true;
// AD-HOC: Chromium and Webkit also match for hidden inputs (and WPT expects this)
// See: https://github.com/whatwg/html/issues/11273
if (input_element->type_state() == HTML::HTMLInputElement::TypeAttributeState::Hidden)
return true;
}
// - select elements that do not have a required attribute
else if (auto const* select_element = as_if<Web::HTML::HTMLSelectElement>(element)) {
if (!select_element->has_attribute(HTML::AttributeNames::required))
return true;
}
// - textarea elements that do not have a required attribute
else if (auto const* textarea_element = as_if<Web::HTML::HTMLTextAreaElement>(element)) {
if (!textarea_element->has_attribute(HTML::AttributeNames::required))
return true;
}
return false;
}
} }
return false; return false;

View file

@ -141,6 +141,8 @@ static void build_invalidation_sets_for_simple_selector(Selector::SimpleSelector
case PseudoClass::Link: case PseudoClass::Link:
case PseudoClass::AnyLink: case PseudoClass::AnyLink:
case PseudoClass::LocalLink: case PseudoClass::LocalLink:
case PseudoClass::Required:
case PseudoClass::Optional:
invalidation_set.set_needs_invalidate_pseudo_class(pseudo_class.type); invalidation_set.set_needs_invalidate_pseudo_class(pseudo_class.type);
break; break;
default: default:

View file

@ -2397,6 +2397,9 @@ void Element::invalidate_style_after_attribute_change(FlyString const& attribute
changed_properties.append({ .type = CSS::InvalidationSet::Property::Type::PseudoClass, .value = CSS::PseudoClass::PlaceholderShown }); changed_properties.append({ .type = CSS::InvalidationSet::Property::Type::PseudoClass, .value = CSS::PseudoClass::PlaceholderShown });
} else if (attribute_name == HTML::AttributeNames::value) { } else if (attribute_name == HTML::AttributeNames::value) {
changed_properties.append({ .type = CSS::InvalidationSet::Property::Type::PseudoClass, .value = CSS::PseudoClass::Checked }); changed_properties.append({ .type = CSS::InvalidationSet::Property::Type::PseudoClass, .value = CSS::PseudoClass::Checked });
} else if (attribute_name == HTML::AttributeNames::required) {
changed_properties.append({ .type = CSS::InvalidationSet::Property::Type::PseudoClass, .value = CSS::PseudoClass::Required });
changed_properties.append({ .type = CSS::InvalidationSet::Property::Type::PseudoClass, .value = CSS::PseudoClass::Optional });
} }
changed_properties.append({ .type = CSS::InvalidationSet::Property::Type::Attribute, .value = attribute_name }); changed_properties.append({ .type = CSS::InvalidationSet::Property::Type::Attribute, .value = attribute_name });

View file

@ -3002,6 +3002,31 @@ bool HTMLInputElement::multiple_applies() const
} }
} }
// https://html.spec.whatwg.org/multipage/input.html#do-not-apply
bool HTMLInputElement::required_applies() const
{
switch (type_state()) {
case TypeAttributeState::Text:
case TypeAttributeState::Search:
case TypeAttributeState::Telephone:
case TypeAttributeState::URL:
case TypeAttributeState::Email:
case TypeAttributeState::Password:
case TypeAttributeState::Date:
case TypeAttributeState::Month:
case TypeAttributeState::Week:
case TypeAttributeState::Time:
case TypeAttributeState::LocalDateAndTime:
case TypeAttributeState::Number:
case TypeAttributeState::Checkbox:
case TypeAttributeState::RadioButton:
case TypeAttributeState::FileUpload:
return true;
default:
return false;
}
}
bool HTMLInputElement::has_selectable_text() const bool HTMLInputElement::has_selectable_text() const
{ {
// Potential FIXME: Date, Month, Week, Time and LocalDateAndTime are rendered as a basic text input for now, // Potential FIXME: Date, Month, Week, Time and LocalDateAndTime are rendered as a basic text input for now,

View file

@ -219,6 +219,7 @@ public:
bool selection_direction_applies() const; bool selection_direction_applies() const;
bool pattern_applies() const; bool pattern_applies() const;
bool multiple_applies() const; bool multiple_applies() const;
bool required_applies() const;
bool has_selectable_text() const; bool has_selectable_text() const;
bool supports_a_picker() const; bool supports_a_picker() const;

View file

@ -0,0 +1,56 @@
<!DOCTYPE html>
<style>
input.required {
border: 2px solid red;
}
input.optional {
border: 2px solid blue;
}
input.required + span {
color: green;
}
div > input.required {
border: 2px dashed red;
}
section input.optional {
border: 2px dashed blue;
}
input.required ~ label {
color: orange;
}
form.has-required {
border: 2px solid purple;
}
</style>
<input type="text" class="required" placeholder="Should be red"><span>Should be green</span>
<input type="text" class="optional" placeholder="Should be blue">
<div>
<input type="text" class="required" placeholder="Should be dashed red">
</div>
<section>
<div>
<input type="text" class="optional" placeholder="Should be dashed blue">
</div>
</section>
<p>Other content</p>
<label>Should be orange</label>
<form class="has-required">
<span>Purple border</span>
<input id="make-me-required" class="required" type="text" placeholder="Should be red">
</form>
<form>
<span>No border</span>
<input id="make-me-optional" class="optional" type="text" placeholder="Should be blue">
</form>

View file

@ -0,0 +1,68 @@
<!DOCTYPE html>
<link rel="match" href="../expected/required-optional-pseudoclass-ref.html" />
<style>
input:required {
border: 2px solid red;
}
input:optional {
border: 2px solid blue;
}
input:required+span {
color: green;
}
div>input:required {
border: 2px dashed red;
}
section input:optional {
border: 2px dashed blue;
}
input:required~label {
color: orange;
}
form:has(input:required) {
border: 2px solid purple;
}
</style>
<input type="text" required placeholder="Should be red"><span>Should be green</span>
<input type="text" placeholder="Should be blue">
<div>
<input type="text" required placeholder="Should be dashed red">
</div>
<section>
<div>
<input type="text" placeholder="Should be dashed blue">
</div>
</section>
<p>Other content</p>
<label>Should be orange</label>
<form>
<span>Purple border</span>
<input id="make-me-required" type="text" placeholder="Should be red">
</form>
<form>
<span>No border</span>
<input id="make-me-optional" type="text" required placeholder="Should be blue">
</form>
<!-- After the document has loaded, toggle the required-ness of the previous two inputs -->
<script>
document.addEventListener("DOMContentLoaded", function () {
const makeMeOptional = document.getElementById("make-me-optional");
const makeMeRequired = document.getElementById("make-me-required");
makeMeOptional.required = false;
makeMeRequired.required = true;
});
</script>

View file

@ -2,5 +2,5 @@ Harness status: OK
Found 1 tests Found 1 tests
1 Fail 1 Pass
Fail Test required attribute Pass Test required attribute

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass Evaluation of :required and :optional changes for input type change.

View file

@ -0,0 +1,11 @@
Harness status: OK
Found 6 tests
6 Pass
Pass ':required' matches required <input>s, <select>s and <textarea>s
Pass ':optional' matches elements <input>s, <select>s and <textarea>s that are not required
Pass ':required' doesn't match elements whose required attribute has been removed
Pass ':optional' matches elements whose required attribute has been removed
Pass ':required' matches elements whose required attribute has been added
Pass ':optional' doesn't match elements whose required attribute has been added

View file

@ -0,0 +1,36 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>Selector: pseudo-classes (:required, :optional) for hidden input</title>
<link rel="author" title="Rune Lillesveen" href="mailto:rune@opera.com">
<link rel="help" href="https://html.spec.whatwg.org/multipage/#pseudo-classes">
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<style>
span {
color: red;
background-color: pink;
}
:required + span {
color: green;
}
:not(:optional) + span {
background-color: lime;
}
</style>
<input id="hiddenInput" type="hidden" required>
<span id="sibling">This text should be green on lime background.</span>
<script>
test(() => {
assert_equals(getComputedStyle(sibling).color, "rgb(255, 0, 0)",
"Not matching :required for type=hidden");
assert_equals(getComputedStyle(sibling).backgroundColor, "rgb(255, 192, 203)",
"Matching :optional for type=hidden");
hiddenInput.type = "text";
assert_equals(getComputedStyle(sibling).color, "rgb(0, 128, 0)",
"Matching :required for type=text");
assert_equals(getComputedStyle(sibling).backgroundColor, "rgb(0, 255, 0)",
"Matching :not(:optional) for type=text");
}, "Evaluation of :required and :optional changes for input type change.");
</script>

View file

@ -0,0 +1,35 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>Selector: pseudo-classes (:required, :optional)</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>
<div id="log"></div>
<input type=text id=text1 value="foobar" required>
<input type=text id=text2 required>
<input type=text id=text3>
<select id=select1 required>
<optgroup label="options" id=optgroup1>
<option value="option1" id=option1>option1
</select>
<select id=select2>
<optgroup label="options" id=optgroup2>
<option value="option2" id=option2>option2
</select>
<textarea required id=textarea1>textarea1</textarea>
<textarea id=textarea2>textarea2</textarea>
<script>
testSelectorIdsMatch(":required", ["text1", "text2", "select1", "textarea1"], "':required' matches required <input>s, <select>s and <textarea>s");
testSelectorIdsMatch(":optional", ["text3", "select2", "textarea2"], "':optional' matches elements <input>s, <select>s and <textarea>s that are not required");
document.getElementById("text1").removeAttribute("required");
testSelectorIdsMatch(":required", ["text2", "select1", "textarea1"], "':required' doesn't match elements whose required attribute has been removed");
testSelectorIdsMatch(":optional", ["text1", "text3", "select2", "textarea2"], "':optional' matches elements whose required attribute has been removed");
document.getElementById("select2").setAttribute("required", "required");
testSelectorIdsMatch(":required", ["text2", "select1", "select2", "textarea1"], "':required' matches elements whose required attribute has been added");
testSelectorIdsMatch(":optional", ["text1", "text3", "textarea2"], "':optional' doesn't match elements whose required attribute has been added");
</script>