LibWeb: Keep track of the order in which option elements are selected

This allows us to locate the most-recently-selected when running the
selectedness update algorithm.
This commit is contained in:
Andreas Kling 2024-11-14 00:05:38 +01:00 committed by Andreas Kling
parent 581597cb34
commit dc9179bb1b
Notes: github-actions[bot] 2024-11-14 22:07:21 +00:00
10 changed files with 107 additions and 27 deletions

View file

@ -77,7 +77,7 @@ JS::ThrowCompletionOr<JS::NonnullGCPtr<JS::Object>> OptionConstructor::construct
} }
// 6. If selected is true, then set option's selectedness to true; otherwise set its selectedness to false (even if defaultSelected is true). // 6. If selected is true, then set option's selectedness to true; otherwise set its selectedness to false (even if defaultSelected is true).
option_element->m_selected = vm.argument(3).to_boolean(); option_element->set_selected_internal(vm.argument(3).to_boolean());
// 7. Return option. // 7. Return option.
return option_element; return option_element;

View file

@ -7,6 +7,7 @@
#include <LibWeb/Bindings/HTMLOptGroupElementPrototype.h> #include <LibWeb/Bindings/HTMLOptGroupElementPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h> #include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/HTML/HTMLOptGroupElement.h> #include <LibWeb/HTML/HTMLOptGroupElement.h>
#include <LibWeb/HTML/HTMLSelectElement.h>
namespace Web::HTML { namespace Web::HTML {
@ -25,4 +26,24 @@ void HTMLOptGroupElement::initialize(JS::Realm& realm)
WEB_SET_PROTOTYPE_FOR_INTERFACE(HTMLOptGroupElement); WEB_SET_PROTOTYPE_FOR_INTERFACE(HTMLOptGroupElement);
} }
void HTMLOptGroupElement::inserted()
{
Base::inserted();
// AD-HOC: We update the selectedness of our <select> parent here,
// to ensure that the correct <option> is selected after an <optgroup> is dynamically inserted.
if (is<HTMLSelectElement>(*parent()) && first_child_of_type<HTMLOptionElement>())
static_cast<HTMLSelectElement&>(*parent()).update_selectedness();
}
void HTMLOptGroupElement::removed_from(Node* old_parent)
{
Base::removed_from(old_parent);
// The optgroup HTML element removing steps, given removedNode and oldParent, are:
// 1. If oldParent is a select element and removedNode has an option child, then run oldParent's selectedness setting algorithm.
if (old_parent && is<HTMLSelectElement>(*old_parent) && first_child_of_type<HTMLOptionElement>())
static_cast<HTMLSelectElement&>(*old_parent).update_selectedness();
}
} }

View file

