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();
}