Commit d1c3206f authored by James Teh's avatar James Teh
Browse files

Bug 1829603 part 1: Include the class attribute in the parent process a11y cache. r=morgan

This is used by the Web Access NVDA add-on to identify and remediate some inaccessible elements on websites with accessibility problems.
It is also useful for developers using screen readers when trying to identify specific elements in the absence of semantics.

Differential Revision: https://phabricator.services.mozilla.com/D176896
parent 34df9a72
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -19,7 +19,7 @@ class CacheDomain {
  static constexpr uint64_t Bounds = ((uint64_t)0x1) << 2;
  static constexpr uint64_t Resolution = ((uint64_t)0x1) << 3;
  static constexpr uint64_t Text = ((uint64_t)0x1) << 4;
  static constexpr uint64_t DOMNodeID = ((uint64_t)0x1) << 5;
  static constexpr uint64_t DOMNodeIDAndClass = ((uint64_t)0x1) << 5;
  static constexpr uint64_t State = ((uint64_t)0x1) << 6;
  static constexpr uint64_t GroupInfo = ((uint64_t)0x1) << 7;
  static constexpr uint64_t Actions = ((uint64_t)0x1) << 8;
+1 −1
Original line number Diff line number Diff line
@@ -845,7 +845,7 @@ void DocAccessible::AttributeChanged(dom::Element* aElement,
    dom::Element* elm = accessible->Elm();
    RelocateARIAOwnedIfNeeded(elm);
    ARIAActiveDescendantIDMaybeMoved(accessible);
    accessible->SendCache(CacheDomain::DOMNodeID, CacheUpdateType::Update);
    QueueCacheUpdate(accessible, CacheDomain::DOMNodeIDAndClass);
    QueueCacheUpdateForDependentRelations(accessible);
  }

+15 −1
Original line number Diff line number Diff line
@@ -1292,6 +1292,11 @@ void LocalAccessible::DOMAttributeChanged(int32_t aNameSpaceID,
    }
  }

  if (aAttribute == nsGkAtoms::_class) {
    mDoc->QueueCacheUpdate(this, CacheDomain::DOMNodeIDAndClass);
    return;
  }

  // When a details object has its open attribute changed
  // we should fire a state-change event on the accessible of
  // its main summary
@@ -3522,13 +3527,22 @@ already_AddRefed<AccAttributes> LocalAccessible::BundleFieldsForCache(
    }
  }

  if (aCacheDomain & CacheDomain::DOMNodeID && mContent) {
  if (aCacheDomain & CacheDomain::DOMNodeIDAndClass && mContent) {
    nsAtom* id = mContent->GetID();
    if (id) {
      fields->SetAttribute(nsGkAtoms::id, id);
    } else if (aUpdateType == CacheUpdateType::Update) {
      fields->SetAttribute(nsGkAtoms::id, DeleteEntry());
    }
    if (auto* el = dom::Element::FromNodeOrNull(mContent)) {
      nsAutoString className;
      el->GetClassName(className);
      if (!className.IsEmpty()) {
        fields->SetAttribute(nsGkAtoms::_class, std::move(className));
      } else if (aUpdateType == CacheUpdateType::Update) {
        fields->SetAttribute(nsGkAtoms::_class, DeleteEntry());
      }
    }
  }

  // State is only included in the initial push. Thereafter, cached state is
+7 −1
Original line number Diff line number Diff line
@@ -1208,7 +1208,7 @@ template <class Derived>
void RemoteAccessibleBase<Derived>::DOMNodeID(nsString& aID) const {
  if (mCachedFields) {
    mCachedFields->GetAttribute(nsGkAtoms::id, aID);
    VERIFY_CACHE(CacheDomain::DOMNodeID);
    VERIFY_CACHE(CacheDomain::DOMNodeIDAndClass);
  }
}

@@ -1421,6 +1421,12 @@ already_AddRefed<AccAttributes> RemoteAccessibleBase<Derived>::Attributes() {
    if (!id.IsEmpty()) {
      attributes->SetAttribute(nsGkAtoms::id, std::move(id));
    }

    nsString className;
    mCachedFields->GetAttribute(nsGkAtoms::_class, className);
    if (!className.IsEmpty()) {
      attributes->SetAttribute(nsGkAtoms::_class, std::move(className));
    }
  }

  nsAutoString name;
+55 −0
Original line number Diff line number Diff line
@@ -548,3 +548,58 @@ addAccessibleTask(
  },
  { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
);

function untilCacheAttrIs(acc, attr, val, msg) {
  return untilCacheOk(() => {
    try {
      return acc.attributes.getStringProperty(attr) == val;
    } catch (e) {
      return false;
    }
  }, msg);
}

function untilCacheAttrAbsent(acc, attr, msg) {
  return untilCacheOk(() => {
    try {
      acc.attributes.getStringProperty(attr);
    } catch (e) {
      return true;
    }
    return false;
  }, msg);
}

/**
 * Test the class attribute.
 */
addAccessibleTask(
  `
<div id="oneClass" class="c1">oneClass</div>
<div id="multiClass" class="c1 c2">multiClass</div>
<div id="noClass">noClass</div>
<div id="mutate">mutate</div>
  `,
  async function(browser, docAcc) {
    const oneClass = findAccessibleChildByID(docAcc, "oneClass");
    testAttrs(oneClass, { class: "c1" }, true);
    const multiClass = findAccessibleChildByID(docAcc, "multiClass");
    testAttrs(multiClass, { class: "c1 c2" }, true);
    const noClass = findAccessibleChildByID(docAcc, "noClass");
    testAbsentAttrs(noClass, { class: "" });

    const mutate = findAccessibleChildByID(docAcc, "mutate");
    testAbsentAttrs(mutate, { class: "" });
    info("Adding class to mutate");
    await invokeContentTask(browser, [], () => {
      content.document.getElementById("mutate").className = "c1 c2";
    });
    await untilCacheAttrIs(mutate, "class", "c1 c2", "mutate class correct");
    info("Removing class from mutate");
    await invokeContentTask(browser, [], () => {
      content.document.getElementById("mutate").removeAttribute("class");
    });
    await untilCacheAttrAbsent(mutate, "class", "mutate class not present");
  },
  { chrome: true, topLevel: true }
);