@ -25,6 +25,8 @@ private:
HTMLOptGroupElement(DOM::Document&, DOM::QualifiedName); HTMLOptGroupElement(DOM::Document&, DOM::QualifiedName);
virtual void initialize(JS::Realm&) override; virtual void initialize(JS::Realm&) override;
virtual void removed_from(Node*) override;
virtual void inserted() override;
}; };
} }

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2020, the SerenityOS developers. * Copyright (c) 2020, the SerenityOS developers.
* Copyright (c) 2022, Andreas Kling <andreas@ladybird.org> * Copyright (c) 2022-2024, Andreas Kling <andreas@ladybird.org>
* *
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
@ -15,12 +15,15 @@
#include <LibWeb/HTML/HTMLOptionElement.h> #include <LibWeb/HTML/HTMLOptionElement.h>
#include <LibWeb/HTML/HTMLScriptElement.h> #include <LibWeb/HTML/HTMLScriptElement.h>
#include <LibWeb/HTML/HTMLSelectElement.h> #include <LibWeb/HTML/HTMLSelectElement.h>
#include <LibWeb/HighResolutionTime/TimeOrigin.h>
#include <LibWeb/Infra/Strings.h> #include <LibWeb/Infra/Strings.h>
namespace Web::HTML { namespace Web::HTML {
JS_DEFINE_ALLOCATOR(HTMLOptionElement); JS_DEFINE_ALLOCATOR(HTMLOptionElement);
static u64 m_next_selectedness_update_index = 1;
HTMLOptionElement::HTMLOptionElement(DOM::Document& document, DOM::QualifiedName qualified_name) HTMLOptionElement::HTMLOptionElement(DOM::Document& document, DOM::QualifiedName qualified_name)
: HTMLElement(document, move(qualified_name)) : HTMLElement(document, move(qualified_name))
{ {
@ -42,13 +45,13 @@ void HTMLOptionElement::attribute_changed(FlyString const& name, Optional<String
if (!value.has_value()) { if (!value.has_value()) {
// Whenever an option element's selected attribute is removed, if its dirtiness is false, its selectedness must be set to false. // Whenever an option element's selected attribute is removed, if its dirtiness is false, its selectedness must be set to false.
if (!m_dirty) if (!m_dirty)
m_selected = false; set_selected_internal(false);
} else { } else {
// Except where otherwise specified, when the element is created, its selectedness must be set to true // Except where otherwise specified, when the element is created, its selectedness must be set to true
// if the element has a selected attribute. Whenever an option element's selected attribute is added, // if the element has a selected attribute. Whenever an option element's selected attribute is added,
// if its dirtiness is false, its selectedness must be set to true. // if its dirtiness is false, its selectedness must be set to true.
if (!m_dirty) if (!m_dirty)
m_selected = true; set_selected_internal(true);
} }
} }
} }
@ -65,6 +68,8 @@ void HTMLOptionElement::set_selected(bool selected)
void HTMLOptionElement::set_selected_internal(bool selected) void HTMLOptionElement::set_selected_internal(bool selected)
{ {
m_selected = selected; m_selected = selected;
if (selected)
m_selectedness_update_index = m_next_selectedness_update_index++;
} }
// https://html.spec.whatwg.org/multipage/form-elements.html#dom-option-value // https://html.spec.whatwg.org/multipage/form-elements.html#dom-option-value
@ -139,7 +144,7 @@ void HTMLOptionElement::ask_for_a_reset()
{ {
// If an option element in the list of options asks for a reset, then run that select element's selectedness setting algorithm. // If an option element in the list of options asks for a reset, then run that select element's selectedness setting algorithm.
if (auto* select = first_ancestor_of_type<HTMLSelectElement>()) { if (auto* select = first_ancestor_of_type<HTMLSelectElement>()) {
select->update_selectedness(this); select->update_selectedness();
} }
} }
@ -177,4 +182,35 @@ Optional<ARIA::Role> HTMLOptionElement::default_role() const
return ARIA::Role::option; return ARIA::Role::option;
} }
void HTMLOptionElement::inserted()
{
Base::inserted();
set_selected_internal(selected());
// 1. The option HTML element insertion steps, given insertedNode, are:
// If insertedNode's parent is a select element,
// or insertedNode's parent is an optgroup element whose parent is a select element,
// then run that select element's selectedness setting algorithm.
if (is<HTMLSelectElement>(*parent()))
static_cast<HTMLSelectElement&>(*parent()).update_selectedness();
else if (is<HTMLOptGroupElement>(parent()) && parent()->parent() && is<HTMLSelectElement>(*parent()->parent()))
static_cast<HTMLSelectElement&>(*parent()->parent()).update_selectedness();
}
void HTMLOptionElement::removed_from(Node* old_parent)
{
Base::removed_from(old_parent);
// The option HTML element removing steps, given removedNode and oldParent, are:
// 1. If oldParent is a select element, or oldParent is an optgroup element whose parent is a select element,
// then run that select element's selectedness setting algorithm.
if (old_parent) {
if (is<HTMLSelectElement>(*old_parent))
static_cast<HTMLSelectElement&>(*old_parent).update_selectedness();
else if (is<HTMLOptGroupElement>(*old_parent) && old_parent->parent_element() && is<HTMLSelectElement>(old_parent->parent_element()))
static_cast<HTMLSelectElement&>(*old_parent->parent_element()).update_selectedness();
}
}
} }

