Title: [294878] trunk
Revision
294878
Author
[email protected]
Date
2022-05-26 09:01:38 -0700 (Thu, 26 May 2022)

Log Message

AX: Refactor implementation of AX object relationships.
https://bugs.webkit.org/show_bug.cgi?id=240842

Reviewed by Chris Fleizach and Tyler Wilcock.

Test: accessibility/grid-with-aria-owned-cells.html

Relationships between AX objects were being computed by the methods
AccessibilityObject::ariaElementsFromAttribute and
ariaElementsReferencedByAttribute, both of which performed walks of the DOM tree
to match ids between origin and target of a specific relationship, having a
significant performance impact when called repeatedly.
With this patch, these two methods are replaced with a single method,
AccessibilityObject::relatedObjects, that in turn calls
AXObjectCache::relatedObjectsFor. This AXObjectCache method computes and caches
all relationships in one walk of the DoM tree. The cache is updated when
relevant event notifications are received. This makes support of relationships between objects more efficient, and the code clearer.
The test added exercises this implementation of relationships via the aria-owns
attribute to relate table rows to their cells. The execution time of this test
before this change was estimated in one system at about 1.91s, and after the
change was reduced to 1.87s.

* Source/WebCore/accessibility/AXObjectCache.cpp:
(WebCore::AXObjectCache::objectsForIDs const):
(WebCore::AXObjectCache::handleActiveDescendantChanged):
(WebCore::AXObjectCache::handleAttributeChange):
(WebCore::AXObjectCache::relationAttributes):
(WebCore::AXObjectCache::symmetricRelation):
(WebCore::AXObjectCache::attributeToRelationType):
(WebCore::AXObjectCache::addRelation):
(WebCore::AXObjectCache::updateRelationsIfNeeded):
(WebCore::AXObjectCache::relatedObjectsFor):
* Source/WebCore/accessibility/AXObjectCache.h:
(WebCore::AXObjectCache::relationsNeedUpdate):
* Source/WebCore/accessibility/AccessibilityObject.cpp:
(WebCore::AccessibilityObject::relatedObjects const):
(WebCore::AccessibilityObject::activeDescendantOfObjects const):
(WebCore::AccessibilityObject::controlledObjects const):
(WebCore::AccessibilityObject::controllers const):
(WebCore::AccessibilityObject::describedByObjects const):
(WebCore::AccessibilityObject::descriptionForObjects const):
(WebCore::AccessibilityObject::detailedByObjects const):
(WebCore::AccessibilityObject::detailsForObjects const):
(WebCore::AccessibilityObject::errorMessageObjects const):
(WebCore::AccessibilityObject::errorMessageForObjects const):
(WebCore::AccessibilityObject::flowToObjects const):
(WebCore::AccessibilityObject::flowFromObjects const):
(WebCore::AccessibilityObject::labelledByObjects const):
(WebCore::AccessibilityObject::labelForObjects const):
(WebCore::AccessibilityObject::ownedObjects const):
(WebCore::AccessibilityObject::owners const):
(WebCore::AccessibilityObject::ariaElementsFromAttribute const): Deleted.
(WebCore::AccessibilityObject::ariaElementsReferencedByAttribute const): Deleted.
* Source/WebCore/accessibility/AccessibilityObject.h:
* Source/WebCore/accessibility/AccessibilityObjectInterface.h:
(WebCore::Accessibility::findRelatedObjectInAncestry):
* Source/WebCore/accessibility/AccessibilityRenderObject.cpp:
(WebCore::AccessibilityRenderObject::activeDescendant const):
* Source/WebCore/accessibility/AccessibilityTableCell.cpp:
(WebCore::AccessibilityTableCell::columnHeaders):
* Source/WebCore/accessibility/atspi/AccessibilityObjectAtspi.cpp:
(WebCore::AccessibilityObjectAtspi::relationMap const):
* Source/WebCore/accessibility/mac/WebAccessibilityObjectWrapperMac.mm:
(-[WebAccessibilityObjectWrapper accessibilityAttributeValue:]):
(-[WebAccessibilityObjectWrapper accessibilityArrayAttributeCount:]):
* LayoutTests/accessibility/grid-with-aria-owned-cells-expected.txt: Added.
* LayoutTests/accessibility/grid-with-aria-owned-cells.html: Added.

Canonical link: https://commits.webkit.org/251008@main

Modified Paths

Added Paths

Diff

Added: trunk/LayoutTests/accessibility/grid-with-aria-owned-cells-expected.txt (0 => 294878)


--- trunk/LayoutTests/accessibility/grid-with-aria-owned-cells-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/accessibility/grid-with-aria-owned-cells-expected.txt	2022-05-26 16:01:38 UTC (rev 294878)
@@ -0,0 +1,12 @@
+This tests that a grid can use aria-owns to establish parent-child relationships between its rows and cells, even when the cell elements are not descendants of the grid in the DOM hierarchy.
+PASS: row1.childAtIndex(0).isEqual(cell_1_1) === true
+PASS: cell_1_1.parentElement().isEqual(row1) === true
+PASS: row3.childAtIndex(13).isEqual(cell_3_14) === true
+PASS: cell_3_14.parentElement().isEqual(row3) === true
+PASS: row5.childAtIndex(27).isEqual(cell_5_28) === true
+PASS: cell_5_28.parentElement().isEqual(row5) === true
+
+PASS successfullyParsed is true
+
+TEST COMPLETE
+

Added: trunk/LayoutTests/accessibility/grid-with-aria-owned-cells.html (0 => 294878)


