mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-07-29 12:19:54 +00:00
LibWeb: Implement element-referencing ARIA attributes
There are ARIA attributes, e.g. ariaControlsElements, which refer to a list of elements by their ID. For example: <div aria-controls="item1 item2"> The div.ariaControlsElements attribute would be a list of elements whose ID matches the values in the aria-controls attribute.
This commit is contained in:
parent
0289df9357
commit
f985ac8884
Notes:
github-actions[bot]
2025-04-25 00:21:23 +00:00
Author: https://github.com/trflynn89
Commit: f985ac8884
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/4460
Reviewed-by: https://github.com/tcl3 ✅
9 changed files with 3354 additions and 28 deletions
|
@ -230,4 +230,17 @@ Vector<String> ARIAMixin::parse_id_reference_list(Optional<String> const& id_lis
|
|||
return result;
|
||||
}
|
||||
|
||||
#define __ENUMERATE_ARIA_ATTRIBUTE(attribute, referencing_attribute) \
|
||||
Optional<Vector<WeakPtr<DOM::Element>>> const& ARIAMixin::attribute() const \
|
||||
{ \
|
||||
return m_##attribute; \
|
||||
} \
|
||||
\
|
||||
void ARIAMixin::set_##attribute(Optional<Vector<WeakPtr<DOM::Element>>> value) \
|
||||
{ \
|
||||
m_##attribute = move(value); \
|
||||
}
|
||||
ENUMERATE_ARIA_ELEMENT_LIST_REFERENCING_ATTRIBUTES
|
||||
#undef __ENUMERATE_ARIA_ATTRIBUTE
|
||||
|
||||
}
|
||||
|
|
|
@ -15,6 +15,15 @@
|
|||
|
||||
namespace Web::ARIA {
|
||||
|
||||
#define ENUMERATE_ARIA_ELEMENT_LIST_REFERENCING_ATTRIBUTES \
|
||||
__ENUMERATE_ARIA_ATTRIBUTE(aria_controls_elements, aria_controls) \
|
||||
__ENUMERATE_ARIA_ATTRIBUTE(aria_described_by_elements, aria_described_by) \
|
||||
__ENUMERATE_ARIA_ATTRIBUTE(aria_details_elements, aria_details) \
|
||||
__ENUMERATE_ARIA_ATTRIBUTE(aria_error_message_elements, aria_error_message) \
|
||||
__ENUMERATE_ARIA_ATTRIBUTE(aria_flow_to_elements, aria_flow_to) \
|
||||
__ENUMERATE_ARIA_ATTRIBUTE(aria_labelled_by_elements, aria_labelled_by) \
|
||||
__ENUMERATE_ARIA_ATTRIBUTE(aria_owns_elements, aria_owns)
|
||||
|
||||
class ARIAMixin {
|
||||
public:
|
||||
virtual ~ARIAMixin();
|
||||
|
@ -51,6 +60,12 @@ public:
|
|||
GC::Ptr<DOM::Element> aria_active_descendant_element() { return m_aria_active_descendant_element; }
|
||||
void set_aria_active_descendant_element(GC::Ptr<DOM::Element> value) { m_aria_active_descendant_element = value; }
|
||||
|
||||
#define __ENUMERATE_ARIA_ATTRIBUTE(attribute, referencing_attribute) \
|
||||
Optional<Vector<WeakPtr<DOM::Element>>> const& attribute() const; \
|
||||
void set_##attribute(Optional<Vector<WeakPtr<DOM::Element>>> value);
|
||||
ENUMERATE_ARIA_ELEMENT_LIST_REFERENCING_ATTRIBUTES
|
||||
#undef __ENUMERATE_ARIA_ATTRIBUTE
|
||||
|
||||
protected:
|
||||
ARIAMixin();
|
||||
|
||||
|
@ -60,6 +75,11 @@ protected:
|
|||
|
||||
private:
|
||||
GC::Ptr<DOM::Element> m_aria_active_descendant_element;
|
||||
|
||||
#define __ENUMERATE_ARIA_ATTRIBUTE(attribute, referencing_attribute) \
|
||||
Optional<Vector<WeakPtr<DOM::Element>>> m_##attribute;
|
||||
ENUMERATE_ARIA_ELEMENT_LIST_REFERENCING_ATTRIBUTES
|
||||
#undef __ENUMERATE_ARIA_ATTRIBUTE
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -12,21 +12,28 @@ interface mixin ARIAMixin {
|
|||
[CEReactions] attribute DOMString? ariaColIndex;
|
||||
[CEReactions] attribute DOMString? ariaColIndexText;
|
||||
[CEReactions] attribute DOMString? ariaColSpan;
|
||||
[Reflect=aria-controls, CEReactions] attribute sequence<Element>? ariaControlsElements; // FIXME: Should `FrozenArray<Element>?`
|
||||
[CEReactions] attribute DOMString? ariaCurrent;
|
||||
[Reflect=aria-describedby, CEReactions] attribute sequence<Element>? ariaDescribedByElements; // FIXME: Should `FrozenArray<Element>?`
|
||||
[CEReactions] attribute DOMString? ariaDescription;
|
||||
[Reflect=aria-details, CEReactions] attribute sequence<Element>? ariaDetailsElements; // FIXME: Should `FrozenArray<Element>?`
|
||||
[CEReactions] attribute DOMString? ariaDisabled;
|
||||
[Reflect=aria-errormessage, CEReactions] attribute sequence<Element>? ariaErrorMessageElements; // FIXME: Should `FrozenArray<Element>?`
|
||||
[CEReactions] attribute DOMString? ariaExpanded;
|
||||
[Reflect=aria-flowto, CEReactions] attribute sequence<Element>? ariaFlowToElements; // FIXME: Should `FrozenArray<Element>?`
|
||||
[CEReactions] attribute DOMString? ariaHasPopup;
|
||||
[CEReactions] attribute DOMString? ariaHidden;
|
||||
[CEReactions] attribute DOMString? ariaInvalid;
|
||||
[CEReactions] attribute DOMString? ariaKeyShortcuts;
|
||||
[CEReactions] attribute DOMString? ariaLabel;
|
||||
[Reflect=aria-labelledby, CEReactions] attribute sequence<Element>? ariaLabelledByElements; // FIXME: Should `FrozenArray<Element>?`
|
||||
[CEReactions] attribute DOMString? ariaLevel;
|
||||
[CEReactions] attribute DOMString? ariaLive;
|
||||
[CEReactions] attribute DOMString? ariaModal;
|
||||
[CEReactions] attribute DOMString? ariaMultiLine;
|
||||
[CEReactions] attribute DOMString? ariaMultiSelectable;
|
||||
[CEReactions] attribute DOMString? ariaOrientation;
|
||||
[Reflect=aria-owns, CEReactions] attribute sequence<Element>? ariaOwnsElements; // FIXME: Should `FrozenArray<Element>?`
|
||||
[CEReactions] attribute DOMString? ariaPlaceholder;
|
||||
[CEReactions] attribute DOMString? ariaPosInSet;
|
||||
[CEReactions] attribute DOMString? ariaPressed;
|
||||
|
|
|
@ -430,6 +430,60 @@ Vector<String> Element::get_attribute_names() const
|
|||
return names;
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#attr-associated-elements
|
||||
Optional<GC::RootVector<GC::Ref<DOM::Element>>> Element::get_the_attribute_associated_elements(FlyString const& content_attribute, Optional<Vector<WeakPtr<DOM::Element>>> const& explicitly_set_attribute_elements) const
|
||||
{
|
||||
// 1. Let elements be an empty list.
|
||||
GC::RootVector<GC::Ref<DOM::Element>> elements(heap());
|
||||
|
||||
// 2. Let element be the result of running reflectedTarget's get the element.
|
||||
auto const& element = *this;
|
||||
|
||||
// 3. If reflectedTarget's explicitly set attr-elements is not null:
|
||||
if (explicitly_set_attribute_elements.has_value()) {
|
||||
// 1. For each attrElement in reflectedTarget's explicitly set attr-elements:
|
||||
for (auto const& attribute_element : *explicitly_set_attribute_elements) {
|
||||
// 1. If attrElement is not a descendant of any of element's shadow-including ancestors, then continue.
|
||||
if (!attribute_element || &attribute_element->root() != &element.shadow_including_root())
|
||||
continue;
|
||||
|
||||
// 2. Append attrElement to elements.
|
||||
elements.append(*attribute_element);
|
||||
}
|
||||
}
|
||||
// 4. Otherwise:
|
||||
else {
|
||||
// 1. Let contentAttributeValue be the result of running reflectedTarget's get the content attribute.
|
||||
auto content_attribute_value = element.get_attribute(content_attribute);
|
||||
|
||||
// 2. If contentAttributeValue is null, then return null.
|
||||
if (!content_attribute_value.has_value())
|
||||
return {};
|
||||
|
||||
// 3. Let tokens be contentAttributeValue, split on ASCII whitespace.
|
||||
auto tokens = content_attribute_value->bytes_as_string_view().split_view_if(Infra::is_ascii_whitespace);
|
||||
|
||||
// 4. For each id of tokens:
|
||||
for (auto id : tokens) {
|
||||
// 1. Let candidate be the first element, in tree order, that meets the following criteria:
|
||||
// * candidate's root is the same as element's root;
|
||||
// * candidate's ID is id; and
|
||||
// * candidate implements T.
|
||||
auto candidate = element.document().get_element_by_id(MUST(FlyString::from_utf8(id)));
|
||||
|
||||
// 2. If no such element exists, then continue.
|
||||
if (!candidate)
|
||||
continue;
|
||||
|
||||
// 3. Append candidate to elements.
|
||||
elements.append(*candidate);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Return elements.
|
||||
return elements;
|
||||
}
|
||||
|
||||
GC::Ptr<Layout::Node> Element::create_layout_node(GC::Ref<CSS::ComputedProperties> style)
|
||||
{
|
||||
if (local_name() == "noscript" && document().is_scripting_enabled())
|
||||
|
@ -3609,6 +3663,17 @@ void Element::attribute_changed(FlyString const& local_name, Optional<String> co
|
|||
// Set element's explicitly set attr-element to null.
|
||||
set_aria_active_descendant_element({});
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#reflecting-content-attributes-in-idl-attributes:concept-element-attributes-change-ext-2
|
||||
// 1. If localName is not attr or namespace is not null, then return.
|
||||
// 2. Set element's explicitly set attr-elements to null.
|
||||
#define __ENUMERATE_ARIA_ATTRIBUTE(attribute, referencing_attribute) \
|
||||
else if (local_name == ARIA::AttributeNames::referencing_attribute && !namespace_.has_value()) \
|
||||
{ \
|
||||
set_##attribute({}); \
|
||||
}
|
||||
ENUMERATE_ARIA_ELEMENT_LIST_REFERENCING_ATTRIBUTES
|
||||
#undef __ENUMERATE_ARIA_ATTRIBUTE
|
||||
}
|
||||
|
||||
auto Element::ensure_custom_element_reaction_queue() -> CustomElementReactionQueue&
|
||||
|
|
|
@ -163,6 +163,8 @@ public:
|
|||
GC::Ptr<Attr> get_attribute_node(FlyString const& name) const;
|
||||
GC::Ptr<Attr> get_attribute_node_ns(Optional<FlyString> const& namespace_, FlyString const& name) const;
|
||||
|
||||
Optional<GC::RootVector<GC::Ref<DOM::Element>>> get_the_attribute_associated_elements(FlyString const& content_attribute, Optional<Vector<WeakPtr<DOM::Element>>> const& explicitly_set_attribute_elements) const;
|
||||
|
||||
DOMTokenList* class_list();
|
||||
|
||||
WebIDL::ExceptionOr<GC::Ref<ShadowRoot>> attach_shadow(ShadowRootInit init);
|
||||
|
|
|
@ -172,6 +172,18 @@ static StringView sequence_storage_type_to_cpp_storage_type_name(SequenceStorage
|
|||
}
|
||||
}
|
||||
|
||||
static bool is_nullable_sequence_of_single_type(Type const& type, StringView type_name)
|
||||
{
|
||||
if (!type.is_nullable() || !type.is_sequence())
|
||||
return false;
|
||||
|
||||
auto const& parameters = type.as_parameterized().parameters();
|
||||
if (parameters.size() != 1)
|
||||
return false;
|
||||
|
||||
return parameters.first()->name() == type_name;
|
||||
}
|
||||
|
||||
CppType idl_type_name_to_cpp_type(Type const& type, Interface const& interface);
|
||||
|
||||
static ByteString union_type_to_variant(UnionType const& union_type, Interface const& interface)
|
||||
|
@ -1024,21 +1036,15 @@ static void generate_to_cpp(SourceGenerator& generator, ParameterType& parameter
|
|||
// 3. If method is undefined, throw a TypeError.
|
||||
// 4. Return the result of creating a sequence from V and method.
|
||||
|
||||
if (optional) {
|
||||
if (optional || parameter.type->is_nullable()) {
|
||||
auto sequence_cpp_type = idl_type_name_to_cpp_type(parameterized_type.parameters().first(), interface);
|
||||
sequence_generator.set("sequence.type", sequence_cpp_type.name);
|
||||
sequence_generator.set("sequence.storage_type", sequence_storage_type_to_cpp_storage_type_name(sequence_cpp_type.sequence_storage_type));
|
||||
|
||||
if (!optional_default_value.has_value()) {
|
||||
if (sequence_cpp_type.sequence_storage_type == IDL::SequenceStorageType::Vector) {
|
||||
sequence_generator.append(R"~~~(
|
||||
sequence_generator.append(R"~~~(
|
||||
Optional<@sequence.storage_type@<@sequence.type@>> @cpp_name@;
|
||||
)~~~");
|
||||
} else {
|
||||
sequence_generator.append(R"~~~(
|
||||
Optional<@sequence.storage_type@> @cpp_name@;
|
||||
)~~~");
|
||||
}
|
||||
} else {
|
||||
if (optional_default_value != "[]")
|
||||
TODO();
|
||||
|
@ -1054,9 +1060,15 @@ static void generate_to_cpp(SourceGenerator& generator, ParameterType& parameter
|
|||
}
|
||||
}
|
||||
|
||||
sequence_generator.append(R"~~~(
|
||||
if (optional) {
|
||||
sequence_generator.append(R"~~~(
|
||||
if (!@js_name@@js_suffix@.is_undefined()) {
|
||||
)~~~");
|
||||
} else {
|
||||
sequence_generator.append(R"~~~(
|
||||
if (!@js_name@@js_suffix@.is_nullish()) {
|
||||
)~~~");
|
||||
}
|
||||
}
|
||||
|
||||
sequence_generator.append(R"~~~(
|
||||
|
@ -1068,9 +1080,9 @@ static void generate_to_cpp(SourceGenerator& generator, ParameterType& parameter
|
|||
return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotIterable, @js_name@@js_suffix@.to_string_without_side_effects());
|
||||
)~~~");
|
||||
|
||||
parameterized_type.generate_sequence_from_iterable(sequence_generator, ByteString::formatted("{}{}", acceptable_cpp_name, optional ? "_non_optional" : ""), ByteString::formatted("{}{}", js_name, js_suffix), ByteString::formatted("{}{}_iterator_method{}", js_name, js_suffix, recursion_depth), interface, recursion_depth + 1);
|
||||
parameterized_type.generate_sequence_from_iterable(sequence_generator, ByteString::formatted("{}{}", acceptable_cpp_name, optional || parameter.type->is_nullable() ? "_non_optional" : ""), ByteString::formatted("{}{}", js_name, js_suffix), ByteString::formatted("{}{}_iterator_method{}", js_name, js_suffix, recursion_depth), interface, recursion_depth + 1);
|
||||
|
||||
if (optional) {
|
||||
if (optional || parameter.type->is_nullable()) {
|
||||
sequence_generator.append(R"~~~(
|
||||
@cpp_name@ = move(@cpp_name@_non_optional);
|
||||
}
|
||||
|
@ -4060,6 +4072,24 @@ JS_DEFINE_NATIVE_FUNCTION(@class_name@::@attribute.getter_callback@)
|
|||
}
|
||||
)~~~");
|
||||
|
||||
}
|
||||
// If a reflected IDL attribute has the type FrozenArray<T>?, where T is either Element or an interface that
|
||||
// inherits from Element, then with attr being the reflected content attribute name:
|
||||
// FIXME: Handle "an interface that inherits from Element".
|
||||
// FIXME: This should handle "FrozenArray" rather than "sequence".
|
||||
else if (is_nullable_sequence_of_single_type(attribute.type, "Element"sv)) {
|
||||
// 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<T>?.
|
||||
// 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);
|
||||
|
@ -4202,6 +4232,45 @@ JS_DEFINE_NATIVE_FUNCTION(@class_name@::@attribute.setter_callback@)
|
|||
// 3. Set this's explicitly set attr-element to a weak reference to the given value.
|
||||
attribute_generator.append(R"~~~(
|
||||
TRY(throw_dom_exception_if_needed(vm, [&] { return impl->set_@attribute.cpp_name@(cpp_value); }));
|
||||
)~~~");
|
||||
}
|
||||
// If a reflected IDL attribute has the type FrozenArray<T>?, where T is either Element or an interface
|
||||
// that inherits from Element, then with attr being the reflected content attribute name:
|
||||
// FIXME: Handle "an interface that inherits from Element".
|
||||
// FIXME: This should handle "FrozenArray" rather than "sequence".
|
||||
else if (is_nullable_sequence_of_single_type(attribute.type, "Element"sv)) {
|
||||
// 1. If the given value is null:
|
||||
// 1. Set this's explicitly set attr-elements to null.
|
||||
// 2. Run this's delete the content attribute.
|
||||
// 3. Return.
|
||||
attribute_generator.append(R"~~~(
|
||||
static auto content_attribute = "@attribute.reflect_name@"_fly_string;
|
||||
|
||||
if (!cpp_value.has_value()) {
|
||||
TRY(throw_dom_exception_if_needed(vm, [&] { return impl->set_@attribute.cpp_name@({}); }));
|
||||
impl->remove_attribute(content_attribute);
|
||||
return JS::js_undefined();
|
||||
}
|
||||
)~~~");
|
||||
|
||||
// 2. Run this's set the content attribute with the empty string.
|
||||
attribute_generator.append(R"~~~(
|
||||
MUST(impl->set_attribute(content_attribute, String {}));
|
||||
)~~~");
|
||||
|
||||
// 3. Let elements be an empty list.
|
||||
// 4. For each element in the given value:
|
||||
// 1. Append a weak reference to element to elements.
|
||||
// 5. Set this's explicitly set attr-elements to elements.
|
||||
attribute_generator.append(R"~~~(
|
||||
Vector<WeakPtr<DOM::Element>> elements;
|
||||
elements.ensure_capacity(cpp_value->size());
|
||||
|
||||
for (auto const& element : *cpp_value) {
|
||||
elements.unchecked_append(*element);
|
||||
}
|
||||
|
||||
TRY(throw_dom_exception_if_needed(vm, [&] { return impl->set_@attribute.cpp_name@(move(elements)); }));
|
||||
)~~~");
|
||||
} else if (attribute.type->is_nullable()) {
|
||||
attribute_generator.append(R"~~~(
|
||||
|
|
|
@ -2,32 +2,32 @@ Harness status: OK
|
|||
|
||||
Found 27 tests
|
||||
|
||||
16 Pass
|
||||
11 Fail
|
||||
22 Pass
|
||||
5 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
|
||||
Pass aria-activedescendant Setting an element reference that crosses into a shadow tree is disallowed, but setting one that is in a shadow inclusive ancestor is allowed.
|
||||
Fail aria-errormessage
|
||||
Pass aria-errormessage
|
||||
Pass ariaErrorMessageElement is not defined
|
||||
Fail aria-details
|
||||
Pass aria-details
|
||||
Pass aria-activedescendant Deleting a reflected element should return null for the IDL attribute and the content attribute will be empty.
|
||||
Pass aria-activedescendant Changing the ID of an element doesn't lose the reference.
|
||||
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.
|
||||
Fail aria-controls.
|
||||
Fail aria-describedby.
|
||||
Fail aria-flowto.
|
||||
Fail aria-owns.
|
||||
Pass aria-controls.
|
||||
Pass aria-describedby.
|
||||
Pass aria-flowto.
|
||||
Pass aria-owns.
|
||||
Fail shadow DOM behaviour for FrozenArray element reflection.
|
||||
Fail Moving explicitly set elements across shadow DOM boundaries.
|
||||
Fail Moving explicitly set elements around within the same scope, and removing from the DOM.
|
||||
Pass Moving explicitly set elements around within the same scope, and removing from the DOM.
|
||||
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.
|
||||
Pass Caching invariant different attributes.
|
||||
Pass Caching invariant different elements.
|
||||
Fail Passing values of the wrong type should throw a TypeError
|
||||
Fail Caching invariant different attributes.
|
||||
Fail Caching invariant different elements.
|
||||
Pass Passing values of the wrong type should throw a TypeError
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue