mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-07-29 04:09:13 +00:00
LibWeb: Implement :required/:optional pseudo-classes
This commit is contained in:
parent
fbd1f77161
commit
7acc0f4a42
Notes:
github-actions[bot]
2025-05-24 09:32:37 +00:00
Author: https://github.com/Gingeh
Commit: 7acc0f4a42
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/4838
Reviewed-by: https://github.com/AtkinsSJ ✅
13 changed files with 300 additions and 2 deletions
|
@ -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": ""
|
||||||
},
|
},
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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 });
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
68
Tests/LibWeb/Ref/input/required-optional-pseudoclass.html
Normal file
68
Tests/LibWeb/Ref/input/required-optional-pseudoclass.html
Normal 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>
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
||||||
|
Harness status: OK
|
||||||
|
|
||||||
|
Found 1 tests
|
||||||
|
|
||||||
|
1 Pass
|
||||||
|
Pass Evaluation of :required and :optional changes for input type change.
|
|
@ -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
|
|
@ -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>
|
|
@ -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>
|
Loading…
Add table
Add a link
Reference in a new issue