--- trunk/LayoutTests/accessibility/grid-with-aria-owned-cells.html	                        (rev 0)
+++ trunk/LayoutTests/accessibility/grid-with-aria-owned-cells.html	2022-05-26 16:01:38 UTC (rev 294878)
@@ -0,0 +1,197 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src=""
+<script src=""
+</head>
+<body>
+
+<div id="content">
+
+<div id="grid" role="grid">
+    <div id="row1" role="row" aria-owns="cell-1-1 cell-1-2 cell-1-3 cell-1-4 cell-1-5 cell-1-6 cell-1-7 cell-1-8 cell-1-9 cell-1-10 cell-1-11 cell-1-12 cell-1-13 cell-1-14 cell-1-15 cell-1-16 cell-1-17 cell-1-18 cell-1-19 cell-1-20 cell-1-21 cell-1-22 cell-1-23 cell-1-24 cell-1-25 cell-1-26 cell-1-27 cell-1-28"></div>
+    <div role="row" aria-owns="cell-2-1 cell-2-2 cell-2-3 cell-2-4 cell-2-5 cell-2-6 cell-2-7 cell-2-8 cell-2-9 cell-2-10 cell-2-11 cell-2-12 cell-2-13 cell-2-14 cell-2-15 cell-2-16 cell-2-17 cell-2-18 cell-2-19 cell-2-20 cell-2-21 cell-2-22 cell-2-23 cell-2-24 cell-2-25 cell-2-26 cell-2-27 cell-2-28"></div>
+    <div id="row3" role="row" aria-owns="cell-3-1 cell-3-2 cell-3-3 cell-3-4 cell-3-5 cell-3-6 cell-3-7 cell-3-8 cell-3-9 cell-3-10 cell-3-11 cell-3-12 cell-3-13 cell-3-14 cell-3-15 cell-3-16 cell-3-17 cell-3-18 cell-3-19 cell-3-20 cell-3-21 cell-3-22 cell-3-23 cell-3-24 cell-3-25 cell-3-26 cell-3-27 cell-3-28"></div>
+    <div role="row" aria-owns="cell-4-1 cell-4-2 cell-4-3 cell-4-4 cell-4-5 cell-4-6 cell-4-7 cell-4-8 cell-4-9 cell-4-10 cell-4-11 cell-4-12 cell-4-13 cell-4-14 cell-4-15 cell-4-16 cell-4-17 cell-4-18 cell-4-19 cell-4-20 cell-4-21 cell-4-22 cell-4-23 cell-4-24 cell-4-25 cell-4-26 cell-4-27 cell-4-28"></div>
+    <div id="row5" role="row" aria-owns="cell-5-1 cell-5-2 cell-5-3 cell-5-4 cell-5-5 cell-5-6 cell-5-7 cell-5-8 cell-5-9 cell-5-10 cell-5-11 cell-5-12 cell-5-13 cell-5-14 cell-5-15 cell-5-16 cell-5-17 cell-5-18 cell-5-19 cell-5-20 cell-5-21 cell-5-22 cell-5-23 cell-5-24 cell-5-25 cell-5-26 cell-5-27 cell-5-28"></div>
+</div>
+
+    <div class="row">
+        <span role="gridcell" class="cell" id="cell-1-1">Hello</span>
+        <span role="gridcell" class="cell" id="cell-1-2">World</span>
+        <span role="gridcell" class="cell" id="cell-1-3">Hello</span>
+        <span role="gridcell" class="cell" id="cell-1-4">World</span>
+        <span role="gridcell" class="cell" id="cell-1-5">Hello</span>
+        <span role="gridcell" class="cell" id="cell-1-6">World</span>
+        <span role="gridcell" class="cell" id="cell-1-7">Hello</span>
+        <span role="gridcell" class="cell" id="cell-1-8">World</span>
+        <span role="gridcell" class="cell" id="cell-1-9">Hello</span>
+        <span role="gridcell" class="cell" id="cell-1-10">World</span>
+        <span role="gridcell" class="cell" id="cell-1-11">Hello</span>
+        <span role="gridcell" class="cell" id="cell-1-12">World</span>
+        <span role="gridcell" class="cell" id="cell-1-13">Hello</span>
+        <span role="gridcell" class="cell" id="cell-1-14">World</span>
+        <span role="gridcell" class="cell" id="cell-1-15">Hello</span>
+        <span role="gridcell" class="cell" id="cell-1-16">World</span>
+        <span role="gridcell" class="cell" id="cell-1-17">Hello</span>
+        <span role="gridcell" class="cell" id="cell-1-18">World</span>
+        <span role="gridcell" class="cell" id="cell-1-19">Hello</span>
+        <span role="gridcell" class="cell" id="cell-1-20">Hello</span>
+        <span role="gridcell" class="cell" id="cell-1-21">Hello</span>
+        <span role="gridcell" class="cell" id="cell-1-22">Hello</span>
+        <span role="gridcell" class="cell" id="cell-1-23">Hello</span>
+        <span role="gridcell" class="cell" id="cell-1-24">Hello</span>
+        <span role="gridcell" class="cell" id="cell-1-25">Hello</span>
+        <span role="gridcell" class="cell" id="cell-1-26">Hello</span>
+        <span role="gridcell" class="cell" id="cell-1-27">Hello</span>
+        <span role="gridcell" class="cell" id="cell-1-28">Hello</span>
+    </div>
+    <div class="row">
+        <span role="gridcell" class="cell" id="cell-2-1">Hello</span>
+        <span role="gridcell" class="cell" id="cell-2-2">World</span>
+        <span role="gridcell" class="cell" id="cell-2-3">Hello</span>
+        <span role="gridcell" class="cell" id="cell-2-4">World</span>
+        <span role="gridcell" class="cell" id="cell-2-5">Hello</span>
+        <span role="gridcell" class="cell" id="cell-2-6">World</span>
+        <span role="gridcell" class="cell" id="cell-2-7">Hello</span>
+        <span role="gridcell" class="cell" id="cell-2-8">World</span>
+        <span role="gridcell" class="cell" id="cell-2-9">Hello</span>
+        <span role="gridcell" class="cell" id="cell-2-10">World</span>
+        <span role="gridcell" class="cell" id="cell-2-11">Hello</span>
+        <span role="gridcell" class="cell" id="cell-2-12">World</span>
+        <span role="gridcell" class="cell" id="cell-2-13">Hello</span>
+        <span role="gridcell" class="cell" id="cell-2-14">World</span>
+        <span role="gridcell" class="cell" id="cell-2-15">Hello</span>
+        <span role="gridcell" class="cell" id="cell-2-16">World</span>
+        <span role="gridcell" class="cell" id="cell-2-17">Hello</span>
+        <span role="gridcell" class="cell" id="cell-2-18">World</span>
+        <span role="gridcell" class="cell" id="cell-2-19">Hello</span>
+        <span role="gridcell" class="cell" id="cell-2-20">Hello</span>
+        <span role="gridcell" class="cell" id="cell-2-21">Hello</span>
+        <span role="gridcell" class="cell" id="cell-2-22">Hello</span>
+        <span role="gridcell" class="cell" id="cell-2-23">Hello</span>
+        <span role="gridcell" class="cell" id="cell-2-24">Hello</span>
+        <span role="gridcell" class="cell" id="cell-2-25">Hello</span>
+        <span role="gridcell" class="cell" id="cell-2-26">Hello</span>
+        <span role="gridcell" class="cell" id="cell-2-27">Hello</span>
+        <span role="gridcell" class="cell" id="cell-2-28">Hello</span>
+    </div>
+    <div class="row">
+        <span role="gridcell" class="cell" id="cell-3-1">Hello</span>
+        <span role="gridcell" class="cell" id="cell-3-2">World</span>
+        <span role="gridcell" class="cell" id="cell-3-3">Hello</span>
+        <span role="gridcell" class="cell" id="cell-3-4">World</span>
+        <span role="gridcell" class="cell" id="cell-3-5">Hello</span>
+        <span role="gridcell" class="cell" id="cell-3-6">World</span>
+        <span role="gridcell" class="cell" id="cell-3-7">Hello</span>
+        <span role="gridcell" class="cell" id="cell-3-8">World</span>
+        <span role="gridcell" class="cell" id="cell-3-9">Hello</span>
+        <span role="gridcell" class="cell" id="cell-3-10">World</span>
+        <span role="gridcell" class="cell" id="cell-3-11">Hello</span>
+        <span role="gridcell" class="cell" id="cell-3-12">World</span>
+        <span role="gridcell" class="cell" id="cell-3-13">Hello</span>
+        <span role="gridcell" class="cell" id="cell-3-14">World</span>
+        <span role="gridcell" class="cell" id="cell-3-15">Hello</span>
+        <span role="gridcell" class="cell" id="cell-3-16">World</span>
+        <span role="gridcell" class="cell" id="cell-3-17">Hello</span>
+        <span role="gridcell" class="cell" id="cell-3-18">World</span>
+        <span role="gridcell" class="cell" id="cell-3-19">Hello</span>
+        <span role="gridcell" class="cell" id="cell-3-20">Hello</span>
+        <span role="gridcell" class="cell" id="cell-3-21">Hello</span>
+        <span role="gridcell" class="cell" id="cell-3-22">Hello</span>
+        <span role="gridcell" class="cell" id="cell-3-23">Hello</span>
+        <span role="gridcell" class="cell" id="cell-3-24">Hello</span>
+        <span role="gridcell" class="cell" id="cell-3-25">Hello</span>
+        <span role="gridcell" class="cell" id="cell-3-26">Hello</span>
+        <span role="gridcell" class="cell" id="cell-3-27">Hello</span>
+        <span role="gridcell" class="cell" id="cell-3-28">Hello</span>
+    </div>
+    <div class="row">
+        <span role="gridcell" class="cell" id="cell-4-1">Hello</span>
+        <span role="gridcell" class="cell" id="cell-4-2">World</span>
+        <span role="gridcell" class="cell" id="cell-4-3">Hello</span>
+        <span role="gridcell" class="cell" id="cell-4-4">World</span>
+        <span role="gridcell" class="cell" id="cell-4-5">Hello</span>
+        <span role="gridcell" class="cell" id="cell-4-6">World</span>
+        <span role="gridcell" class="cell" id="cell-4-7">Hello</span>
+        <span role="gridcell" class="cell" id="cell-4-8">World</span>
+        <span role="gridcell" class="cell" id="cell-4-9">Hello</span>
+        <span role="gridcell" class="cell" id="cell-4-10">World</span>
+        <span role="gridcell" class="cell" id="cell-4-11">Hello</span>
+        <span role="gridcell" class="cell" id="cell-4-12">World</span>
+        <span role="gridcell" class="cell" id="cell-4-13">Hello</span>
+        <span role="gridcell" class="cell" id="cell-4-14">World</span>
+        <span role="gridcell" class="cell" id="cell-4-15">Hello</span>
+        <span role="gridcell" class="cell" id="cell-4-16">World</span>
+        <span role="gridcell" class="cell" id="cell-4-17">Hello</span>
+        <span role="gridcell" class="cell" id="cell-4-18">World</span>
+        <span role="gridcell" class="cell" id="cell-4-19">Hello</span>
+        <span role="gridcell" class="cell" id="cell-4-20">Hello</span>
+        <span role="gridcell" class="cell" id="cell-4-21">Hello</span>
+        <span role="gridcell" class="cell" id="cell-4-22">Hello</span>
+        <span role="gridcell" class="cell" id="cell-4-23">Hello</span>
+        <span role="gridcell" class="cell" id="cell-4-24">Hello</span>
+        <span role="gridcell" class="cell" id="cell-4-25">Hello</span>
+        <span role="gridcell" class="cell" id="cell-4-26">Hello</span>
+        <span role="gridcell" class="cell" id="cell-4-27">Hello</span>
+        <span role="gridcell" class="cell" id="cell-4-28">Hello</span>
+    </div>
+    <div class="row">
+        <span role="gridcell" class="cell" id="cell-5-1">Hello</span>
+        <span role="gridcell" class="cell" id="cell-5-2">World</span>
+        <span role="gridcell" class="cell" id="cell-5-3">Hello</span>
+        <span role="gridcell" class="cell" id="cell-5-4">World</span>
+        <span role="gridcell" class="cell" id="cell-5-5">Hello</span>
+        <span role="gridcell" class="cell" id="cell-5-6">World</span>
+        <span role="gridcell" class="cell" id="cell-5-7">Hello</span>
+        <span role="gridcell" class="cell" id="cell-5-8">World</span>
+        <span role="gridcell" class="cell" id="cell-5-9">Hello</span>
+        <span role="gridcell" class="cell" id="cell-5-10">World</span>
+        <span role="gridcell" class="cell" id="cell-5-11">Hello</span>
+        <span role="gridcell" class="cell" id="cell-5-12">World</span>
+        <span role="gridcell" class="cell" id="cell-5-13">Hello</span>
+        <span role="gridcell" class="cell" id="cell-5-14">World</span>
+        <span role="gridcell" class="cell" id="cell-5-15">Hello</span>
+        <span role="gridcell" class="cell" id="cell-5-16">World</span>
+        <span role="gridcell" class="cell" id="cell-5-17">Hello</span>
+        <span role="gridcell" class="cell" id="cell-5-18">World</span>
+        <span role="gridcell" class="cell" id="cell-5-19">Hello</span>
+        <span role="gridcell" class="cell" id="cell-5-20">Hello</span>
+        <span role="gridcell" class="cell" id="cell-5-21">Hello</span>
+        <span role="gridcell" class="cell" id="cell-5-22">Hello</span>
+        <span role="gridcell" class="cell" id="cell-5-23">Hello</span>
+        <span role="gridcell" class="cell" id="cell-5-24">Hello</span>
+        <span role="gridcell" class="cell" id="cell-5-25">Hello</span>
+        <span role="gridcell" class="cell" id="cell-5-26">Hello</span>
+        <span role="gridcell" class="cell" id="cell-5-27">Hello</span>
+        <span role="gridcell" class="cell" id="cell-5-28">Hello</span>
+    </div>
+
+</div>
+
+<script>
+    if (window.accessibilityController) {
+        let output = "This tests that a grid can use aria-owns to establish parent-child relationships between its rows and cells, even when the cell elements are not descendants of the grid in the DOM hierarchy.\n";
+
+        var table = accessibilityController.accessibleElementById("grid");
+        var row1 = accessibilityController.accessibleElementById("row1");
+        var cell_1_1 = table.cellForColumnAndRow(0, 0);
+        output += expect("row1.childAtIndex(0).isEqual(cell_1_1)", "true");
+        output += expect("cell_1_1.parentElement().isEqual(row1)", "true");
+
+        var row3 = accessibilityController.accessibleElementById("row3");
+        var cell_3_14 = table.cellForColumnAndRow(13, 2);
+        output += expect("row3.childAtIndex(13).isEqual(cell_3_14)", "true");
+        output += expect("cell_3_14.parentElement().isEqual(row3)", "true");
+
+        var row5 = accessibilityController.accessibleElementById("row5");
+        var cell_5_28 = table.cellForColumnAndRow(27, 4);
+        output += expect("row5.childAtIndex(27).isEqual(cell_5_28)", "true");
+        output += expect("cell_5_28.parentElement().isEqual(row5)", "true");
+
+        document.getElementById("content").style.visibility = "hidden";
+        debug(output);
+    }
+</script>
+</body>
+</html>

