diff --git a/Libraries/LibWeb/ARIA/ARIAMixin.cpp b/Libraries/LibWeb/ARIA/ARIAMixin.cpp index cb2d4a43a47..1bd04f4cdd7 100644 --- a/Libraries/LibWeb/ARIA/ARIAMixin.cpp +++ b/Libraries/LibWeb/ARIA/ARIAMixin.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include @@ -14,6 +15,13 @@ namespace Web::ARIA { ARIAMixin::ARIAMixin() = default; ARIAMixin::~ARIAMixin() = default; +void ARIAMixin::visit_edges(GC::Cell::Visitor& visitor) { +#define __ENUMERATE_ARIA_ATTRIBUTE(attribute, referencing_attribute) \ + visitor.visit(m_cached_##attribute); + ENUMERATE_ARIA_ELEMENT_LIST_REFERENCING_ATTRIBUTES +#undef __ENUMERATE_ARIA_ATTRIBUTE +} + // https://www.w3.org/TR/wai-aria-1.2/#introroles Optional ARIAMixin::role_from_role_attribute_value() const { @@ -247,6 +255,16 @@ ENUMERATE_ARIA_ELEMENT_REFERENCING_ATTRIBUTES void ARIAMixin::set_##attribute(Optional>> value) \ { \ m_##attribute = move(value); \ + } \ + \ + GC::Ptr ARIAMixin::cached_##attribute() const \ + { \ + return m_cached_##attribute; \ + } \ + \ + void ARIAMixin::set_cached_##attribute(GC::Ptr value) \ + { \ + m_cached_##attribute = value; \ } ENUMERATE_ARIA_ELEMENT_LIST_REFERENCING_ATTRIBUTES #undef __ENUMERATE_ARIA_ATTRIBUTE diff --git a/Libraries/LibWeb/ARIA/ARIAMixin.h b/Libraries/LibWeb/ARIA/ARIAMixin.h index 2a928feed04..1759486c474 100644 --- a/Libraries/LibWeb/ARIA/ARIAMixin.h +++ b/Libraries/LibWeb/ARIA/ARIAMixin.h @@ -68,13 +68,18 @@ public: #define __ENUMERATE_ARIA_ATTRIBUTE(attribute, referencing_attribute) \ Optional>> const& attribute() const; \ - void set_##attribute(Optional>> value); + void set_##attribute(Optional>>); \ + \ + GC::Ptr cached_##attribute() const; \ + void set_cached_##attribute(GC::Ptr); ENUMERATE_ARIA_ELEMENT_LIST_REFERENCING_ATTRIBUTES #undef __ENUMERATE_ARIA_ATTRIBUTE protected: ARIAMixin(); + void visit_edges(GC::Cell::Visitor&); + virtual bool id_reference_exists(String const&) const = 0; private: @@ -84,7 +89,8 @@ private: #undef __ENUMERATE_ARIA_ATTRIBUTE #define __ENUMERATE_ARIA_ATTRIBUTE(attribute, referencing_attribute) \ - Optional>> m_##attribute; + Optional>> m_##attribute; \ + GC::Ptr m_cached_##attribute; ENUMERATE_ARIA_ELEMENT_LIST_REFERENCING_ATTRIBUTES #undef __ENUMERATE_ARIA_ATTRIBUTE }; diff --git a/Libraries/LibWeb/DOM/Element.cpp b/Libraries/LibWeb/DOM/Element.cpp index 8b753a10e30..7899b8f39e2 100644 --- a/Libraries/LibWeb/DOM/Element.cpp +++ b/Libraries/LibWeb/DOM/Element.cpp @@ -106,6 +106,7 @@ void Element::visit_edges(Cell::Visitor& visitor) Base::visit_edges(visitor); SlottableMixin::visit_edges(visitor); Animatable::visit_edges(visitor); + ARIAMixin::visit_edges(visitor); visitor.visit(m_attributes); visitor.visit(m_inline_style); diff --git a/Libraries/LibWeb/WebIDL/AbstractOperations.cpp b/Libraries/LibWeb/WebIDL/AbstractOperations.cpp index 794b27eb170..f2b05290d10 100644 --- a/Libraries/LibWeb/WebIDL/AbstractOperations.cpp +++ b/Libraries/LibWeb/WebIDL/AbstractOperations.cpp @@ -528,4 +528,25 @@ template JS::ThrowCompletionOr convert_to_int(JS::VM& vm, JS::Valu template JS::ThrowCompletionOr convert_to_int(JS::VM& vm, JS::Value, EnforceRange, Clamp); template JS::ThrowCompletionOr convert_to_int(JS::VM& vm, JS::Value, EnforceRange, Clamp); +// AD-HOC: For same-object caching purposes, this can be used to compare a cached JS array of DOM::Elements with another +// list. Either list can be null, in which case they are considered the same only if they are both null. +bool lists_contain_same_elements(GC::Ptr array, Optional>> const& elements) +{ + if (!array || !elements.has_value()) + return !array && !elements.has_value(); + + bool is_equivalent = array->indexed_properties().array_like_size() == elements->size(); + + for (size_t i = 0; is_equivalent && i < elements->size(); ++i) { + auto cached_value = array->get_without_side_effects(JS::PropertyKey { i }); + auto const& cached_element = as(cached_value.as_object()); + + auto it = elements->find_if([&](auto const& element) { return element.ptr() == &cached_element; }); + if (it == elements->end()) + is_equivalent = false; + } + + return is_equivalent; +} + } diff --git a/Libraries/LibWeb/WebIDL/AbstractOperations.h b/Libraries/LibWeb/WebIDL/AbstractOperations.h index 3044a9c39ae..10139afbac8 100644 --- a/Libraries/LibWeb/WebIDL/AbstractOperations.h +++ b/Libraries/LibWeb/WebIDL/AbstractOperations.h @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -54,4 +55,6 @@ enum class Clamp { template JS::ThrowCompletionOr convert_to_int(JS::VM& vm, JS::Value, EnforceRange enforce_range = EnforceRange::No, Clamp clamp = Clamp::No); +bool lists_contain_same_elements(GC::Ptr array, Optional>> const& elements); + } diff --git a/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp b/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp index 0fc75e975e8..11865e8e2ad 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp @@ -1874,7 +1874,7 @@ static void generate_wrap_statement(SourceGenerator& generator, ByteString const // This might need to change if we switch to a RootVector. if (is_platform_object(sequence_generic_type.parameters().first())) { scoped_generator.append(R"~~~( - auto* wrapped_element@recursion_depth@ = &(*element@recursion_depth@); + auto* wrapped_element@recursion_depth@ = &(*element@recursion_depth@); )~~~"); } else { scoped_generator.append("JS::Value wrapped_element@recursion_depth@;\n"sv); @@ -3748,6 +3748,9 @@ static void generate_prototype_or_global_mixin_definitions(IDL::Interface const& for (auto& attribute : interface.attributes) { if (attribute.extended_attributes.contains("FIXME")) continue; + + bool generated_reflected_element_array = false; + auto attribute_generator = generator.fork(); attribute_generator.set("attribute.name", attribute.name); attribute_generator.set("attribute.getter_callback", attribute.getter_callback_name); @@ -4054,18 +4057,14 @@ JS_DEFINE_NATIVE_FUNCTION(@class_name@::@attribute.getter_callback@) // inherits from Element, then with attr being the reflected content attribute name: // FIXME: Handle "an interface that inherits from Element". else if (is_nullable_frozen_array_of_single_type(attribute.type, "Element"sv)) { + generated_reflected_element_array = true; + // 1. Let elements be the result of running this's get the attr-associated elements. attribute_generator.append(R"~~~( static auto content_attribute = "@attribute.reflect_name@"_fly_string; auto retval = impl->get_the_attribute_associated_elements(content_attribute, TRY(throw_dom_exception_if_needed(vm, [&] { return impl->@attribute.cpp_name@(); }))); )~~~"); - - // FIXME: 2. If the contents of elements is equal to the contents of this's cached attr-associated elements, then return - // this's cached attr-associated elements object. - // FIXME: 3. Let elementsAsFrozenArray be elements, converted to a FrozenArray?. - // FIXME: 4. Set this's cached attr-associated elements to elements. - // FIXME: 5. Set this's cached attr-associated elements object to elementsAsFrozenArray. } else { attribute_generator.append(R"~~~( auto retval = impl->get_attribute_value("@attribute.reflect_name@"_fly_string); @@ -4083,6 +4082,19 @@ JS_DEFINE_NATIVE_FUNCTION(@class_name@::@attribute.getter_callback@) Bindings::invoke_custom_element_reactions(queue); )~~~"); } + + if (generated_reflected_element_array) { + // 2. If the contents of elements is equal to the contents of this's cached attr-associated elements, + // then return this's cached attr-associated elements object. + attribute_generator.append(R"~~~( + auto cached_@attribute.cpp_name@ = TRY(throw_dom_exception_if_needed(vm, [&] { return impl->cached_@attribute.cpp_name@(); })); + if (WebIDL::lists_contain_same_elements(cached_@attribute.cpp_name@, retval)) + return cached_@attribute.cpp_name@; + + auto result = TRY([&]() -> JS::ThrowCompletionOr { +)~~~"); + } + } else { if (!attribute.extended_attributes.contains("CEReactions")) { attribute_generator.append(R"~~~( @@ -4127,6 +4139,24 @@ JS_DEFINE_NATIVE_FUNCTION(@class_name@::@attribute.getter_callback@) generate_return_statement(generator, *attribute.type, interface); + if (generated_reflected_element_array) { + // 3. Let elementsAsFrozenArray be elements, converted to a FrozenArray?. + // 4. Set this's cached attr-associated elements to elements. + // 5. Set this's cached attr-associated elements object to elementsAsFrozenArray. + attribute_generator.append(R"~~~( + }()); + + if (result.is_null()) { + TRY(throw_dom_exception_if_needed(vm, [&] { impl->set_cached_@attribute.cpp_name@({}); })); + } else { + auto& array = as(result.as_object()); + TRY(throw_dom_exception_if_needed(vm, [&] { impl->set_cached_@attribute.cpp_name@(&array); })); + } + + return result; +)~~~"); + } + attribute_generator.append(R"~~~( } )~~~"); diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/dom/aria-element-reflection.txt b/Tests/LibWeb/Text/expected/wpt-import/html/dom/aria-element-reflection.txt index 15b29c9dfaa..219654305e2 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/html/dom/aria-element-reflection.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/html/dom/aria-element-reflection.txt @@ -2,8 +2,8 @@ Harness status: OK Found 27 tests -22 Pass -5 Fail +25 Pass +2 Fail Pass aria-activedescendant element reflection Pass aria-activedescendant If the content attribute is set directly, the IDL attribute getter always returns the first element whose ID matches the content attribute. Pass aria-activedescendant Setting the IDL attribute to an element which is not the first element in DOM order with its ID causes the content attribute to be an empty string @@ -16,7 +16,7 @@ Pass aria-activedescendant Changing the ID of an element doesn't lose the refere Pass aria-activedescendant Reparenting an element into a descendant shadow scope hides the element reference. Pass aria-activedescendant Reparenting referenced element cannot cause retargeting of reference. Pass aria-activedescendant Element reference set in invalid scope remains intact throughout move to valid scope. -Fail aria-labelledby. +Pass aria-labelledby. Pass aria-controls. Pass aria-describedby. Pass aria-flowto. @@ -28,6 +28,6 @@ Pass aria-activedescendant Reparenting. Pass aria-activedescendant Attaching element reference before it's inserted into the DOM. Pass aria-activedescendant Cross-document references and moves. Pass aria-activedescendant Adopting element keeps references. -Fail Caching invariant different attributes. -Fail Caching invariant different elements. +Pass Caching invariant different attributes. +Pass Caching invariant different elements. Pass Passing values of the wrong type should throw a TypeError \ No newline at end of file