View file

@ -21,6 +21,7 @@ public:
bool selected() const { return m_selected; } bool selected() const { return m_selected; }
void set_selected(bool); void set_selected(bool);
void set_selected_internal(bool); void set_selected_internal(bool);
[[nodiscard]] u64 selectedness_update_index() const { return m_selectedness_update_index; }
String value() const; String value() const;
WebIDL::ExceptionOr<void> set_value(String const&); WebIDL::ExceptionOr<void> set_value(String const&);
@ -46,6 +47,9 @@ private:
virtual void attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_) override; virtual void attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_) override;
virtual void inserted() override;
virtual void removed_from(Node*) override;
void ask_for_a_reset(); void ask_for_a_reset();
// https://html.spec.whatwg.org/multipage/form-elements.html#concept-option-selectedness // https://html.spec.whatwg.org/multipage/form-elements.html#concept-option-selectedness
@ -53,6 +57,8 @@ private:
// https://html.spec.whatwg.org/multipage/form-elements.html#concept-option-dirtiness // https://html.spec.whatwg.org/multipage/form-elements.html#concept-option-dirtiness
bool m_dirty { false }; bool m_dirty { false };
u64 m_selectedness_update_index { 0 };
}; };
} }

View file

@ -211,7 +211,7 @@ void HTMLSelectElement::reset_algorithm()
// The reset algorithm for select elements is to go through all the option elements in the element's list of options, // The reset algorithm for select elements is to go through all the option elements in the element's list of options,
for (auto const& option_element : list_of_options()) { for (auto const& option_element : list_of_options()) {
// set their selectedness to true if the option element has a selected attribute, and false otherwise, // set their selectedness to true if the option element has a selected attribute, and false otherwise,
option_element->m_selected = option_element->has_attribute(AttributeNames::selected); option_element->set_selected_internal(option_element->has_attribute(AttributeNames::selected));
// set their dirtiness to false, // set their dirtiness to false,
option_element->m_dirty = false; option_element->m_dirty = false;
// and then have the option elements ask for a reset. // and then have the option elements ask for a reset.
@ -241,13 +241,13 @@ void HTMLSelectElement::set_selected_index(WebIDL::Long index)
// if any, must have its selectedness set to true and its dirtiness set to true. // if any, must have its selectedness set to true and its dirtiness set to true.
auto options = list_of_options(); auto options = list_of_options();
for (auto& option : options) for (auto& option : options)
option->m_selected = false; option->set_selected_internal(false);
if (index < 0 || index >= static_cast<int>(options.size())) if (index < 0 || index >= static_cast<int>(options.size()))
return; return;
auto& selected_option = options[index]; auto& selected_option = options[index];
selected_option->m_selected = true; selected_option->set_selected_internal(true);
selected_option->m_dirty = true; selected_option->m_dirty = true;
} }
@ -258,6 +258,12 @@ i32 HTMLSelectElement::default_tab_index_value() const
return 0; return 0;
} }
void HTMLSelectElement::children_changed()
{
Base::children_changed();
update_selectedness();
}
// https://html.spec.whatwg.org/multipage/form-elements.html#dom-select-type // https://html.spec.whatwg.org/multipage/form-elements.html#dom-select-type
String const& HTMLSelectElement::type() const String const& HTMLSelectElement::type() const
{ {
@ -562,7 +568,7 @@ void HTMLSelectElement::update_inner_text_element()
} }
// https://html.spec.whatwg.org/multipage/form-elements.html#selectedness-setting-algorithm // https://html.spec.whatwg.org/multipage/form-elements.html#selectedness-setting-algorithm
void HTMLSelectElement::update_selectedness(JS::GCPtr<HTML::HTMLOptionElement> last_selected_option) void HTMLSelectElement::update_selectedness()
{ {
if (has_attribute(AttributeNames::multiple)) if (has_attribute(AttributeNames::multiple))
return; return;
@ -605,14 +611,22 @@ void HTMLSelectElement::update_selectedness(JS::GCPtr<HTML::HTMLOptionElement> l
if (number_of_selected >= 2) { if (number_of_selected >= 2) {
// then set the selectedness of all but the last option element with its selectedness set to true // then set the selectedness of all but the last option element with its selectedness set to true
// in the list of options in tree order to false. // in the list of options in tree order to false.
JS::GCPtr<HTML::HTMLOptionElement> last_selected_option;
u64 last_selected_option_update_index = 0;
for (auto const& option_element : list_of_options()) { for (auto const& option_element : list_of_options()) {
if (option_element == last_selected_option) if (!option_element->selected())
continue; continue;
if (number_of_selected == 1) { if (!last_selected_option
break; || option_element->selectedness_update_index() > last_selected_option_update_index) {
last_selected_option = option_element;
last_selected_option_update_index = option_element->selectedness_update_index();
} }
option_element->set_selected_internal(false); }
--number_of_selected;
for (auto const& option_element : list_of_options()) {
if (option_element != last_selected_option)
option_element->set_selected_internal(false);
} }
} }
update_inner_text_element(); update_inner_text_element();

View file

@ -94,7 +94,7 @@ public:
void did_select_item(Optional<u32> const& id); void did_select_item(Optional<u32> const& id);
void update_selectedness(JS::GCPtr<HTMLOptionElement> last_selected_option = nullptr); void update_selectedness();
private: private:
HTMLSelectElement(DOM::Document&, DOM::QualifiedName); HTMLSelectElement(DOM::Document&, DOM::QualifiedName);
@ -107,6 +107,8 @@ private:
virtual void computed_css_values_changed() override; virtual void computed_css_values_changed() override;
virtual void children_changed() override;
void show_the_picker_if_applicable(); void show_the_picker_if_applicable();
void create_shadow_tree_if_needed(); void create_shadow_tree_if_needed();

View file

@ -6,6 +6,6 @@ Rerun
Found 1 tests Found 1 tests
1 Fail 1 Pass
Details Details
Result Test Name MessageFail <select> needs to be updated when <optgroup> is removed Result Test Name MessagePass <select> needs to be updated when <optgroup> is removed

View file

@ -6,11 +6,10 @@ Rerun
Found 5 tests Found 5 tests
1 Pass 5 Pass
4 Fail
Details Details
Result Test Name MessagePass The last selected OPTION should win; Inserted by parser Result Test Name MessagePass The last selected OPTION should win; Inserted by parser
Fail The last selected OPTION should win; Inserted by DOM API Pass The last selected OPTION should win; Inserted by DOM API
Fail The last selected OPTION should win; Inserted by jQuery append() Pass The last selected OPTION should win; Inserted by jQuery append()
Fail The last selected OPTION should win; Inserted by innerHTML Pass The last selected OPTION should win; Inserted by innerHTML
Fail If an OPTION says it is selected, it should be selected after it is inserted. Pass If an OPTION says it is selected, it should be selected after it is inserted.

View file

@ -6,8 +6,8 @@ Rerun
Found 3 tests Found 3 tests
3 Fail 3 Pass
Details Details
Result Test Name MessageFail ask for reset on node remove, non multiple. Result Test Name MessagePass ask for reset on node remove, non multiple.
Fail ask for reset on node insert, non multiple. Pass ask for reset on node insert, non multiple.
Fail change selectedness of option, non multiple. Pass change selectedness of option, non multiple.