Bug 1710751: Create LINKS_TO relation to track anchors and their corresponding elements r=eeejay
authorMorgan Reschenberg <mreschenberg@mozilla.com>
Wed, 26 May 2021 21:31:42 +0000
changeset 580804 13a48807371bcdd8dd74d4b83528da43e9429fc0
parent 580803 adceb058a4a15f1076c6ddeef2e870513674224c
child 580805 1afba5f05f3e895a4364b7ac9181070fd7126d94
push id38494
push userimoraru@mozilla.com
push dateThu, 27 May 2021 03:12:53 +0000
treeherdermozilla-central@257c3c51ab23 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerseeejay
bugs1710751
milestone90.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1710751: Create LINKS_TO relation to track anchors and their corresponding elements r=eeejay Differential Revision: https://phabricator.services.mozilla.com/D115841
accessible/base/RelationType.h
accessible/base/RelationTypeMap.h
accessible/generic/LocalAccessible.cpp
accessible/interfaces/nsIAccessibleRelation.idl
accessible/mac/mozHTMLAccessible.h
accessible/mac/mozHTMLAccessible.mm
accessible/tests/browser/mac/browser_link.js
accessible/windows/msaa/MsaaAccessible.h
other-licenses/atk-1.0/atk/atkrelationtype.h
other-licenses/ia2/AccessibleRelation.idl
--- a/accessible/base/RelationType.h
+++ b/accessible/base/RelationType.h
@@ -149,15 +149,20 @@ enum class RelationType {
    */
   ERRORMSG = 0x17,
 
   /**
    * This object is the error message for the target object.
    */
   ERRORMSG_FOR = 0x18,
 
-  LAST = ERRORMSG_FOR
+  /**
+   * The target object is the anchor referenced by this link.
+   */
+  LINKS_TO = 0x19,
+
+  LAST = LINKS_TO
 };
 
 }  // namespace a11y
 }  // namespace mozilla
 
 #endif
--- a/accessible/base/RelationTypeMap.h
+++ b/accessible/base/RelationTypeMap.h
@@ -80,8 +80,11 @@ RELATIONTYPE(DETAILS, "details", ATK_REL
 RELATIONTYPE(DETAILS_FOR, "details for", ATK_RELATION_DETAILS_FOR,
              NAVRELATION_DETAILS_FOR, IA2_RELATION_DETAILS_FOR)
 
 RELATIONTYPE(ERRORMSG, "error", ATK_RELATION_ERROR_MESSAGE, NAVRELATION_ERROR,
              IA2_RELATION_ERROR)
 
 RELATIONTYPE(ERRORMSG_FOR, "error for", ATK_RELATION_ERROR_FOR,
              NAVRELATION_ERROR_FOR, IA2_RELATION_ERROR_FOR)
+
+RELATIONTYPE(LINKS_TO, "links to", ATK_RELATION_LINKS_TO, NAVRELATION_LINKS_TO,
+             IA2_RELATION_LINKS_TO)
--- a/accessible/generic/LocalAccessible.cpp
+++ b/accessible/generic/LocalAccessible.cpp
@@ -33,16 +33,17 @@
 #include "nsIDOMXULButtonElement.h"
 #include "nsIDOMXULSelectCntrlEl.h"
 #include "nsIDOMXULSelectCntrlItemEl.h"
 #include "nsINodeList.h"
 #include "nsPIDOMWindow.h"
 
 #include "mozilla/dom/Document.h"
 #include "mozilla/dom/HTMLFormElement.h"
+#include "mozilla/dom/HTMLAnchorElement.h"
 #include "nsIContent.h"
 #include "nsIForm.h"
 #include "nsIFormControl.h"
 
 #include "nsDeckFrame.h"
 #include "nsLayoutUtils.h"
 #include "nsIStringBundle.h"
 #include "nsPresContext.h"
@@ -1793,16 +1794,54 @@ Relation LocalAccessible::RelationByType
          * relation here if the above check fails. */
 
         return rel;
       }
 
       return Relation(mDoc, GetAtomicRegion());
     }
 
