already_AddRefed LocalAccessible::BundleFieldsForCache()

in accessible/generic/LocalAccessible.cpp [3414:4244]


already_AddRefed<AccAttributes> LocalAccessible::BundleFieldsForCache(
    uint64_t aCacheDomain, CacheUpdateType aUpdateType,
    uint64_t aInitialDomains) {
  MOZ_ASSERT((~aCacheDomain & aInitialDomains) == CacheDomain::None,
             "Initial domain pushes without domains requested!");
  RefPtr<AccAttributes> fields = new AccAttributes();

  if (aUpdateType == CacheUpdateType::Initial) {
    aInitialDomains = CacheDomain::All;
  }
  // Pass a single cache domain in to query whether this is the initial push for
  // this domain.
  auto IsInitialPush = [aInitialDomains](uint64_t aCacheDomain) {
    return (aCacheDomain & aInitialDomains) == aCacheDomain;
  };
  auto IsUpdatePush = [aInitialDomains](uint64_t aCacheDomain) {
    return (aCacheDomain & aInitialDomains) == CacheDomain::None;
  };

  // Caching name for text leaf Accessibles is redundant, since their name is
  // always their text. Text gets handled below.
  if (aCacheDomain & CacheDomain::NameAndDescription && !IsText()) {
    nsString name;
    int32_t nameFlag = Name(name);
    if (nameFlag != eNameOK) {
      fields->SetAttribute(CacheKey::NameValueFlag, nameFlag);
    } else if (IsUpdatePush(CacheDomain::NameAndDescription)) {
      fields->SetAttribute(CacheKey::NameValueFlag, DeleteEntry());
    }

    if (IsTextField()) {
      MOZ_ASSERT(mContent);
      nsString placeholder;
      // Only cache the placeholder separately if it isn't used as the name.
      if (Elm()->GetAttr(nsGkAtoms::placeholder, placeholder) &&
          name != placeholder) {
        fields->SetAttribute(CacheKey::HTMLPlaceholder, std::move(placeholder));
      } else if (IsUpdatePush(CacheDomain::NameAndDescription)) {
        fields->SetAttribute(CacheKey::HTMLPlaceholder, DeleteEntry());
      }
    }

    if (!name.IsEmpty()) {
      fields->SetAttribute(CacheKey::Name, std::move(name));
    } else if (IsUpdatePush(CacheDomain::NameAndDescription)) {
      fields->SetAttribute(CacheKey::Name, DeleteEntry());
    }

    nsString description;
    Description(description);
    if (!description.IsEmpty()) {
      fields->SetAttribute(CacheKey::Description, std::move(description));
    } else if (IsUpdatePush(CacheDomain::NameAndDescription)) {
      fields->SetAttribute(CacheKey::Description, DeleteEntry());
    }
  }

  if (aCacheDomain & CacheDomain::Value) {
    // We cache the text value in 3 cases:
    // 1. Accessible is an HTML input type that holds a number.
    // 2. Accessible has a numeric value and an aria-valuetext.
    // 3. Accessible is an HTML input type that holds text.
    // 4. Accessible is a link, in which case value is the target URL.
    // ... for all other cases we divine the value remotely.
    bool cacheValueText = false;
    if (HasNumericValue()) {
      fields->SetAttribute(CacheKey::NumericValue, CurValue());
      fields->SetAttribute(CacheKey::MaxValue, MaxValue());
      fields->SetAttribute(CacheKey::MinValue, MinValue());
      fields->SetAttribute(CacheKey::Step, Step());
      cacheValueText = NativeHasNumericValue() ||
                       (mContent->IsElement() &&
                        nsAccUtils::HasARIAAttr(mContent->AsElement(),
                                                nsGkAtoms::aria_valuetext));
    } else {
      cacheValueText = IsTextField() || IsHTMLLink();
    }

    if (cacheValueText) {
      nsString value;
      Value(value);
      if (!value.IsEmpty()) {
        fields->SetAttribute(CacheKey::TextValue, std::move(value));
      } else if (IsUpdatePush(CacheDomain::Value)) {
        fields->SetAttribute(CacheKey::TextValue, DeleteEntry());
      }
    }

    if (IsImage()) {
      // Cache the src of images. This is used by some clients to help remediate
      // inaccessible images.
      MOZ_ASSERT(mContent, "Image must have mContent");
      nsString src;
      mContent->AsElement()->GetAttr(nsGkAtoms::src, src);
      if (!src.IsEmpty()) {
        fields->SetAttribute(CacheKey::SrcURL, std::move(src));
      } else if (IsUpdatePush(CacheDomain::Value)) {
        fields->SetAttribute(CacheKey::SrcURL, DeleteEntry());
      }
    }

    if (TagName() == nsGkAtoms::meter) {
      // We should only cache value region for HTML meter elements. A meter
      // should always have a value region, so this attribute should never
      // be empty (i.e. there is no DeleteEntry() clause here).
      HTMLMeterAccessible* meter = static_cast<HTMLMeterAccessible*>(this);
      fields->SetAttribute(CacheKey::ValueRegion, meter->ValueRegion());
    }
  }

  if (aCacheDomain & CacheDomain::Viewport && IsDoc()) {
    // Construct the viewport cache for this document. This cache domain will
    // only be requested after we finish painting.
    DocAccessible* doc = AsDoc();
    PresShell* presShell = doc->PresShellPtr();

    if (nsIFrame* rootFrame = presShell->GetRootFrame()) {
      nsTArray<nsIFrame*> frames;
      ScrollContainerFrame* sf = presShell->GetRootScrollContainerFrame();
      nsRect scrollPort = sf ? sf->GetScrollPortRect() : rootFrame->GetRect();

      nsLayoutUtils::GetFramesForArea(
          RelativeTo{rootFrame}, scrollPort, frames,
          {{// We don't add the ::OnlyVisible option here, because
            // it means we always consider frames with pointer-events: none.
            // See usage of HitTestIsForVisibility in nsDisplayList::HitTest.
            // This flag ensures the display lists are built, even if
            // the page hasn't finished loading.
            nsLayoutUtils::FrameForPointOption::IgnorePaintSuppression,
            // Each doc should have its own viewport cache, so we can
            // ignore cross-doc content as an optimization.
            nsLayoutUtils::FrameForPointOption::IgnoreCrossDoc}});

      nsTHashSet<LocalAccessible*> inViewAccs;
      nsTArray<uint64_t> viewportCache(frames.Length());
      // Layout considers table rows fully occluded by their containing cells.
      // This means they don't have their own display list items, and they won't
      // show up in the list returned from GetFramesForArea. To prevent table
      // rows from appearing offscreen, we manually add any rows for which we
      // have on-screen cells.
      LocalAccessible* prevParentRow = nullptr;
      for (nsIFrame* frame : frames) {
        if (frame->IsInlineFrame() && !frame->IsPrimaryFrame()) {
          // This is a line other than the first line in an inline element. Even
          // though there are multiple frames for this element (one per line),
          // there is only a single Accessible with bounds encompassing all the
          // frames. We don't have any additional information about the
          // individual continuation frames in our cache. Thus, we don't want
          // this Accessible to appear before leaves on other lines which are
          // later in the `frames` array. Otherwise, when hit testing, this
          // Accessible will match instead of those leaves. We will add this
          // Accessible when we get to its primary frame later.
          continue;
        }
        nsIContent* content = frame->GetContent();
        if (!content) {
          continue;
        }

        LocalAccessible* acc = doc->GetAccessible(content);
        // The document should always be present at the end of the list, so
        // including it is unnecessary and wasteful. We skip the document here
        // and handle it as a fallback when hit testing.
        if (!acc || acc == mDoc) {
          continue;
        }

        if (acc->IsTextLeaf() && nsAccUtils::MustPrune(acc->LocalParent())) {
          acc = acc->LocalParent();
        }
        if (acc->IsTableCell()) {
          LocalAccessible* parent = acc->LocalParent();
          if (parent && parent->IsTableRow() && parent != prevParentRow) {
            // If we've entered a new row since the last cell we saw, add the
            // previous parent row to our viewport cache here to maintain
            // hittesting order. Keep track of the current parent row.
            if (prevParentRow && inViewAccs.EnsureInserted(prevParentRow)) {
              viewportCache.AppendElement(prevParentRow->ID());
            }
            prevParentRow = parent;
          }
        } else if (acc->IsTable()) {
          // If we've encountered a table, we know we've already
          // handled all of this table's content (because we're traversing
          // in hittesting order). Add our table's final row to the viewport
          // cache before adding the table itself. Reset our marker for the next
          // table.
          if (prevParentRow && inViewAccs.EnsureInserted(prevParentRow)) {
            viewportCache.AppendElement(prevParentRow->ID());
          }
          prevParentRow = nullptr;
        } else if (acc->IsImageMap()) {
          // Layout doesn't walk image maps, so we do that
          // manually here. We do this before adding the map itself
          // so the children come earlier in the hittesting order.
          for (uint32_t i = 0; i < acc->ChildCount(); i++) {
            LocalAccessible* child = acc->LocalChildAt(i);
            MOZ_ASSERT(child);
            if (inViewAccs.EnsureInserted(child)) {
              MOZ_ASSERT(!child->IsDoc());
              viewportCache.AppendElement(child->ID());
            }
          }
        } else if (acc->IsHTMLCombobox()) {
          // Layout doesn't consider combobox lists (or their
          // currently selected items) to be onscreen, but we do.
          // Add those things manually here.
          HTMLComboboxAccessible* combobox =
              static_cast<HTMLComboboxAccessible*>(acc);
          HTMLComboboxListAccessible* list = combobox->List();
          LocalAccessible* currItem = combobox->SelectedOption();
          // Preserve hittesting order by adding the item, then
          // the list, and finally the combobox itself.
          if (currItem && inViewAccs.EnsureInserted(currItem)) {
            viewportCache.AppendElement(currItem->ID());
          }
          if (list && inViewAccs.EnsureInserted(list)) {
            viewportCache.AppendElement(list->ID());
          }
        }

        if (inViewAccs.EnsureInserted(acc)) {
          MOZ_ASSERT(!acc->IsDoc());
          viewportCache.AppendElement(acc->ID());
        }
      }

      // Always send the viewport cache, even if we have no accessibles
      // in it. We don't want to send a delete entry because the viewport
      // cache _does_ exist, it is simply representing an empty screen.
      fields->SetAttribute(CacheKey::Viewport, std::move(viewportCache));
    }
  }

  if (aCacheDomain & CacheDomain::APZ && IsDoc()) {
    PresShell* presShell = AsDoc()->PresShellPtr();
    MOZ_ASSERT(presShell, "Can't get APZ factor for null presShell");
    nsPoint viewportOffset =
        presShell->GetVisualViewportOffsetRelativeToLayoutViewport();
    if (viewportOffset.x || viewportOffset.y) {
      nsTArray<int32_t> offsetArray(2);
      offsetArray.AppendElement(viewportOffset.x);
      offsetArray.AppendElement(viewportOffset.y);
      fields->SetAttribute(CacheKey::VisualViewportOffset,
                           std::move(offsetArray));
    } else if (IsUpdatePush(CacheDomain::APZ)) {
      fields->SetAttribute(CacheKey::VisualViewportOffset, DeleteEntry());
    }
  }

  bool boundsChanged = false;
  nsIFrame* frame = GetFrame();
  if (aCacheDomain & CacheDomain::Bounds) {
    nsRect newBoundsRect = ParentRelativeBounds();

    // 1. Layout might notify us of a possible bounds change when the bounds
    // haven't really changed. Therefore, we cache the last  bounds we sent
    // and don't send an update if they haven't changed.
    // 2. For an initial cache push, we ignore 1)  and always send the bounds.
    // This handles the case where this LocalAccessible was moved (but not
    // re-created). In that case, we will have cached bounds, but we currently
    // do an initial cache push.
    MOZ_ASSERT(IsInitialPush(CacheDomain::Bounds) || mBounds.isSome(),
               "Incremental cache push but mBounds is not set!");

    if (OuterDocAccessible* doc = AsOuterDoc()) {
      if (nsIFrame* docFrame = doc->GetFrame()) {
        const nsMargin& newOffset = docFrame->GetUsedBorderAndPadding();
        Maybe<nsMargin> currOffset = doc->GetCrossDocOffset();
        if (!currOffset || *currOffset != newOffset) {
          // OOP iframe docs can't compute their position within their
          // cross-proc parent, so we have to manually cache that offset
          // on the parent (outer doc) itself. For simplicity and consistency,
          // we do this here for both OOP and in-process iframes. For in-process
          // iframes, this also avoids the need to push a cache update for the
          // embedded document when the iframe changes its padding, gets
          // re-created, etc. Similar to bounds, we maintain a local cache and a
          // remote cache to avoid sending redundant updates.
          doc->SetCrossDocOffset(newOffset);
          nsTArray<int32_t> offsetArray(2);
          offsetArray.AppendElement(newOffset.Side(eSideLeft));  // X offset
          offsetArray.AppendElement(newOffset.Side(eSideTop));   // Y offset
          fields->SetAttribute(CacheKey::CrossDocOffset,
                               std::move(offsetArray));
        }
      }
    }

    // mBounds should never be Nothing, but sometimes it is (see Bug 1922691).
    // We null check mBounds here to avoid a crash while we figure out how this
    // can happen.
    boundsChanged = IsInitialPush(CacheDomain::Bounds) || !mBounds ||
                    !newBoundsRect.IsEqualEdges(mBounds.value());
    if (boundsChanged) {
      mBounds = Some(newBoundsRect);

      nsTArray<int32_t> boundsArray(4);

      boundsArray.AppendElement(newBoundsRect.x);
      boundsArray.AppendElement(newBoundsRect.y);
      boundsArray.AppendElement(newBoundsRect.width);
      boundsArray.AppendElement(newBoundsRect.height);

      fields->SetAttribute(CacheKey::ParentRelativeBounds,
                           std::move(boundsArray));
    }

    if (frame && frame->ScrollableOverflowRect().IsEmpty()) {
      fields->SetAttribute(CacheKey::IsClipped, true);
    } else if (IsUpdatePush(CacheDomain::Bounds)) {
      fields->SetAttribute(CacheKey::IsClipped, DeleteEntry());
    }
  }

  if (aCacheDomain & CacheDomain::Text) {
    if (!HasChildren()) {
      // We only cache text and line offsets on leaf Accessibles.
      // Only text Accessibles can have actual text.
      if (IsText()) {
        nsString text;
        AppendTextTo(text);
        fields->SetAttribute(CacheKey::Text, std::move(text));
        TextLeafPoint point(this, 0);
        RefPtr<AccAttributes> attrs = point.GetTextAttributesLocalAcc(
            /* aIncludeDefaults */ false);
        fields->SetAttribute(CacheKey::TextAttributes, std::move(attrs));
      }
    }
    if (HyperTextAccessible* ht = AsHyperText()) {
      RefPtr<AccAttributes> attrs = ht->DefaultTextAttributes();
      fields->SetAttribute(CacheKey::TextAttributes, std::move(attrs));
    } else if (!IsText()) {
      // Language is normally cached in text attributes, but Accessibles that
      // aren't HyperText or Text (e.g. <img>, <input type="radio">) don't have
      // text attributes. The Text domain isn't a great fit, but the kinds of
      // clients (e.g. screen readers) that care about language are likely to
      // care about text as well.
      nsString language;
      Language(language);
      if (!language.IsEmpty()) {
        fields->SetAttribute(CacheKey::Language, std::move(language));
      } else if (IsUpdatePush(CacheDomain::Text)) {
        fields->SetAttribute(CacheKey::Language, DeleteEntry());
      }
    }
  }

  // If text changes, we must also update text offset attributes.
  if (aCacheDomain & (CacheDomain::TextOffsetAttributes | CacheDomain::Text) &&
      IsTextLeaf()) {
    auto offsetAttrs = TextLeafPoint::GetTextOffsetAttributes(this);
    if (!offsetAttrs.IsEmpty()) {
      fields->SetAttribute(CacheKey::TextOffsetAttributes,
                           std::move(offsetAttrs));
    } else if (IsUpdatePush(CacheDomain::TextOffsetAttributes) ||
               IsUpdatePush(CacheDomain::Text)) {
      fields->SetAttribute(CacheKey::TextOffsetAttributes, DeleteEntry());
    }
  }

  if (aCacheDomain & (CacheDomain::TextBounds) && !HasChildren()) {
    // We cache line start offsets for both text and non-text leaf Accessibles
    // because non-text leaf Accessibles can still start a line.
    TextLeafPoint lineStart =
        TextLeafPoint(this, 0).FindNextLineStartSameLocalAcc(
            /* aIncludeOrigin */ true);
    int32_t lineStartOffset = lineStart ? lineStart.mOffset : -1;
    // We push line starts and text bounds in two cases:
    // 1. TextBounds is pushed initially.
    // 2. CacheDomain::Bounds was requested (indicating that the frame was
    // reflowed) but the bounds  didn't actually change. This can happen when
    // the spanned text is non-rectangular. For example, an Accessible might
    // cover two characters on one line and a single character on another line.
    // An insertion in a previous text node might cause it to shift such that it
    // now covers a single character on the first line and two characters on the
    // second line. Its bounding rect will be the same both before and after the
    // insertion. In this case, we use the first line start to determine whether
    // there was a change. This should be safe because the text didn't change in
    // this Accessible, so if the first line start doesn't shift, none of them
    // should shift.
    if (IsInitialPush(CacheDomain::TextBounds) ||
        aCacheDomain & CacheDomain::Text || boundsChanged ||
        mFirstLineStart != lineStartOffset) {
      mFirstLineStart = lineStartOffset;
      nsTArray<int32_t> lineStarts;
      for (; lineStart;
           lineStart = lineStart.FindNextLineStartSameLocalAcc(false)) {
        lineStarts.AppendElement(lineStart.mOffset);
      }
      if (!lineStarts.IsEmpty()) {
        fields->SetAttribute(CacheKey::TextLineStarts, std::move(lineStarts));
      } else if (IsUpdatePush(CacheDomain::TextBounds)) {
        fields->SetAttribute(CacheKey::TextLineStarts, DeleteEntry());
      }

      if (frame && frame->IsTextFrame()) {
        if (nsTextFrame* currTextFrame = do_QueryFrame(frame)) {
          nsTArray<int32_t> charData(nsAccUtils::TextLength(this) *
                                     kNumbersInRect);
          // Continuation offsets are calculated relative to the primary frame.
          // However, the acc's bounds are calculated using
          // GetAllInFlowRectsUnion. For wrapped text which starts part way
          // through a line, this might mean the top left of the acc is
          // different to the top left of the primary frame. This also happens
          // when the primary frame is empty (e.g. a blank line at the start of
          // pre-formatted text), since the union rect will exclude the origin
          // in that case. Calculate the offset from the acc's rect to the
          // primary frame's rect.
          nsRect accOffset =
              nsLayoutUtils::GetAllInFlowRectsUnion(frame, frame);
          while (currTextFrame) {
            nsPoint contOffset = currTextFrame->GetOffsetTo(frame);
            contOffset -= accOffset.TopLeft();
            int32_t length = currTextFrame->GetContentLength();
            nsTArray<nsRect> charBounds(length);
            currTextFrame->GetCharacterRectsInRange(
                currTextFrame->GetContentOffset(), length, charBounds);
            for (nsRect& charRect : charBounds) {
              if (charRect.width == 0 &&
                  !currTextFrame->StyleText()->WhiteSpaceIsSignificant()) {
                // GetCharacterRectsInRange gives us one rect per content
                // offset. However, TextLeafAccessibles use rendered offsets;
                // e.g. they might exclude some content white space. If we get
                // a 0 width rect and it's white space, skip this rect, since
                // this character isn't in the rendered text. We do have
                // a way to convert between content and rendered offsets, but
                // doing this for every character is expensive.
                const char16_t contentChar = mContent->GetText()->CharAt(
                    charData.Length() / kNumbersInRect);
                if (contentChar == u' ' || contentChar == u'\t' ||
                    contentChar == u'\n') {
                  continue;
                }
              }
              // We expect each char rect to be relative to the text leaf
              // acc this text lives in. Unfortunately, GetCharacterRectsInRange
              // returns rects relative to their continuation. Add the
              // continuation's relative position here to make our final
              // rect relative to the text leaf acc.
              charRect.MoveBy(contOffset);
              charData.AppendElement(charRect.x);
              charData.AppendElement(charRect.y);
              charData.AppendElement(charRect.width);
              charData.AppendElement(charRect.height);
            }
            currTextFrame = currTextFrame->GetNextContinuation();
          }
          if (charData.Length()) {
            fields->SetAttribute(CacheKey::TextBounds, std::move(charData));
          }
        }
      }
    }
  }

  if (aCacheDomain & CacheDomain::TransformMatrix) {
    bool transformed = false;
    if (frame && frame->IsTransformed()) {
      // This matrix is only valid when applied to CSSPixel points/rects
      // in the coordinate space of `frame`.
      gfx::Matrix4x4 mtx = nsDisplayTransform::GetResultingTransformMatrix(
          frame, nsPoint(0, 0), AppUnitsPerCSSPixel(),
          nsDisplayTransform::INCLUDE_PERSPECTIVE);
      // We might get back the identity matrix. This can happen if there is no
      // actual transform. For example, if an element has
      // will-change: transform, nsIFrame::IsTransformed will return true, but
      // this doesn't necessarily mean there is a transform right now.
      // Applying the identity matrix is effectively a no-op, so there's no
      // point caching it.
      transformed = !mtx.IsIdentity();
      if (transformed) {
        UniquePtr<gfx::Matrix4x4> ptr = MakeUnique<gfx::Matrix4x4>(mtx);
        fields->SetAttribute(CacheKey::TransformMatrix, std::move(ptr));
      }
    }
    if (!transformed && IsUpdatePush(CacheDomain::TransformMatrix)) {
      // Otherwise, if we're bundling a transform update but this
      // frame isn't transformed (or doesn't exist), we need
      // to send a DeleteEntry() to remove any
      // transform that was previously cached for this frame.
      fields->SetAttribute(CacheKey::TransformMatrix, DeleteEntry());
    }
  }

  if (aCacheDomain & CacheDomain::ScrollPosition && frame) {
    const auto [scrollPosition, scrollRange] = mDoc->ComputeScrollData(this);
    if (scrollRange.width || scrollRange.height) {
      // If the scroll range is 0 by 0, this acc is not scrollable. We
      // can't simply check scrollPosition != 0, since it's valid for scrollable
      // frames to have a (0, 0) position. We also can't check IsEmpty or
      // ZeroArea because frames with only one scrollable dimension will return
      // a height/width of zero for the non-scrollable dimension, yielding zero
      // area even if the width/height for the scrollable dimension is nonzero.
      // We also cache (0, 0) for accs with overflow:auto or overflow:scroll,
      // even if the content is not currently large enough to be scrollable
      // right now -- these accs have a non-zero scroll range.
      nsTArray<int32_t> positionArr(2);
      positionArr.AppendElement(scrollPosition.x);
      positionArr.AppendElement(scrollPosition.y);
      fields->SetAttribute(CacheKey::ScrollPosition, std::move(positionArr));
    } else if (IsUpdatePush(CacheDomain::ScrollPosition)) {
      fields->SetAttribute(CacheKey::ScrollPosition, DeleteEntry());
    }
  }

  if (aCacheDomain & CacheDomain::DOMNodeIDAndClass && mContent) {
    nsAtom* id = mContent->GetID();
    if (id) {
      fields->SetAttribute(CacheKey::DOMNodeID, id);
    } else if (IsUpdatePush(CacheDomain::DOMNodeIDAndClass)) {
      fields->SetAttribute(CacheKey::DOMNodeID, DeleteEntry());
    }
    nsString className;
    DOMNodeClass(className);
    if (!className.IsEmpty()) {
      fields->SetAttribute(CacheKey::DOMNodeClass, std::move(className));
    } else if (IsUpdatePush(CacheDomain::DOMNodeIDAndClass)) {
      fields->SetAttribute(CacheKey::DOMNodeClass, DeleteEntry());
    }
  }

  // State is only included in the initial push. Thereafter, cached state is
  // updated via events.
  if (aCacheDomain & CacheDomain::State) {
    if (IsInitialPush(CacheDomain::State)) {
      // Most states are updated using state change events, so we only send
      // these for the initial cache push.
      uint64_t state = State();
      // Exclude states which must be calculated by RemoteAccessible.
      state &= ~kRemoteCalculatedStates;
      fields->SetAttribute(CacheKey::State, state);
    }
    // If aria-selected isn't specified, there may be no SELECTED state.
    // However, aria-selected can be implicit in some cases when an item is
    // focused. We don't want to do this if aria-selected is explicitly
    // set to "false", so we need to differentiate between false and unset.
    if (auto ariaSelected = ARIASelected()) {
      fields->SetAttribute(CacheKey::ARIASelected, *ariaSelected);
    } else if (IsUpdatePush(CacheDomain::State)) {
      fields->SetAttribute(CacheKey::ARIASelected, DeleteEntry());  // Unset.
    }
  }

  if (aCacheDomain & CacheDomain::GroupInfo && mContent) {
    for (nsAtom* attr : {nsGkAtoms::aria_level, nsGkAtoms::aria_setsize,
                         nsGkAtoms::aria_posinset}) {
      int32_t value = 0;
      if (nsCoreUtils::GetUIntAttr(mContent, attr, &value)) {
        fields->SetAttribute(attr, value);
      } else if (IsUpdatePush(CacheDomain::GroupInfo)) {
        fields->SetAttribute(attr, DeleteEntry());
      }
    }
  }

  if (aCacheDomain & CacheDomain::Actions) {
    if (HasPrimaryAction()) {
      // Here we cache the primary action.
      nsAutoString actionName;
      ActionNameAt(0, actionName);
      RefPtr<nsAtom> actionAtom = NS_Atomize(actionName);
      fields->SetAttribute(CacheKey::PrimaryAction, actionAtom);
    } else if (IsUpdatePush(CacheDomain::Actions)) {
      fields->SetAttribute(CacheKey::PrimaryAction, DeleteEntry());
    }

    if (ImageAccessible* imgAcc = AsImage()) {
      // Here we cache the showlongdesc action.
      if (imgAcc->HasLongDesc()) {
        fields->SetAttribute(CacheKey::HasLongdesc, true);
      } else if (IsUpdatePush(CacheDomain::Actions)) {
        fields->SetAttribute(CacheKey::HasLongdesc, DeleteEntry());
      }
    }

    KeyBinding accessKey = AccessKey();
    if (!accessKey.IsEmpty()) {
      fields->SetAttribute(CacheKey::AccessKey, accessKey.Serialize());
    } else if (IsUpdatePush(CacheDomain::Actions)) {
      fields->SetAttribute(CacheKey::AccessKey, DeleteEntry());
    }
  }

  if (aCacheDomain & CacheDomain::Style) {
    if (RefPtr<nsAtom> display = DisplayStyle()) {
      fields->SetAttribute(CacheKey::CSSDisplay, display);
    }

    float opacity = Opacity();
    if (opacity != 1.0f) {
      fields->SetAttribute(CacheKey::Opacity, opacity);
    } else if (IsUpdatePush(CacheDomain::Style)) {
      fields->SetAttribute(CacheKey::Opacity, DeleteEntry());
    }

    if (frame &&
        frame->StyleDisplay()->mPosition == StylePositionProperty::Fixed &&
        nsLayoutUtils::IsReallyFixedPos(frame)) {
      fields->SetAttribute(CacheKey::CssPosition, nsGkAtoms::fixed);
    } else if (IsUpdatePush(CacheDomain::Style)) {
      fields->SetAttribute(CacheKey::CssPosition, DeleteEntry());
    }

    if (frame) {
      nsAutoCString overflow;
      frame->Style()->GetComputedPropertyValue(eCSSProperty_overflow, overflow);
      RefPtr<nsAtom> overflowAtom = NS_Atomize(overflow);
      if (overflowAtom == nsGkAtoms::hidden) {
        fields->SetAttribute(CacheKey::CSSOverflow, nsGkAtoms::hidden);
      } else if (IsUpdatePush(CacheDomain::Style)) {
        fields->SetAttribute(CacheKey::CSSOverflow, DeleteEntry());
      }
    }
  }

  if (aCacheDomain & CacheDomain::Table) {
    if (auto* table = HTMLTableAccessible::GetFrom(this)) {
      if (table->IsProbablyLayoutTable()) {
        fields->SetAttribute(CacheKey::TableLayoutGuess, true);
      } else if (IsUpdatePush(CacheDomain::Table)) {
        fields->SetAttribute(CacheKey::TableLayoutGuess, DeleteEntry());
      }
    } else if (auto* cell = HTMLTableCellAccessible::GetFrom(this)) {
      // For HTML table cells, we must use the HTMLTableCellAccessible
      // GetRow/ColExtent methods rather than using the DOM attributes directly.
      // This is because of things like rowspan="0" which depend on knowing
      // about thead, tbody, etc., which is info we don't have in the a11y tree.
      int32_t value = static_cast<int32_t>(cell->RowExtent());
      MOZ_ASSERT(value > 0);
      if (value > 1) {
        fields->SetAttribute(CacheKey::RowSpan, value);
      } else if (IsUpdatePush(CacheDomain::Table)) {
        fields->SetAttribute(CacheKey::RowSpan, DeleteEntry());
      }
      value = static_cast<int32_t>(cell->ColExtent());
      MOZ_ASSERT(value > 0);
      if (value > 1) {
        fields->SetAttribute(CacheKey::ColSpan, value);
      } else if (IsUpdatePush(CacheDomain::Table)) {
        fields->SetAttribute(CacheKey::ColSpan, DeleteEntry());
      }
      if (mContent->AsElement()->HasAttr(nsGkAtoms::headers)) {
        nsTArray<uint64_t> headers;
        AssociatedElementsIterator iter(mDoc, mContent, nsGkAtoms::headers);
        while (LocalAccessible* cell = iter.Next()) {
          if (cell->IsTableCell()) {
            headers.AppendElement(cell->ID());
          }
        }
        fields->SetAttribute(CacheKey::CellHeaders, std::move(headers));
      } else if (IsUpdatePush(CacheDomain::Table)) {
        fields->SetAttribute(CacheKey::CellHeaders, DeleteEntry());
      }
    }
  }

  if (aCacheDomain & CacheDomain::ARIA && mContent && mContent->IsElement()) {
    // We use a nested AccAttributes to make cache updates simpler. Rather than
    // managing individual removals, we just replace or remove the entire set of
    // ARIA attributes.
    RefPtr<AccAttributes> ariaAttrs;
    aria::AttrIterator attrIt(mContent);
    while (attrIt.Next()) {
      if (!ariaAttrs) {
        ariaAttrs = new AccAttributes();
      }
      attrIt.ExposeAttr(ariaAttrs);
    }
    if (ariaAttrs) {
      fields->SetAttribute(CacheKey::ARIAAttributes, std::move(ariaAttrs));
    } else if (IsUpdatePush(CacheDomain::ARIA)) {
      fields->SetAttribute(CacheKey::ARIAAttributes, DeleteEntry());
    }
  }

  if (aCacheDomain & CacheDomain::Relations && mContent) {
    if (IsHTMLRadioButton() ||
        (mContent->IsElement() &&
         mContent->AsElement()->IsHTMLElement(nsGkAtoms::a))) {
      // HTML radio buttons with the same name should be grouped
      // and returned together when their MEMBER_OF relation is
      // requested. Computing LINKS_TO also requires we cache `name` on
      // anchor elements.
      nsString name;
      mContent->AsElement()->GetAttr(nsGkAtoms::name, name);
      if (!name.IsEmpty()) {
        fields->SetAttribute(CacheKey::DOMName, std::move(name));
      } else if (IsUpdatePush(CacheDomain::Relations)) {
        // It's possible we used to have a name and it's since been
        // removed. Send a delete entry.
        fields->SetAttribute(CacheKey::DOMName, DeleteEntry());
      }
    }

    for (auto const& data : kRelationTypeAtoms) {
      nsTArray<uint64_t> ids;
      nsStaticAtom* const relAtom = data.mAtom;

      Relation rel;
      if (data.mType == RelationType::LABEL_FOR) {
        // Labels are a special case -- we need to validate that the target of
        // their `for` attribute is in fact labelable. DOM checks this when we
        // call GetControl(). If a label contains an element we will return it
        // here.
        if (dom::HTMLLabelElement* labelEl =
                dom::HTMLLabelElement::FromNode(mContent)) {
          rel.AppendTarget(mDoc, labelEl->GetControl());
        }
      } else if (data.mType == RelationType::DETAILS ||
                 data.mType == RelationType::CONTROLLER_FOR) {
        // We need to use RelationByType for details because it might include
        // popovertarget. Nothing exposes an implicit reverse details
        // relation, so using RelationByType here is fine.
        //
        // We need to use RelationByType for controls because it might include
        // failed aria-owned relocations or it may be an output element.
        // Nothing exposes an implicit reverse controls relation, so using
        // RelationByType here is fine.
        rel = RelationByType(data.mType);
      } else {
        // We use an AssociatedElementsIterator here instead of calling
        // RelationByType directly because we only want to cache explicit
        // relations. Implicit relations (e.g. LABEL_FOR exposed on the target
        // of aria-labelledby) will be computed and stored separately in the
        // parent process.
        rel.AppendIter(new AssociatedElementsIterator(mDoc, mContent, relAtom));
      }

      while (LocalAccessible* acc = rel.LocalNext()) {
        ids.AppendElement(acc->ID());
      }
      if (ids.Length()) {
        fields->SetAttribute(relAtom, std::move(ids));
      } else if (IsUpdatePush(CacheDomain::Relations)) {
        fields->SetAttribute(relAtom, DeleteEntry());
      }
    }
  }

#if defined(XP_WIN)
  if (aCacheDomain & CacheDomain::InnerHTML && HasOwnContent() &&
      mContent->IsMathMLElement(nsGkAtoms::math)) {
    nsString innerHTML;
    mContent->AsElement()->GetInnerHTML(innerHTML, IgnoreErrors());
    fields->SetAttribute(CacheKey::InnerHTML, std::move(innerHTML));
  }
#endif  // defined(XP_WIN)

  if (aUpdateType == CacheUpdateType::Initial) {
    // Add fields which never change and thus only need to be included in the
    // initial cache push.
    if (mContent && mContent->IsElement()) {
      fields->SetAttribute(CacheKey::TagName, mContent->NodeInfo()->NameAtom());

      dom::Element* el = mContent->AsElement();
      if (IsTextField() || IsDateTimeField()) {
        // Cache text input types. Accessible is recreated if this changes,
        // so it is considered immutable.
        if (const nsAttrValue* attr = el->GetParsedAttr(nsGkAtoms::type)) {
          RefPtr<nsAtom> inputType = attr->GetAsAtom();
          if (inputType) {
            fields->SetAttribute(CacheKey::InputType, inputType);
          }
        }
      }

      // Changing the role attribute currently re-creates the Accessible, so
      // it's immutable in the cache.
      if (const nsRoleMapEntry* roleMap = ARIARoleMap()) {
        // Most of the time, the role attribute is a single, known role. We
        // already send the map index, so we don't need to double up.
        if (!nsAccUtils::ARIAAttrValueIs(el, nsGkAtoms::role, roleMap->roleAtom,
                                         eIgnoreCase)) {
          // Multiple roles or unknown roles are rare, so just send them as a
          // string.
          nsAutoString role;
          nsAccUtils::GetARIAAttr(el, nsGkAtoms::role, role);
          fields->SetAttribute(CacheKey::ARIARole, std::move(role));
        }
      }

      if (auto* htmlEl = nsGenericHTMLElement::FromNode(mContent)) {
        // Changing popover recreates the Accessible, so it's immutable in the
        // cache.
        nsAutoString popover;
        htmlEl->GetPopover(popover);
        if (!popover.IsEmpty()) {
          fields->SetAttribute(CacheKey::PopupType,
                               RefPtr{NS_Atomize(popover)});
        }
      }
    }

    if (frame) {
      // Note our frame's current computed style so we can track style changes
      // later on.
      mOldComputedStyle = frame->Style();
      if (frame->IsTransformed()) {
        mStateFlags |= eOldFrameHasValidTransformStyle;
      } else {
        mStateFlags &= ~eOldFrameHasValidTransformStyle;
      }
    }

    if (IsDoc()) {
      if (PresShell* presShell = AsDoc()->PresShellPtr()) {
        // Send the initial resolution of the document. When this changes, we
        // will ne notified via nsAS::NotifyOfResolutionChange
        float resolution = presShell->GetResolution();
        fields->SetAttribute(CacheKey::Resolution, resolution);
        int32_t appUnitsPerDevPixel =
            presShell->GetPresContext()->AppUnitsPerDevPixel();
        fields->SetAttribute(CacheKey::AppUnitsPerDevPixel,
                             appUnitsPerDevPixel);
      }

      nsString mimeType;
      AsDoc()->MimeType(mimeType);
      fields->SetAttribute(CacheKey::MimeType, std::move(mimeType));
    }
  }

  if ((aCacheDomain & (CacheDomain::Text | CacheDomain::ScrollPosition |
                       CacheDomain::APZ) ||
       boundsChanged) &&
      mDoc) {
    mDoc->SetViewportCacheDirty(true);
  }

  return fields.forget();
}