Modified: trunk/LayoutTests/platform/win/TestExpectations (294877 => 294878)


--- trunk/LayoutTests/platform/win/TestExpectations	2022-05-26 14:00:35 UTC (rev 294877)
+++ trunk/LayoutTests/platform/win/TestExpectations	2022-05-26 16:01:38 UTC (rev 294878)
@@ -943,6 +943,7 @@
 # TODO ARIA 1.1 table related attributes are not supported
 webkit.org/b/148967 accessibility/aria-table-attributes.html [ Skip ]
 accessibility/aria-grid-with-aria-owns-rows.html [ Skip ]
+accessibility/grid-with-aria-owned-cells.html [ Skip ]
 
 # TODO webkit-font-smoothing is not supported.
 webkit.org/b/149245 imported/w3c/web-platform-tests/css/css-multicol/multicol-basic-001.html [ Skip ]

Modified: trunk/Source/WebCore/accessibility/AXObjectCache.cpp (294877 => 294878)


--- trunk/Source/WebCore/accessibility/AXObjectCache.cpp	2022-05-26 14:00:35 UTC (rev 294877)
+++ trunk/Source/WebCore/accessibility/AXObjectCache.cpp	2022-05-26 16:01:38 UTC (rev 294878)
@@ -116,7 +116,9 @@
 #include "TextIterator.h"
 #include <utility>
 #include <wtf/DataLog.h>