+    case RelationType::LINKS_TO: {
+      Relation rel = Relation();
+      if (Role() == roles::LINK) {
+        dom::HTMLAnchorElement* anchor =
+            dom::HTMLAnchorElement::FromNode(mContent);
+        if (!anchor) {
+          return rel;
+        }
+        // If this node is an anchor element, query its hash to find the
+        // target.
+        nsAutoString hash;
+        anchor->GetHash(hash);
+        if (hash.IsEmpty()) {
+          return rel;
+        }
+
+        // GetHash returns an ID or name with a leading '#', trim it so we can
+        // search the doc by ID or name alone.
+        hash.Trim("#");
+        if (dom::Element* elm = mContent->OwnerDoc()->GetElementById(hash)) {
+          rel.AppendTarget(mDoc->GetAccessibleOrContainer(elm));
+        } else if (nsCOMPtr<nsINodeList> list =
+                       mContent->OwnerDoc()->GetElementsByName(hash)) {
+          // Loop through the named nodes looking for the first anchor
+          uint32_t length = list->Length();
+          for (uint32_t i = 0; i < length; i++) {
+            nsIContent* node = list->Item(i);
+            if (node->IsHTMLElement(nsGkAtoms::a)) {
+              rel.AppendTarget(mDoc->GetAccessibleOrContainer(node));
+              break;
+            }
+          }
+        }
+      }
+
+      return rel;
+    }
+
     case RelationType::SUBWINDOW_OF:
     case RelationType::EMBEDS:
     case RelationType::EMBEDDED_BY:
     case RelationType::POPUP_FOR:
     case RelationType::PARENT_WINDOW_OF:
       return Relation();
 
     case RelationType::DEFAULT_BUTTON: {
--- a/accessible/interfaces/nsIAccessibleRelation.idl
+++ b/accessible/interfaces/nsIAccessibleRelation.idl
@@ -151,16 +151,21 @@ interface nsIAccessibleRelation : nsISup
   const unsigned long RELATION_ERRORMSG = 0x17;
 
   /**
    * This object is the error message for the target object.
    */
   const unsigned long RELATION_ERRORMSG_FOR = 0x18;
 
   /**
+   * The target object is the anchor referenced by this link.
+   */
+  const unsigned long RELATION_LINKS_TO = 0x19;
+
+  /**
    * Returns the type of the relation.
    */
   readonly attribute unsigned long relationType;
 
   /**
    * Returns the number of targets for this relation.
    */
   readonly attribute unsigned long targetsCount;
--- a/accessible/mac/mozHTMLAccessible.h
+++ b/accessible/mac/mozHTMLAccessible.h
@@ -26,16 +26,19 @@
 - (NSString*)moxRole;
 
 // override
 - (NSURL*)moxURL;
 
 // override
 - (NSNumber*)moxVisited;
 
+// override
+- (NSArray*)moxLinkedUIElements;
+
 @end
 
 @interface MOXSummaryAccessible : mozAccessible
 
 // override
 - (NSNumber*)moxExpanded;
 
 @end
--- a/accessible/mac/mozHTMLAccessible.mm
+++ b/accessible/mac/mozHTMLAccessible.mm
@@ -73,16 +73,20 @@ using namespace mozilla::a11y;
   // the HTML Accessibility API Mappings spec says this should be an AXGroup.
   if (![self stateWithMask:states::LINKED]) {
     return NSAccessibilityGroupRole;
   }
 
   return [super moxRole];
 }
 
+- (NSArray*)moxLinkedUIElements {
+  return [self getRelationsByType:RelationType::LINKS_TO];
+}
+
 @end
 
 @implementation MOXSummaryAccessible
 
 - (NSNumber*)moxExpanded {
   return @([self stateWithMask:states::EXPANDED] != 0);
 }
 
--- a/accessible/tests/browser/mac/browser_link.js
+++ b/accessible/tests/browser/mac/browser_link.js
@@ -118,8 +118,102 @@ addAccessibleTask(
 
     ok(
       link3.attributeNames.includes("AXVisited"),
       "Link has visited attribute"
     );
     ok(link3.attributeNames.includes("AXURL"), "Link has URL attribute");
   }
 );
+
+/**
+ * Test anchors and linked ui elements attr
+ */
+addAccessibleTask(
+  `
+  <a id="link0" href="http://example.com">I am a link</a>
+  <a id="link1" href="#">I am a link with an empty anchor</a>
+  <a id="link2" href="#hello">I am a link with no corresponding element</a>
+  <a id="link3" href="#world">I am a link with a corresponding element</a>
+  <a id="link4" href="#empty">I jump to an empty element</a>
+  <a id="link5" href="#namedElem">I jump to a named element</a>
+  <a id="link6" href="#emptyNamed">I jump to an empty named element</a>
+  <h1 id="world">I am that element</h1>
+  <h2 id="empty"></h2>
+  <a name="namedElem">I have a name</a>
+  <a name="emptyNamed"></a>
+  <h3>I have no name and no ID</h3>
+  <h4></h4>
+  `,
+  async (browser, accDoc) => {
+    let link0 = getNativeInterface(accDoc, "link0");
+    let link1 = getNativeInterface(accDoc, "link1");
+    let link2 = getNativeInterface(accDoc, "link2");
+    let link3 = getNativeInterface(accDoc, "link3");
+    let link4 = getNativeInterface(accDoc, "link4");
+    let link5 = getNativeInterface(accDoc, "link5");
+    let link6 = getNativeInterface(accDoc, "link6");
+
+    is(
+      link0.getAttributeValue("AXLinkedUIElements").length,
+      0,
+      "Link 0 has no linked UI elements"
+    );
+    is(
+      link1.getAttributeValue("AXLinkedUIElements").length,
+      0,
+      "Link 1 has no linked UI elements"
+    );
+    is(
+      link2.getAttributeValue("AXLinkedUIElements").length,
+      0,
+      "Link 2 has no linked UI elements"
+    );
+    is(
+      link3.getAttributeValue("AXLinkedUIElements").length,
+      1,
+      "Link 3 has one linked UI element"
+    );
+    is(
+      link3
+        .getAttributeValue("AXLinkedUIElements")[0]
+        .getAttributeValue("AXTitle"),
+      "I am that element",
+      "Link 3 is linked to the heading"
+    );
+    is(
+      link4.getAttributeValue("AXLinkedUIElements").length,
+      1,
+      "Link 4 has one linked UI element"
+    );
+    is(
+      link4
+        .getAttributeValue("AXLinkedUIElements")[0]
+        .getAttributeValue("AXTitle"),
+      "",
+      "Link 4 is linked to the heading"
+    );
+    is(
+      link5.getAttributeValue("AXLinkedUIElements").length,
+      1,
+      "Link 5 has one linked UI element"
+    );
+    is(
+      link5
+        .getAttributeValue("AXLinkedUIElements")[0]
+        .getAttributeValue("AXTitle"),
+      "I have a name",
+      "Link 5 is linked to a named element"
+    );
+    is(
+      link6.getAttributeValue("AXLinkedUIElements").length,
+      1,
+      "Link 6 has one linked UI element"
+    );
+    is(
+      link6
+        .getAttributeValue("AXLinkedUIElements")[0]
+        .getAttributeValue("AXTitle"),
+      "",
+      "Link 6 is linked to an empty named element"
+    );
+  }
+);
--- a/accessible/windows/msaa/MsaaAccessible.h
+++ b/accessible/windows/msaa/MsaaAccessible.h
@@ -186,17 +186,18 @@ class MsaaAccessible : public ia2Accessi
     NAVRELATION_NODE_PARENT_OF = 0x1010,
     NAVRELATION_CONTAINING_DOCUMENT = 0x1011,
     NAVRELATION_CONTAINING_TAB_PANE = 0x1012,
     NAVRELATION_CONTAINING_WINDOW = 0x1013,
     NAVRELATION_CONTAINING_APPLICATION = 0x1014,
     NAVRELATION_DETAILS = 0x1015,
     NAVRELATION_DETAILS_FOR = 0x1016,
     NAVRELATION_ERROR = 0x1017,