+#include <wtf/NeverDestroyed.h>
 #include <wtf/SetForScope.h>
+#include <wtf/text/AtomString.h>
 
 #if COMPILER(MSVC)
 // See https://msdn.microsoft.com/en-us/library/1wea5zwe.aspx
@@ -948,7 +950,7 @@
 {
     ASSERT(isMainThread());
 
-    return axIDs.map([this] (AXID axID) -> RefPtr<AXCoreObject> {
+    return axIDs.map([this] (const auto& axID) -> RefPtr<AXCoreObject> {
         ASSERT(axID.isValid());
         return objectFromAXID(axID);
     });
@@ -1839,9 +1841,9 @@
 #if PLATFORM(COCOA)
         // If the combobox's activeDescendant is inside a descendant owned or controlled by the combobox, that descendant should be the target of the notification and not the combobox itself.
         if (object->isComboBox()) {
-            if (auto* ownedObject = Accessibility::findRelatedObjectInAncestry(*object, aria_ownsAttr, *activeDescendant))
+            if (auto* ownedObject = Accessibility::findRelatedObjectInAncestry(*object, AXRelationType::OwnerFor, *activeDescendant))
                 target = ownedObject;
-            else if (auto* controlledObject = Accessibility::findRelatedObjectInAncestry(*object, aria_controlsAttr, *activeDescendant))
+            else if (auto* controlledObject = Accessibility::findRelatedObjectInAncestry(*object, AXRelationType::ControllerFor, *activeDescendant))
                 target = controlledObject;
         }
 #endif
@@ -1889,12 +1891,15 @@
 
     return false;
 }
-    
+
 void AXObjectCache::handleAttributeChange(const QualifiedName& attrName, Element* element)
 {
     if (!shouldProcessAttributeChange(attrName, element))
         return;
 
+    if (relationAttributes().contains(attrName))
+        relationsNeedUpdate(true);
+
     if (attrName == roleAttr) {
         if (auto* axObject = get(element))
             axObject->updateRole();
@@ -1910,8 +1915,10 @@
 #if ENABLE(ACCESSIBILITY_ISOLATED_TREE)
     else if (attrName == langAttr)
         updateIsolatedTree(get(element), AXObjectCache::AXLanguageChanged);
-    else if (attrName == idAttr)
+    else if (attrName == idAttr) {
+        relationsNeedUpdate(true);
         updateIsolatedTree(get(element), AXObjectCache::AXIdAttributeChanged);
+    }
 #endif
     else if (attrName == openAttr && is<HTMLDialogElement>(*element)) {
         deferModalChange(element);
@@ -3430,7 +3437,6 @@
 void AXObjectCache::updateIsolatedTree(const Vector<std::pair<RefPtr<AXCoreObject>, AXNotification>>& notifications)
 {
     AXTRACE("AXObjectCache::updateIsolatedTree"_s);
-    AXLOG(*this);
 
     if (!m_pageID) {
         AXLOG("No pageID.");
@@ -3657,6 +3663,191 @@
     return data;
 }
 
+Vector<QualifiedName>& AXObjectCache::relationAttributes()
+{
+    static NeverDestroyed<Vector<QualifiedName>> relationAttributes = Vector<QualifiedName> {
+        aria_activedescendantAttr,
+        aria_controlsAttr,
+        aria_describedbyAttr,
+        aria_detailsAttr,
+        aria_errormessageAttr,
+        aria_flowtoAttr,
+        aria_labelledbyAttr,
+        aria_labeledbyAttr,
+        aria_ownsAttr,
+        headersAttr,
+    };
+    return relationAttributes;
+}
+
+AXRelationType AXObjectCache::symmetricRelation(AXRelationType relationType)
+{
+    switch (relationType) {
+    case AXRelationType::ActiveDescendant:
+        return AXRelationType::ActiveDescendantOf;
+    case AXRelationType::ActiveDescendantOf:
+        return AXRelationType::ActiveDescendant;
+    case AXRelationType::ControlledBy:
+        return AXRelationType::ControllerFor;
+    case AXRelationType::ControllerFor:
+        return AXRelationType::ControlledBy;
+    case AXRelationType::DescribedBy:
+        return AXRelationType::DescriptionFor;
+    case AXRelationType::DescriptionFor:
+        return AXRelationType::DescribedBy;
+    case AXRelationType::Details:
+        return AXRelationType::DetailsFor;
+    case AXRelationType::DetailsFor:
+        return AXRelationType::Details;
+    case AXRelationType::ErrorMessage:
+        return AXRelationType::ErrorMessageFor;
+    case AXRelationType::ErrorMessageFor:
+        return AXRelationType::ErrorMessage;
+    case AXRelationType::FlowsFrom:
+        return AXRelationType::FlowsTo;
+    case AXRelationType::FlowsTo:
+        return AXRelationType::FlowsFrom;
+    case AXRelationType::Headers:
+        return AXRelationType::HeaderFor;
+    case AXRelationType::HeaderFor:
+        return AXRelationType::Headers;
+    case AXRelationType::LabelledBy:
+        return AXRelationType::LabelFor;
+    case AXRelationType::LabelFor:
+        return AXRelationType::LabelledBy;
+    case AXRelationType::OwnedBy:
+        return AXRelationType::OwnerFor;
+    case AXRelationType::OwnerFor:
+        return AXRelationType::OwnedBy;
+    case AXRelationType::None:
+        return AXRelationType::None;
+    }
+}
+
+AXRelationType AXObjectCache::attributeToRelationType(const QualifiedName& attribute)
+{
+    if (attribute == aria_activedescendantAttr)
+        return AXRelationType::ActiveDescendant;
+    if (attribute == aria_controlsAttr)
+        return AXRelationType::ControllerFor;
+    if (attribute == aria_describedbyAttr)
+        return AXRelationType::DescribedBy;
+    if (attribute == aria_detailsAttr)
+        return AXRelationType::Details;
+    if (attribute == aria_errormessageAttr)
+        return AXRelationType::ErrorMessage;
+    if (attribute == aria_flowtoAttr)
+        return AXRelationType::FlowsTo;
+    if (attribute == aria_labelledbyAttr || attribute == aria_labeledbyAttr)
+        return AXRelationType::LabelledBy;
+    if (attribute == aria_ownsAttr)
+        return AXRelationType::OwnerFor;
+    if (attribute == headersAttr)
+        return AXRelationType::Headers;
+    return AXRelationType::None;
+}
+
+void AXObjectCache::addRelation(Element* origin, Element* target, AXRelationType relationType)
+{
+    if (!origin || !target || origin == target || relationType == AXRelationType::None) {
+        ASSERT_NOT_REACHED();
+        return;
+    }
+    addRelation(getOrCreate(origin), getOrCreate(target), relationType);
+}
+
+void AXObjectCache::addRelation(AccessibilityObject* origin, AccessibilityObject* target, AXRelationType relationType, AddingSymmetricRelation addingSymmetricRelation)
+{
+    if (!origin || !target || origin == target || relationType == AXRelationType::None)
+        return;
+
+    auto relationsIterator = m_relations.find(origin->objectID());
+    if (relationsIterator == m_relations.end()) {
+        // No relations for this object, add the first one.
+        m_relations.add(origin->objectID(), Relations { { static_cast<uint8_t>(relationType), { target->objectID() } } });
+    } else if (auto targetsIterator = relationsIterator->value.find(static_cast<uint8_t>(relationType)); targetsIterator == relationsIterator->value.end()) {
+        // No relation of this type for this object, add the first one.
+        relationsIterator->value.add(static_cast<uint8_t>(relationType), Vector<AXID> { target->objectID() });
+    } else {
+        // There are already relations of this type for the object. Add the new relation.
+        if (relationType == AXRelationType::ActiveDescendant
+            || relationType == AXRelationType::OwnedBy) {
+            // There should be only one active descendant and only one owner. Enforce that by removing any existing targets.
+            targetsIterator->value.clear();
+        }
+        targetsIterator->value.append(target->objectID());
+    }
+
+    if (addingSymmetricRelation == AddingSymmetricRelation::No) {
+        if (auto symmetric = symmetricRelation(relationType); symmetric != AXRelationType::None)
+            addRelation(target, origin, symmetric, AddingSymmetricRelation::Yes);
+    }
+}
+
+void AXObjectCache::updateRelationsIfNeeded()
+{
+    AXTRACE("AXObjectCache::updateRelationsIfNeeded"_s);
+
+    if (!m_relationsNeedUpdate)
+        return;
+    relationsNeedUpdate(false);
+    AXLOG("Updating relations.");
+    m_relations.clear();
+
+    struct RelationOrigin {
+        Element* originElement { nullptr };
+        AtomString targetID;
+        AXRelationType relationType;
+    };
+
+    struct RelationTarget {
+        Element* targetElement { nullptr };
+        AtomString targetID;
+    };
+
+    Vector<RelationOrigin> origins;
+    Vector<RelationTarget> targets;
+    for (auto& element : descendantsOfType<Element>(m_document.rootNode())) {
+        // Collect all possible origins, i.e., elements with non-empty relation attributes.
+        for (const auto& attribute : relationAttributes()) {
+            auto& idsString = element.attributeWithoutSynchronization(attribute);
+            SpaceSplitString ids(idsString, SpaceSplitString::ShouldFoldCase::No);
+            for (size_t i = 0; i < ids.size(); ++i)
+                origins.append({ &element, ids[i], attributeToRelationType(attribute) });
+        }
+
+        // Collect all possible targets, i.e., elements with a non-empty id attribute.
+        auto elementID = element.attributeWithoutSynchronization(idAttr);
+        if (!elementID.isEmpty())
+            targets.append({ &element, elementID });
+    }
+
+    for (const auto& origin : origins) {
+        for (const auto& target : targets) {
+            if (origin.originElement == target.targetElement) {
+                // Relationship should be between different elements.
+                continue;
+            }
+
+            if (origin.targetID == target.targetID)
+                addRelation(origin.originElement, target.targetElement, origin.relationType);
+        }
+    }
+}
+
+std::optional<Vector<AXID>> AXObjectCache::relatedObjectsFor(const AXCoreObject& object, AXRelationType relationType)
+{
+    updateRelationsIfNeeded();
+    auto relationsIterator = m_relations.find(object.objectID());
+    if (relationsIterator == m_relations.end())
+        return std::nullopt;
+
+    auto targetsIterator = relationsIterator->value.find(static_cast<uint8_t>(relationType));
+    if (targetsIterator == relationsIterator->value.end())
+        return std::nullopt;
+    return targetsIterator->value;
+}
+
 AXAttributeCacheEnabler::AXAttributeCacheEnabler(AXObjectCache* cache)
     : m_cache(cache)
 {

Modified: trunk/Source/WebCore/accessibility/AXObjectCache.h (294877 => 294878)


--- trunk/Source/WebCore/accessibility/AXObjectCache.h	2022-05-26 14:00:35 UTC (rev 294877)
+++ trunk/Source/WebCore/accessibility/AXObjectCache.h	2022-05-26 16:01:38 UTC (rev 294878)
@@ -377,6 +377,9 @@
 
     AXTreeData treeData();
 
+    // Returns the IDs of the objects that relate to the given object with the specified relationship.
+    std::optional<Vector<AXID>> relatedObjectsFor(const AXCoreObject&, AXRelationType);
+
 #if ENABLE(ACCESSIBILITY_ISOLATED_TREE)
     WEBCORE_EXPORT static bool isIsolatedTreeEnabled();
     WEBCORE_EXPORT static bool usedOnAXThread();
@@ -487,6 +490,16 @@
     void handleModalChange(Element&);
     bool modalElementHasAccessibleContent(Element&);
 
+    // Relationships between objects.
+    static Vector<QualifiedName>& relationAttributes();
+    static AXRelationType attributeToRelationType(const QualifiedName&);
+    enum class AddingSymmetricRelation : bool { No, Yes };
+    static AXRelationType symmetricRelation(AXRelationType);
+    void addRelation(Element*, Element*, AXRelationType);
+    void addRelation(AccessibilityObject*, AccessibilityObject*, AXRelationType, AddingSymmetricRelation = AddingSymmetricRelation::No);
+    void updateRelationsIfNeeded();
+    void relationsNeedUpdate(bool needUpdate) { m_relationsNeedUpdate = needUpdate; }
+
     Document& m_document;
     const std::optional<PageIdentifier> m_pageID; // constant for object's lifetime.
     HashMap<AXID, RefPtr<AccessibilityObject>> m_objects;
@@ -542,6 +555,11 @@
     bool m_performingDeferredCacheUpdate { false };
     double m_loadingProgress { 0 };
 
+    // Relationships between objects.
+    using Relations = HashMap<AXRelationType, Vector<AXID>, DefaultHash<uint8_t>, WTF::UnsignedWithZeroKeyHashTraits<uint8_t>>;
+    HashMap<AXID, Relations> m_relations;
+    bool m_relationsNeedUpdate { true };
+
 #if USE(ATSPI)
     ListHashSet<RefPtr<AXCoreObject>> m_deferredParentChangedList;
 #endif

Modified: trunk/Source/WebCore/accessibility/AccessibilityObject.cpp (294877 => 294878)


--- trunk/Source/WebCore/accessibility/AccessibilityObject.cpp	2022-05-26 14:00:35 UTC (rev 294877)
+++ trunk/Source/WebCore/accessibility/AccessibilityObject.cpp	2022-05-26 16:01:38 UTC (rev 294878)
@@ -3922,38 +3922,16 @@
     return nullptr;
 }
 
-AXCoreObject::AccessibilityChildrenVector AccessibilityObject::ariaElementsFromAttribute(const QualifiedName& attribute) const
+AXCoreObject::AccessibilityChildrenVector AccessibilityObject::relatedObjects(AXRelationType relationType) const
 {
     auto* cache = axObjectCache();
     if (!cache)
         return { };
-    return elementsFromAttribute(attribute).map([cache] (auto* element) -> RefPtr<AXCoreObject> {
-        return cache->getOrCreate(element);
-    });
-}
 
-// FIXME: This function iterates the whole DOM tree and tries to match every Element in the tree, which is very expensive.
-// We should find a better way to achieve this.
-AXCoreObject::AccessibilityChildrenVector AccessibilityObject::ariaElementsReferencedByAttribute(const QualifiedName& attribute) const
-{
-    auto id = identifierAttribute();
-    if (id.isEmpty())
+    auto relatedObjectIDs = cache->relatedObjectsFor(*this, relationType);
+    if (!relatedObjectIDs)
         return { };
-
-    auto* cache = axObjectCache();
-    if (!cache)
-        return { };
-
-    AccessibilityChildrenVector objects;
-    for (auto& element : descendantsOfType<Element>(node()->treeScope().rootNode())) {
-        auto& idList = element.attributeWithoutSynchronization(attribute);
-        if (!SpaceSplitString::spaceSplitStringContainsValue(idList, id, SpaceSplitString::ShouldFoldCase::No))
-            continue;
-
-        if (auto* object = cache->getOrCreate(&element))
-            objects.append(object);
-    }
-    return objects;
+    return cache->objectsForIDs(*relatedObjectIDs);
 }
 
 bool AccessibilityObject::isActiveDescendantOfFocusedContainer() const
@@ -3969,83 +3947,77 @@
 
 AXCoreObject::AccessibilityChildrenVector AccessibilityObject::activeDescendantOfObjects() const
 {
-    return ariaElementsReferencedByAttribute(aria_activedescendantAttr);
+    return relatedObjects(AXRelationType::ActiveDescendantOf);
 }
 
 AXCoreObject::AccessibilityChildrenVector AccessibilityObject::controlledObjects() const
 {
-    return ariaElementsFromAttribute(aria_controlsAttr);
+    return relatedObjects(AXRelationType::ControllerFor);
 }
 
 AXCoreObject::AccessibilityChildrenVector AccessibilityObject::controllers() const
 {
-    return ariaElementsReferencedByAttribute(aria_controlsAttr);
+    return relatedObjects(AXRelationType::ControlledBy);
 }
 
 AXCoreObject::AccessibilityChildrenVector AccessibilityObject::describedByObjects() const
 {
-    return ariaElementsFromAttribute(aria_describedbyAttr);
+    return relatedObjects(AXRelationType::DescribedBy);
 }
 
 AXCoreObject::AccessibilityChildrenVector AccessibilityObject::descriptionForObjects() const
 {
-    return ariaElementsReferencedByAttribute(aria_describedbyAttr);
+    return relatedObjects(AXRelationType::DescriptionFor);
 }
 
 AXCoreObject::AccessibilityChildrenVector AccessibilityObject::detailedByObjects() const
 {
-    return ariaElementsFromAttribute(aria_detailsAttr);
+    return relatedObjects(AXRelationType::Details);
 }
 
 AXCoreObject::AccessibilityChildrenVector AccessibilityObject::detailsForObjects() const
 {
-    return ariaElementsReferencedByAttribute(aria_detailsAttr);
+    return relatedObjects(AXRelationType::DetailsFor);
 }
 
 AXCoreObject::AccessibilityChildrenVector AccessibilityObject::errorMessageObjects() const
 {
-    return ariaElementsFromAttribute(aria_errormessageAttr);
+    return relatedObjects(AXRelationType::ErrorMessage);
 }
 
 AXCoreObject::AccessibilityChildrenVector AccessibilityObject::errorMessageForObjects() const
 {
-    return ariaElementsReferencedByAttribute(aria_errormessageAttr);
+    return relatedObjects(AXRelationType::ErrorMessageFor);
 }
 
 AXCoreObject::AccessibilityChildrenVector AccessibilityObject::flowToObjects() const
 {
-    return ariaElementsFromAttribute(aria_flowtoAttr);
+    return relatedObjects(AXRelationType::FlowsTo);
 }
 
 AXCoreObject::AccessibilityChildrenVector AccessibilityObject::flowFromObjects() const
 {
-    return ariaElementsReferencedByAttribute(aria_flowtoAttr);
+    return relatedObjects(AXRelationType::FlowsFrom);
 }
 
 AXCoreObject::AccessibilityChildrenVector AccessibilityObject::labelledByObjects() const
 {
-    auto labelledByObjects = ariaElementsFromAttribute(aria_labelledbyAttr);
-    if (labelledByObjects.isEmpty())
-        labelledByObjects = ariaElementsFromAttribute(aria_labeledbyAttr);
-    return labelledByObjects;
+    return relatedObjects(AXRelationType::LabelledBy);
 }
 
 AXCoreObject::AccessibilityChildrenVector AccessibilityObject::labelForObjects() const
 {
-    auto objects = ariaElementsReferencedByAttribute(aria_labelledbyAttr);
-    if (objects.isEmpty())
-        objects = ariaElementsReferencedByAttribute(aria_labeledbyAttr);
-    return objects;
+    return relatedObjects(AXRelationType::LabelFor);
 }
 
 AXCoreObject::AccessibilityChildrenVector AccessibilityObject::ownedObjects() const
 {
-    return ariaElementsFromAttribute(aria_ownsAttr);
+    return relatedObjects(AXRelationType::OwnerFor);
 }
 
 AXCoreObject::AccessibilityChildrenVector AccessibilityObject::owners() const
 {
-    return ariaElementsReferencedByAttribute(aria_ownsAttr);
+    return relatedObjects(AXRelationType::OwnedBy);
 }
 
 void AccessibilityObject::setIsIgnoredFromParentDataForChild(AXCoreObject* child)

Modified: trunk/Source/WebCore/accessibility/AccessibilityObject.h (294877 => 294878)


--- trunk/Source/WebCore/accessibility/AccessibilityObject.h	2022-05-26 14:00:35 UTC (rev 294877)
+++ trunk/Source/WebCore/accessibility/AccessibilityObject.h	2022-05-26 16:01:38 UTC (rev 294878)
@@ -785,8 +785,7 @@
     String documentEncoding() const override;
     AccessibilityChildrenVector documentLinks() override { return AccessibilityChildrenVector(); }
 
-    AccessibilityChildrenVector ariaElementsFromAttribute(const QualifiedName&) const;
-    AccessibilityChildrenVector ariaElementsReferencedByAttribute(const QualifiedName&) const;
+    AccessibilityChildrenVector relatedObjects(AXRelationType) const;
 protected:
     AccessibilityObject() = default;
 

Modified: trunk/Source/WebCore/accessibility/AccessibilityObjectInterface.h (294877 => 294878)


--- trunk/Source/WebCore/accessibility/AccessibilityObjectInterface.h	2022-05-26 14:00:35 UTC (rev 294877)
+++ trunk/Source/WebCore/accessibility/AccessibilityObjectInterface.h	2022-05-26 16:01:38 UTC (rev 294878)
@@ -776,6 +776,29 @@
 enum class AccessibilityMathScriptObjectType { Subscript, Superscript };
 enum class AccessibilityMathMultiscriptObjectType { PreSubscript, PreSuperscript, PostSubscript, PostSuperscript };
 
+// Relationships between AX objects.
+enum class AXRelationType : uint8_t {
+    None,
+    ActiveDescendant,
+    ActiveDescendantOf,
+    ControlledBy,
+    ControllerFor,
+    DescribedBy,
+    DescriptionFor,
+    Details,
+    DetailsFor,
+    ErrorMessage,
+    ErrorMessageFor,
+    FlowsFrom,
+    FlowsTo,
+    Headers,
+    HeaderFor,
+    LabelledBy,
+    LabelFor,
+    OwnedBy,
+    OwnerFor,
+};
+
 // Use this struct to store the isIgnored data that depends on the parents, so that in addChildren()
 // we avoid going up the parent chain for each element while traversing the tree with useful information already.
 struct AccessibilityIsIgnoredFromParentData {
@@ -1617,9 +1640,9 @@
 }
 
 template<typename T>
-T* findRelatedObjectInAncestry(const T& object, const QualifiedName& relationAttribute, const T& descendant)
+T* findRelatedObjectInAncestry(const T& object, AXRelationType relationType, const T& descendant)
 {
-    auto relatedObjects = object.ariaElementsFromAttribute(relationAttribute);
+    auto relatedObjects = object.relatedObjects(relationType);
     for (const auto& object : relatedObjects) {
         auto* ancestor = findAncestor(descendant, false, [&object] (const auto& ancestor) {
             return object.get() == &ancestor;

Modified: trunk/Source/WebCore/accessibility/AccessibilityRenderObject.cpp (294877 => 294878)


--- trunk/Source/WebCore/accessibility/AccessibilityRenderObject.cpp	2022-05-26 14:00:35 UTC (rev 294877)
+++ trunk/Source/WebCore/accessibility/AccessibilityRenderObject.cpp	2022-05-26 16:01:38 UTC (rev 294878)
@@ -2681,7 +2681,7 @@
 
 AccessibilityObject* AccessibilityRenderObject::activeDescendant() const
 {
-    auto activeDescendants = ariaElementsFromAttribute(aria_activedescendantAttr);
+    auto activeDescendants = relatedObjects(AXRelationType::ActiveDescendant);
     ASSERT(activeDescendants.size() <= 1);
     if (!activeDescendants.isEmpty())
         return downcast<AccessibilityObject>(activeDescendants[0].get());

Modified: trunk/Source/WebCore/accessibility/AccessibilityTableCell.cpp (294877 => 294878)


--- trunk/Source/WebCore/accessibility/AccessibilityTableCell.cpp	2022-05-26 14:00:35 UTC (rev 294877)
+++ trunk/Source/WebCore/accessibility/AccessibilityTableCell.cpp	2022-05-26 16:01:38 UTC (rev 294878)
@@ -244,7 +244,7 @@
         return { };
 
     // Choose columnHeaders as the place where the "headers" attribute is reported.
-    auto headers = ariaElementsFromAttribute(headersAttr);
+    auto headers = relatedObjects(AXRelationType::Headers);
     // If the headers attribute returned valid values, then do not further search for column headers.
     if (!headers.isEmpty())
         return headers;

Modified: trunk/Source/WebCore/accessibility/atspi/AccessibilityObjectAtspi.cpp (294877 => 294878)


--- trunk/Source/WebCore/accessibility/atspi/AccessibilityObjectAtspi.cpp	2022-05-26 14:00:35 UTC (rev 294877)
+++ trunk/Source/WebCore/accessibility/atspi/AccessibilityObjectAtspi.cpp	2022-05-26 16:01:38 UTC (rev 294878)
@@ -1049,7 +1049,6 @@
         }
         if (!wrappers.isEmpty())
             map.add(relation, WTFMove(wrappers));
-
     };
 
     AccessibilityObject::AccessibilityChildrenVector ariaLabelledByElements;

Modified: trunk/Source/WebCore/accessibility/mac/WebAccessibilityObjectWrapperMac.mm (294877 => 294878)


--- trunk/Source/WebCore/accessibility/mac/WebAccessibilityObjectWrapperMac.mm	2022-05-26 14:00:35 UTC (rev 294877)
+++ trunk/Source/WebCore/accessibility/mac/WebAccessibilityObjectWrapperMac.mm	2022-05-26 16:01:38 UTC (rev 294878)
@@ -1932,7 +1932,6 @@
         return [self computedRoleString];
 
     if ([attributeName isEqualToString: NSAccessibilityParentAttribute]) {
-
         // This will return the parent of the AXWebArea, if this is a web area.
         id scrollViewParent = [self scrollViewParent];
         if (scrollViewParent)
@@ -4138,6 +4137,8 @@
 ALLOW_DEPRECATED_DECLARATIONS_BEGIN
 - (NSUInteger)accessibilityArrayAttributeCount:(NSString *)attribute
 {
+    AXTRACE(makeString("WebAccessibilityObjectWrapper accessibilityArrayAttributeCount:", String(attribute)));
+
     auto* backingObject = self.updateObjectBackingStore;
     if (!backingObject)
         return 0;
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to