-    NAVRELATION_ERROR_FOR = 0x1018
+    NAVRELATION_ERROR_FOR = 0x1018,
+    NAVRELATION_LINKS_TO = 0x1019
   };
 
  private:
   /**
    * Find a remote accessible by the given child ID.
    */
   [[nodiscard]] already_AddRefed<IAccessible> GetRemoteIAccessibleFor(
       const VARIANT& aVarChild);
--- a/other-licenses/atk-1.0/atk/atkrelationtype.h
+++ b/other-licenses/atk-1.0/atk/atkrelationtype.h
@@ -75,16 +75,17 @@ G_BEGIN_DECLS
  * %ATK_RELATION_DESCRIPTION_FOR and %ATK_RELATION_ERROR_FOR. @Since: ATK-2.26.
  *@ATK_RELATION_ERROR_MESSAGE: Reciprocal of %ATK_RELATION_ERROR_FOR. Indicates that this object
  * has one or more errors, the nature of which is described in the contents of the target
  * object(s). Objects that have this relation type should also contain %ATK_STATE_INVALID_ENTRY
  * in their #AtkStateSet. @Since: ATK-2.26.
  *@ATK_RELATION_ERROR_FOR: Reciprocal of %ATK_RELATION_ERROR_MESSAGE. Indicates that this object
  * contains an error message describing an invalid condition in the target object(s). @Since:
  * ATK_2.26.
+ *@ATK_RELATION_LINKS_TO: For an anchor link, indicates the element that the link is anchored to.
  *@ATK_RELATION_LAST_DEFINED: Not used, this value indicates the end of the enumeration.
  * 
  *Describes the type of the relation
  **/
 typedef enum
 {
   ATK_RELATION_NULL = 0,
   ATK_RELATION_CONTROLLED_BY,
@@ -102,14 +103,15 @@ typedef enum
   ATK_RELATION_PARENT_WINDOW_OF, 
   ATK_RELATION_DESCRIBED_BY,
   ATK_RELATION_DESCRIPTION_FOR,
   ATK_RELATION_NODE_PARENT_OF,
   ATK_RELATION_DETAILS,
   ATK_RELATION_DETAILS_FOR,
   ATK_RELATION_ERROR_MESSAGE,
   ATK_RELATION_ERROR_FOR,
+  ATK_RELATION_LINKS_TO,
   ATK_RELATION_LAST_DEFINED
 } AtkRelationType;
 
 G_END_DECLS
 
 #endif /* __ATK_RELATION_TYPE_H__ */
--- a/other-licenses/ia2/AccessibleRelation.idl
+++ b/other-licenses/ia2/AccessibleRelation.idl
@@ -167,16 +167,19 @@ const WCHAR *const IA2_RELATION_DETAILS 
 const WCHAR *const IA2_RELATION_DETAILS_FOR = L"detailsFor";
 
 /** The target object is the error message for this object. */
 const WCHAR *const IA2_RELATION_ERROR = L"error";
 
 /** This object is the error message for the target object. */
 const WCHAR *const IA2_RELATION_ERROR_FOR = L"errorFor";
 
+/** The target object is the anchor referenced by this link. */
+const WCHAR *const IA2_RELATION_LINKS_TO = L"linksTo";
+
 ///@}
 
 /** This interface gives access to an object's set of relations.
 */
 [object, uuid(7CDF86EE-C3DA-496a-BDA4-281B336E1FDC)]
 interface IAccessibleRelation : IUnknown
 {
   /** @brief Returns the type of the relation.