LocalAccessible.cpp (170932B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 #include "AccEvent.h" 7 #include "LocalAccessible-inl.h" 8 9 #include "EmbeddedObjCollector.h" 10 #include "AccGroupInfo.h" 11 #include "AccIterator.h" 12 #include "CachedTableAccessible.h" 13 #include "CssAltContent.h" 14 #include "DocAccessible-inl.h" 15 #include "mozilla/a11y/AccAttributes.h" 16 #include "mozilla/a11y/DocAccessibleChild.h" 17 #include "mozilla/a11y/Platform.h" 18 #include "mozilla/FocusModel.h" 19 #include "nsAccUtils.h" 20 #include "nsMenuPopupFrame.h" 21 #include "nsAccessibilityService.h" 22 #include "ApplicationAccessible.h" 23 #include "nsGenericHTMLElement.h" 24 #include "NotificationController.h" 25 #include "nsEventShell.h" 26 #include "nsTextEquivUtils.h" 27 #include "EventTree.h" 28 #include "OuterDocAccessible.h" 29 #include "Pivot.h" 30 #include "Relation.h" 31 #include "mozilla/a11y/Role.h" 32 #include "RootAccessible.h" 33 #include "States.h" 34 #include "TextLeafAccessible.h" 35 #include "TextLeafRange.h" 36 #include "TextRange.h" 37 #include "HTMLElementAccessibles.h" 38 #include "HTMLSelectAccessible.h" 39 #include "HTMLTableAccessible.h" 40 #include "ImageAccessible.h" 41 42 #include "nsComputedDOMStyle.h" 43 #include "nsGkAtoms.h" 44 #include "nsIDOMXULButtonElement.h" 45 #include "nsIDOMXULSelectCntrlEl.h" 46 #include "nsIDOMXULSelectCntrlItemEl.h" 47 #include "nsIMutationObserver.h" 48 #include "nsINodeList.h" 49 50 #include "mozilla/dom/Document.h" 51 #include "mozilla/dom/HTMLFormElement.h" 52 #include "mozilla/dom/HTMLAnchorElement.h" 53 #include "mozilla/gfx/Matrix.h" 54 #include "nsIContent.h" 55 #include "nsIFormControl.h" 56 57 #include "nsDisplayList.h" 58 #include "nsLayoutUtils.h" 59 #include "nsPresContext.h" 60 #include "nsIFrame.h" 61 #include "nsTextFrame.h" 62 #include "nsIDocShellTreeItem.h" 63 #include "nsStyleStructInlines.h" 64 #include "nsFocusManager.h" 65 66 #include "nsString.h" 67 #include "nsAtom.h" 68 #include "nsContainerFrame.h" 69 70 #include "mozilla/Assertions.h" 71 #include "mozilla/BasicEvents.h" 72 #include "mozilla/ErrorResult.h" 73 #include "mozilla/FloatingPoint.h" 74 #include "mozilla/PerfStats.h" 75 #include "mozilla/PresShell.h" 76 #include "mozilla/ProfilerMarkers.h" 77 #include "mozilla/ScrollContainerFrame.h" 78 #include "mozilla/StaticPrefs_accessibility.h" 79 #include "mozilla/StaticPrefs_dom.h" 80 #include "mozilla/StaticPrefs_ui.h" 81 #include "mozilla/dom/Element.h" 82 #include "mozilla/dom/HTMLLabelElement.h" 83 #include "mozilla/dom/KeyboardEventBinding.h" 84 #include "mozilla/dom/TreeWalker.h" 85 #include "mozilla/dom/UserActivation.h" 86 87 using namespace mozilla; 88 using namespace mozilla::a11y; 89 90 //////////////////////////////////////////////////////////////////////////////// 91 // LocalAccessible: nsISupports and cycle collection 92 93 NS_IMPL_CYCLE_COLLECTION_CLASS(LocalAccessible) 94 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(LocalAccessible) 95 tmp->Shutdown(); 96 NS_IMPL_CYCLE_COLLECTION_UNLINK_END 97 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(LocalAccessible) 98 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContent, mDoc) 99 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END 100 101 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LocalAccessible) 102 NS_INTERFACE_MAP_ENTRY_CONCRETE(LocalAccessible) 103 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, LocalAccessible) 104 NS_INTERFACE_MAP_END 105 106 NS_IMPL_CYCLE_COLLECTING_ADDREF(LocalAccessible) 107 NS_IMPL_CYCLE_COLLECTING_RELEASE_WITH_DESTROY(LocalAccessible, LastRelease()) 108 109 LocalAccessible::LocalAccessible(nsIContent* aContent, DocAccessible* aDoc) 110 : mContent(aContent), 111 mDoc(aDoc), 112 mParent(nullptr), 113 mIndexInParent(-1), 114 mFirstLineStart(-1), 115 mStateFlags(0), 116 mContextFlags(0), 117 mReorderEventTarget(false), 118 mShowEventTarget(false), 119 mHideEventTarget(false), 120 mIndexOfEmbeddedChild(-1), 121 mGroupInfo(nullptr) {} 122 123 LocalAccessible::~LocalAccessible() { 124 NS_ASSERTION(!mDoc, "LastRelease was never called!?!"); 125 } 126 127 ENameValueFlag LocalAccessible::DirectName(nsString& aName) const { 128 if (!HasOwnContent()) return eNameOK; 129 130 ENameValueFlag nameFlag = ARIAName(aName); 131 if (!aName.IsEmpty()) return nameFlag; 132 133 nameFlag = NativeName(aName); 134 aName.CompressWhitespace(); 135 136 return nameFlag; 137 } 138 139 ENameValueFlag LocalAccessible::Name(nsString& aName) const { 140 aName.Truncate(); 141 142 ENameValueFlag nameFlag = DirectName(aName); 143 if (!aName.IsEmpty()) return nameFlag; 144 145 nsTextEquivUtils::GetNameFromSubtree(this, aName); 146 if (!aName.IsEmpty()) return eNameFromSubtree; 147 148 // In the end get the name from tooltip. 149 if (Tooltip(aName)) { 150 return eNameFromTooltip; 151 } 152 153 if (auto cssAlt = CssAltContent(mContent)) { 154 cssAlt.AppendToString(aName); 155 return eNameOK; 156 } 157 158 aName.SetIsVoid(true); 159 160 return eNameOK; 161 } 162 163 EDescriptionValueFlag LocalAccessible::Description( 164 nsString& aDescription) const { 165 // There are 4 conditions that make an accessible have no accDescription: 166 // 1. it's a text node; or 167 // 2. It has no ARIA describedby or description property 168 // 3. it doesn't have an accName; or 169 // 4. its title attribute already equals to its accName nsAutoString name; 170 171 EDescriptionValueFlag descFlag = eDescriptionOK; 172 aDescription.Truncate(); 173 174 if (!HasOwnContent() || mContent->IsText()) { 175 return descFlag; 176 } 177 178 if (ARIADescription(aDescription)) { 179 descFlag = eDescriptionFromARIA; 180 } 181 182 if (aDescription.IsEmpty()) { 183 NativeDescription(aDescription); 184 aDescription.CompressWhitespace(); 185 } 186 187 if (aDescription.IsEmpty()) { 188 Tooltip(aDescription); 189 } 190 191 if (!aDescription.IsEmpty()) { 192 nsAutoString name; 193 Name(name); 194 // Don't expose a description if it is the same as the name. 195 if (aDescription.Equals(name)) aDescription.Truncate(); 196 } 197 198 return descFlag; 199 } 200 201 KeyBinding LocalAccessible::AccessKey() const { 202 if (!HasOwnContent()) return KeyBinding(); 203 204 uint32_t key = nsCoreUtils::GetAccessKeyFor(mContent); 205 if (!key && mContent->IsElement()) { 206 LocalAccessible* label = nullptr; 207 208 // Copy access key from label node. 209 if (mContent->IsHTMLElement()) { 210 // Unless it is labeled via an ancestor <label>, in which case that would 211 // be redundant. 212 HTMLLabelIterator iter(Document(), this, 213 HTMLLabelIterator::eSkipAncestorLabel); 214 label = iter.Next(); 215 } 216 if (!label) { 217 XULLabelIterator iter(Document(), mContent); 218 label = iter.Next(); 219 } 220 221 if (label) key = nsCoreUtils::GetAccessKeyFor(label->GetContent()); 222 } 223 224 if (!key) return KeyBinding(); 225 226 // Get modifier mask. Use ui.key.generalAccessKey (unless it is -1). 227 switch (StaticPrefs::ui_key_generalAccessKey()) { 228 case -1: 229 break; 230 case dom::KeyboardEvent_Binding::DOM_VK_SHIFT: 231 return KeyBinding(key, KeyBinding::kShift); 232 case dom::KeyboardEvent_Binding::DOM_VK_CONTROL: 233 return KeyBinding(key, KeyBinding::kControl); 234 case dom::KeyboardEvent_Binding::DOM_VK_ALT: 235 return KeyBinding(key, KeyBinding::kAlt); 236 case dom::KeyboardEvent_Binding::DOM_VK_META: 237 return KeyBinding(key, KeyBinding::kMeta); 238 default: 239 return KeyBinding(); 240 } 241 242 // Determine the access modifier used in this context. 243 dom::Document* document = mContent->GetComposedDoc(); 244 if (!document) return KeyBinding(); 245 246 nsCOMPtr<nsIDocShellTreeItem> treeItem(document->GetDocShell()); 247 if (!treeItem) return KeyBinding(); 248 249 nsresult rv = NS_ERROR_FAILURE; 250 int32_t modifierMask = 0; 251 switch (treeItem->ItemType()) { 252 case nsIDocShellTreeItem::typeChrome: 253 modifierMask = StaticPrefs::ui_key_chromeAccess(); 254 rv = NS_OK; 255 break; 256 case nsIDocShellTreeItem::typeContent: 257 modifierMask = StaticPrefs::ui_key_contentAccess(); 258 rv = NS_OK; 259 break; 260 } 261 262 return NS_SUCCEEDED(rv) ? KeyBinding(key, modifierMask) : KeyBinding(); 263 } 264 265 KeyBinding LocalAccessible::KeyboardShortcut() const { return KeyBinding(); } 266 267 uint64_t LocalAccessible::VisibilityState() const { 268 if (IPCAccessibilityActive()) { 269 // Visibility states must be calculated by RemoteAccessible, so there's no 270 // point calculating them here. 271 return 0; 272 } 273 nsIFrame* frame = GetFrame(); 274 if (!frame) { 275 // Element having display:contents is considered visible semantically, 276 // despite it doesn't have a visually visible box. 277 if (nsCoreUtils::IsDisplayContents(mContent)) { 278 return states::OFFSCREEN; 279 } 280 return states::INVISIBLE; 281 } 282 283 if (!frame->StyleVisibility()->IsVisible()) return states::INVISIBLE; 284 285 // It's invisible if the presshell is hidden by a visibility:hidden element in 286 // an ancestor document. 287 if (frame->PresShell()->IsUnderHiddenEmbedderElement()) { 288 return states::INVISIBLE; 289 } 290 291 // Offscreen state if the document's visibility state is not visible. 292 if (Document()->IsHidden()) return states::OFFSCREEN; 293 294 // Walk the parent frame chain to see if the frame is in background tab or 295 // scrolled out. 296 nsIFrame* curFrame = frame; 297 do { 298 if (nsMenuPopupFrame* popup = do_QueryFrame(curFrame)) { 299 return popup->IsOpen() ? 0 : states::INVISIBLE; 300 } 301 302 if (curFrame->StyleUIReset()->mMozSubtreeHiddenOnlyVisually) { 303 // Offscreen state for background tab content. 304 return states::OFFSCREEN; 305 } 306 307 nsIFrame* parentFrame = curFrame->GetParent(); 308 // If contained by scrollable frame then check that at least 12 pixels 309 // around the object is visible, otherwise the object is offscreen. 310 const nscoord kMinPixels = nsPresContext::CSSPixelsToAppUnits(12); 311 if (ScrollContainerFrame* scrollContainerFrame = 312 do_QueryFrame(parentFrame)) { 313 nsRect scrollPortRect = scrollContainerFrame->GetScrollPortRect(); 314 nsRect frameRect = nsLayoutUtils::TransformFrameRectToAncestor( 315 frame, frame->GetRectRelativeToSelf(), parentFrame); 316 if (!scrollPortRect.Contains(frameRect)) { 317 scrollPortRect.Deflate(kMinPixels, kMinPixels); 318 if (!scrollPortRect.Intersects(frameRect)) return states::OFFSCREEN; 319 } 320 } 321 322 if (!parentFrame) { 323 parentFrame = nsLayoutUtils::GetCrossDocParentFrameInProcess(curFrame); 324 // Even if we couldn't find the parent frame, it might mean we are in an 325 // out-of-process iframe, try to see if |frame| is scrolled out in an 326 // scrollable frame in a cross-process ancestor document. 327 if (!parentFrame && 328 nsLayoutUtils::FrameIsMostlyScrolledOutOfViewInCrossProcess( 329 frame, kMinPixels)) { 330 return states::OFFSCREEN; 331 } 332 } 333 334 curFrame = parentFrame; 335 } while (curFrame); 336 337 // Zero area rects can occur in the first frame of a multi-frame text flow, 338 // in which case the rendered text is not empty and the frame should not be 339 // marked invisible. 340 // XXX Can we just remove this check? Why do we need to mark empty 341 // text invisible? 342 if (frame->IsTextFrame() && !frame->HasAnyStateBits(NS_FRAME_OUT_OF_FLOW) && 343 frame->GetRect().IsEmpty()) { 344 nsIFrame::RenderedText text = frame->GetRenderedText( 345 0, UINT32_MAX, nsIFrame::TextOffsetType::OffsetsInContentText, 346 nsIFrame::TrailingWhitespace::DontTrim); 347 if (text.mString.IsEmpty()) { 348 return states::INVISIBLE; 349 } 350 } 351 352 return 0; 353 } 354 355 uint64_t LocalAccessible::NativeState() const { 356 uint64_t state = 0; 357 358 if (!IsInDocument()) state |= states::STALE; 359 360 if (HasOwnContent() && mContent->IsElement()) { 361 dom::ElementState elementState = mContent->AsElement()->State(); 362 363 if (elementState.HasState(dom::ElementState::INVALID)) { 364 state |= states::INVALID; 365 } 366 367 if (elementState.HasState(dom::ElementState::REQUIRED)) { 368 state |= states::REQUIRED; 369 } 370 371 state |= NativeInteractiveState(); 372 } 373 374 // Gather states::INVISIBLE and states::OFFSCREEN flags for this object. 375 state |= VisibilityState(); 376 377 nsIFrame* frame = GetFrame(); 378 if (frame && frame->HasAnyStateBits(NS_FRAME_OUT_OF_FLOW)) { 379 state |= states::FLOATING; 380 } 381 382 // Check if a XUL element has the popup attribute (an attached popup menu). 383 if (HasOwnContent() && mContent->IsXULElement() && 384 mContent->AsElement()->HasAttr(nsGkAtoms::popup)) { 385 state |= states::HASPOPUP; 386 } 387 388 // Bypass the link states specialization for non links. 389 const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); 390 if (!roleMapEntry || roleMapEntry->roleRule == kUseNativeRole || 391 roleMapEntry->role == roles::LINK) { 392 state |= NativeLinkState(); 393 } 394 395 return state; 396 } 397 398 uint64_t LocalAccessible::NativeInteractiveState() const { 399 if (!mContent->IsElement()) return 0; 400 401 if (NativelyUnavailable()) return states::UNAVAILABLE; 402 403 nsIFrame* frame = GetFrame(); 404 auto flags = IsFocusableFlags(0); 405 // If we're caching this remote document in the parent process, we 406 // need to cache focusability irrespective of visibility. Otherwise, 407 // if this document is invisible when it first loads, we'll cache that 408 // all descendants are unfocusable and this won't get updated when the 409 // document becomes visible. Even if we did get notified when the 410 // document becomes visible, it would be wasteful to walk the entire 411 // tree to figure out what is now focusable and push cache updates. 412 // Although ignoring visibility means IsFocusable will return true for 413 // visibility: hidden, etc., this isn't a problem because we don't include 414 // those hidden elements in the a11y tree anyway. 415 if (mDoc->IPCDoc()) { 416 flags |= IsFocusableFlags::IgnoreVisibility; 417 } 418 if (frame && frame->IsFocusable(flags)) { 419 return states::FOCUSABLE; 420 } 421 return 0; 422 } 423 424 uint64_t LocalAccessible::NativeLinkState() const { return 0; } 425 426 bool LocalAccessible::NativelyUnavailable() const { 427 if (mContent->IsHTMLElement()) return mContent->AsElement()->IsDisabled(); 428 429 return mContent->IsElement() && 430 mContent->AsElement()->GetBoolAttr(nsGkAtoms::disabled); 431 } 432 433 Accessible* LocalAccessible::ChildAtPoint(int32_t aX, int32_t aY, 434 EWhichChildAtPoint aWhichChild) { 435 Accessible* child = LocalChildAtPoint(aX, aY, aWhichChild); 436 if (aWhichChild != EWhichChildAtPoint::DirectChild && child && 437 child->IsOuterDoc()) { 438 child = child->ChildAtPoint(aX, aY, aWhichChild); 439 } 440 441 return child; 442 } 443 444 LocalAccessible* LocalAccessible::LocalChildAtPoint( 445 int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) { 446 // If we can't find the point in a child, we will return the fallback answer: 447 // we return |this| if the point is within it, otherwise nullptr. 448 LocalAccessible* fallbackAnswer = nullptr; 449 LayoutDeviceIntRect rect = Bounds(); 450 if (rect.Contains(aX, aY)) fallbackAnswer = this; 451 452 if (nsAccUtils::MustPrune(this)) { // Do not dig any further 453 return fallbackAnswer; 454 } 455 456 // Search an accessible at the given point starting from accessible document 457 // because containing block (see CSS2) for out of flow element (for example, 458 // absolutely positioned element) may be different from its DOM parent and 459 // therefore accessible for containing block may be different from accessible 460 // for DOM parent but GetFrameForPoint() should be called for containing block 461 // to get an out of flow element. 462 DocAccessible* accDocument = Document(); 463 NS_ENSURE_TRUE(accDocument, nullptr); 464 465 nsIFrame* rootFrame = accDocument->GetFrame(); 466 NS_ENSURE_TRUE(rootFrame, nullptr); 467 468 nsIFrame* startFrame = rootFrame; 469 470 // Check whether the point is at popup content. 471 nsIWidget* rootWidget = rootFrame->GetNearestWidget(); 472 NS_ENSURE_TRUE(rootWidget, nullptr); 473 474 LayoutDeviceIntRect rootRect = rootWidget->GetScreenBounds(); 475 476 auto point = LayoutDeviceIntPoint(aX - rootRect.X(), aY - rootRect.Y()); 477 478 nsIFrame* popupFrame = nsLayoutUtils::GetPopupFrameForPoint( 479 accDocument->PresContext()->GetRootPresContext(), rootWidget, point); 480 if (popupFrame) { 481 // If 'this' accessible is not inside the popup then ignore the popup when 482 // searching an accessible at point. 483 DocAccessible* popupDoc = 484 GetAccService()->GetDocAccessible(popupFrame->GetContent()->OwnerDoc()); 485 LocalAccessible* popupAcc = 486 popupDoc->GetAccessibleOrContainer(popupFrame->GetContent()); 487 LocalAccessible* popupChild = this; 488 while (popupChild && !popupChild->IsDoc() && popupChild != popupAcc) { 489 popupChild = popupChild->LocalParent(); 490 } 491 492 if (popupChild == popupAcc) startFrame = popupFrame; 493 } 494 495 nsPresContext* presContext = startFrame->PresContext(); 496 nsRect screenRect = startFrame->GetScreenRectInAppUnits(); 497 nsPoint offset(presContext->DevPixelsToAppUnits(aX) - screenRect.X(), 498 presContext->DevPixelsToAppUnits(aY) - screenRect.Y()); 499 500 nsIFrame* foundFrame = nsLayoutUtils::GetFrameForPoint( 501 RelativeTo{startFrame, ViewportType::Visual}, offset); 502 503 nsIContent* content = nullptr; 504 if (!foundFrame || !(content = foundFrame->GetContent())) { 505 return fallbackAnswer; 506 } 507 508 // Get accessible for the node with the point or the first accessible in 509 // the DOM parent chain. 510 DocAccessible* contentDocAcc = 511 GetAccService()->GetDocAccessible(content->OwnerDoc()); 512 513 // contentDocAcc in some circumstances can be nullptr. See bug 729861 514 NS_ASSERTION(contentDocAcc, "could not get the document accessible"); 515 if (!contentDocAcc) return fallbackAnswer; 516 517 LocalAccessible* accessible = 518 contentDocAcc->GetAccessibleOrContainer(content); 519 if (!accessible) return fallbackAnswer; 520 521 // Hurray! We have an accessible for the frame that layout gave us. 522 // Since DOM node of obtained accessible may be out of flow then we should 523 // ensure obtained accessible is a child of this accessible. 524 LocalAccessible* child = accessible; 525 while (child != this) { 526 LocalAccessible* parent = child->LocalParent(); 527 if (!parent) { 528 // Reached the top of the hierarchy. These bounds were inside an 529 // accessible that is not a descendant of this one. 530 return fallbackAnswer; 531 } 532 533 // If we landed on a legitimate child of |this|, and we want the direct 534 // child, return it here. 535 if (parent == this && aWhichChild == EWhichChildAtPoint::DirectChild) { 536 return child; 537 } 538 539 child = parent; 540 } 541 542 // Manually walk through accessible children and see if the are within this 543 // point. Skip offscreen or invisible accessibles. This takes care of cases 544 // where layout won't walk into things for us, such as image map areas and 545 // sub documents (XXX: subdocuments should be handled by methods of 546 // OuterDocAccessibles). 547 uint32_t childCount = accessible->ChildCount(); 548 if (childCount == 1 && accessible->IsOuterDoc() && 549 accessible->FirstChild()->IsRemote()) { 550 // No local children. 551 return accessible; 552 } 553 for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) { 554 LocalAccessible* child = accessible->LocalChildAt(childIdx); 555 556 LayoutDeviceIntRect childRect = child->Bounds(); 557 if (childRect.Contains(aX, aY) && 558 (child->State() & states::INVISIBLE) == 0) { 559 if (aWhichChild == EWhichChildAtPoint::DeepestChild) { 560 return child->LocalChildAtPoint(aX, aY, 561 EWhichChildAtPoint::DeepestChild); 562 } 563 564 return child; 565 } 566 } 567 568 return accessible; 569 } 570 571 nsIFrame* LocalAccessible::FindNearestAccessibleAncestorFrame() { 572 nsIFrame* frame = GetFrame(); 573 if (frame->StyleDisplay()->mPosition == StylePositionProperty::Fixed && 574 nsLayoutUtils::IsReallyFixedPos(frame)) { 575 return mDoc->PresShellPtr()->GetRootFrame(); 576 } 577 578 if (IsDoc()) { 579 // We bound documents by their own frame, which is their PresShell's root 580 // frame. We cache the document offset elsewhere in BundleFieldsForCache 581 // using the nsGkAtoms::crossorigin attribute. 582 MOZ_ASSERT(frame, "DocAccessibles should always have a frame"); 583 return frame; 584 } 585 586 // Iterate through accessible's ancestors to find one with a frame. 587 LocalAccessible* ancestor = mParent; 588 while (ancestor) { 589 if (nsIFrame* boundingFrame = ancestor->GetFrame()) { 590 return boundingFrame; 591 } 592 ancestor = ancestor->LocalParent(); 593 } 594 595 MOZ_ASSERT_UNREACHABLE("No ancestor with frame?"); 596 return nsLayoutUtils::GetContainingBlockForClientRect(frame); 597 } 598 599 nsRect LocalAccessible::ParentRelativeBounds() { 600 nsIFrame* frame = GetFrame(); 601 if (frame && mContent) { 602 nsIFrame* boundingFrame = FindNearestAccessibleAncestorFrame(); 603 nsRect result = nsLayoutUtils::GetAllInFlowRectsUnion(frame, boundingFrame); 604 605 if (result.IsEmpty()) { 606 // If we end up with a 0x0 rect from above (or one with negative 607 // height/width) we should try using the ink overflow rect instead. If we 608 // use this rect, our relative bounds will match the bounds of what 609 // appears visually. We do this because some web authors (icloud.com for 610 // example) employ things like 0x0 buttons with visual overflow. Without 611 // this, such frames aren't navigable by screen readers. 612 result = frame->InkOverflowRectRelativeToSelf(); 613 result.MoveBy(frame->GetOffsetTo(boundingFrame)); 614 } 615 616 if (boundingFrame->GetRect().IsEmpty() || 617 nsLayoutUtils::GetNextContinuationOrIBSplitSibling(boundingFrame)) { 618 // Constructing a bounding box across a frame that has an IB split means 619 // the origin is likely be different from that of boundingFrame. 620 // Descendants will need their parent-relative bounds adjusted 621 // accordingly, since parent-relative bounds are constructed to the 622 // bounding box of the entire element and not each individual IB split 623 // frame. In the case that boundingFrame's rect is empty, 624 // GetAllInFlowRectsUnion might exclude its origin. For example, if 625 // boundingFrame is empty with an origin of (0, -840) but has a non-empty 626 // ib-split-sibling with (0, 0), the union rect will originate at (0, 0). 627 // This means the bounds returned for our parent Accessible might be 628 // offset from boundingFrame's rect. Since result is currently relative to 629 // boundingFrame's rect, we might need to adjust it to make it parent 630 // relative. 631 nsRect boundingUnion = 632 nsLayoutUtils::GetAllInFlowRectsUnion(boundingFrame, boundingFrame); 633 if (!boundingUnion.IsEmpty()) { 634 // The origin of boundingUnion is relative to boundingFrame, meaning 635 // when we call MoveBy on result with this value we're offsetting 636 // `result` by the distance boundingFrame's origin was moved to 637 // construct its bounding box. 638 result.MoveBy(-boundingUnion.TopLeft()); 639 } else { 640 // Since GetAllInFlowRectsUnion returned an empty rect on our parent 641 // Accessible, we would have used the ink overflow rect. However, 642 // GetAllInFlowRectsUnion calculates relative to the bounding frame's 643 // main rect, not its ink overflow rect. We need to adjust for the ink 644 // overflow offset to make our result parent relative. 645 nsRect boundingOverflow = 646 boundingFrame->InkOverflowRectRelativeToSelf(); 647 result.MoveBy(-boundingOverflow.TopLeft()); 648 } 649 } 650 651 if (frame->StyleDisplay()->mPosition == StylePositionProperty::Fixed && 652 nsLayoutUtils::IsReallyFixedPos(frame)) { 653 // If we're dealing with a fixed position frame, we've already made it 654 // relative to the document which should have gotten rid of its scroll 655 // offset. 656 return result; 657 } 658 659 if (ScrollContainerFrame* sf = 660 mParent == mDoc 661 ? mDoc->PresShellPtr()->GetRootScrollContainerFrame() 662 : boundingFrame->GetScrollTargetFrame()) { 663 // If boundingFrame has a scroll position, result is currently relative 664 // to that. Instead, we want result to remain the same regardless of 665 // scrolling. We then subtract the scroll position later when 666 // calculating absolute bounds. We do this because we don't want to push 667 // cache updates for the bounds of all descendants every time we scroll. 668 nsPoint scrollPos = sf->GetScrollPosition().ApplyResolution( 669 mDoc->PresShellPtr()->GetResolution()); 670 result.MoveBy(scrollPos.x, scrollPos.y); 671 } 672 673 return result; 674 } 675 676 return nsRect(); 677 } 678 679 nsRect LocalAccessible::RelativeBounds(nsIFrame** aBoundingFrame) const { 680 nsIFrame* frame = GetFrame(); 681 if (frame && mContent) { 682 *aBoundingFrame = nsLayoutUtils::GetContainingBlockForClientRect(frame); 683 nsRect unionRect = nsLayoutUtils::GetAllInFlowRectsUnion( 684 frame, *aBoundingFrame, 685 nsLayoutUtils::GetAllInFlowRectsFlag::AccountForTransforms); 686 687 if (unionRect.IsEmpty()) { 688 // If we end up with a 0x0 rect from above (or one with negative 689 // height/width) we should try using the ink overflow rect instead. If we 690 // use this rect, our relative bounds will match the bounds of what 691 // appears visually. We do this because some web authors (icloud.com for 692 // example) employ things like 0x0 buttons with visual overflow. Without 693 // this, such frames aren't navigable by screen readers. 694 nsRect overflow = frame->InkOverflowRectRelativeToSelf(); 695 nsLayoutUtils::TransformRect(frame, *aBoundingFrame, overflow); 696 return overflow; 697 } 698 699 return unionRect; 700 } 701 702 return nsRect(); 703 } 704 705 nsRect LocalAccessible::BoundsInAppUnits() const { 706 nsIFrame* boundingFrame = nullptr; 707 nsRect unionRectTwips = RelativeBounds(&boundingFrame); 708 if (!boundingFrame) { 709 return nsRect(); 710 } 711 712 PresShell* presShell = mDoc->PresContext()->PresShell(); 713 714 // We need to inverse translate with the offset of the edge of the visual 715 // viewport from top edge of the layout viewport. 716 nsPoint viewportOffset = presShell->GetVisualViewportOffset() - 717 presShell->GetLayoutViewportOffset(); 718 unionRectTwips.MoveBy(-viewportOffset); 719 720 // We need to take into account a non-1 resolution set on the presshell. 721 // This happens with async pinch zooming. Here we scale the bounds before 722 // adding the screen-relative offset. 723 unionRectTwips.ScaleRoundOut(presShell->GetResolution()); 724 // We have the union of the rectangle, now we need to put it in absolute 725 // screen coords. 726 nsRect orgRectPixels = boundingFrame->GetScreenRectInAppUnits(); 727 unionRectTwips.MoveBy(orgRectPixels.X(), orgRectPixels.Y()); 728 729 return unionRectTwips; 730 } 731 732 LayoutDeviceIntRect LocalAccessible::Bounds() const { 733 return LayoutDeviceIntRect::FromAppUnitsToNearest( 734 BoundsInAppUnits(), mDoc->PresContext()->AppUnitsPerDevPixel()); 735 } 736 737 void LocalAccessible::SetSelected(bool aSelect) { 738 if (!HasOwnContent()) return; 739 740 if (nsAccUtils::GetSelectableContainer(this, State()) && aSelect) { 741 TakeFocus(); 742 } 743 } 744 745 void LocalAccessible::TakeSelection() { 746 LocalAccessible* select = nsAccUtils::GetSelectableContainer(this, State()); 747 if (select) { 748 if (select->State() & states::MULTISELECTABLE) select->UnselectAll(); 749 SetSelected(true); 750 } 751 } 752 753 void LocalAccessible::TakeFocus() const { 754 nsIFrame* frame = GetFrame(); 755 if (!frame) return; 756 757 nsIContent* focusContent = mContent; 758 759 // If the accessible focus is managed by container widget then focus the 760 // widget and set the accessible as its current item. 761 if (!frame->IsFocusable()) { 762 LocalAccessible* widget = ContainerWidget(); 763 if (widget && widget->AreItemsOperable()) { 764 nsIContent* widgetElm = widget->GetContent(); 765 nsIFrame* widgetFrame = widgetElm->GetPrimaryFrame(); 766 if (widgetFrame && widgetFrame->IsFocusable()) { 767 focusContent = widgetElm; 768 widget->SetCurrentItem(this); 769 } 770 } 771 } 772 773 if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) { 774 dom::AutoHandlingUserInputStatePusher inputStatePusher(true); 775 // XXXbz: Can we actually have a non-element content here? 776 RefPtr<dom::Element> element = dom::Element::FromNodeOrNull(focusContent); 777 fm->SetFocus(element, 0); 778 } 779 } 780 781 void LocalAccessible::NameFromAssociatedXULLabel(DocAccessible* aDocument, 782 nsIContent* aElm, 783 nsString& aName) { 784 LocalAccessible* label = nullptr; 785 XULLabelIterator iter(aDocument, aElm); 786 while ((label = iter.Next())) { 787 // Check if label's value attribute is used 788 label->Elm()->GetAttr(nsGkAtoms::value, aName); 789 if (aName.IsEmpty()) { 790 // If no value attribute, a non-empty label must contain 791 // children that define its text -- possibly using HTML 792 nsTextEquivUtils::AppendTextEquivFromContent(label, label->Elm(), &aName); 793 } 794 } 795 aName.CompressWhitespace(); 796 } 797 798 void LocalAccessible::XULElmName(DocAccessible* aDocument, nsIContent* aElm, 799 nsString& aName) { 800 /** 801 * 3 main cases for XUL Controls to be labeled 802 * 1 - control contains label="foo" 803 * 2 - non-child label contains control="controlID" 804 * - label has either value="foo" or children 805 * 3 - name from subtree; e.g. a child label element 806 * Cases 1 and 2 are handled here. 807 * Case 3 is handled by GetNameFromSubtree called in NativeName. 808 * Once a label is found, the search is discontinued, so a control 809 * that has a label attribute as well as having a label external to 810 * the control that uses the control="controlID" syntax will use 811 * the label attribute for its Name. 812 */ 813 814 // CASE #1 (via label attribute) -- great majority of the cases 815 // Only do this if this is not a select control element, which uses label 816 // attribute to indicate, which option is selected. 817 nsCOMPtr<nsIDOMXULSelectControlElement> select = 818 aElm->AsElement()->AsXULSelectControl(); 819 if (!select) { 820 aElm->AsElement()->GetAttr(nsGkAtoms::label, aName); 821 } 822 823 // CASE #2 -- label as <label control="id" ... ></label> 824 if (aName.IsEmpty()) { 825 NameFromAssociatedXULLabel(aDocument, aElm, aName); 826 } 827 828 aName.CompressWhitespace(); 829 } 830 831 nsresult LocalAccessible::HandleAccEvent(AccEvent* aEvent) { 832 NS_ENSURE_ARG_POINTER(aEvent); 833 834 if (profiler_thread_is_being_profiled_for_markers()) { 835 nsAutoCString strEventType; 836 GetAccService()->GetStringEventType(aEvent->GetEventType(), strEventType); 837 nsAutoCString strMarker; 838 strMarker.AppendLiteral("A11y Event - "); 839 strMarker.Append(strEventType); 840 PROFILER_MARKER_UNTYPED(strMarker, A11Y); 841 } 842 843 if (IPCAccessibilityActive() && Document()) { 844 DocAccessibleChild* ipcDoc = mDoc->IPCDoc(); 845 // If ipcDoc is null, we can't fire the event to the client. We shouldn't 846 // have fired the event in the first place, since this makes events 847 // inconsistent for local and remote documents. To avoid this, don't call 848 // nsEventShell::FireEvent on a DocAccessible for which 849 // HasLoadState(eTreeConstructed) is false. 850 MOZ_ASSERT(ipcDoc); 851 if (ipcDoc) { 852 uint64_t id = aEvent->GetAccessible()->ID(); 853 854 auto getCaretRect = [aEvent] { 855 HyperTextAccessible* ht = aEvent->GetAccessible()->AsHyperText(); 856 if (ht) { 857 auto [rect, widget] = ht->GetCaretRect(); 858 // Remove doc offset and reapply in parent. 859 LayoutDeviceIntRect docBounds = ht->Document()->Bounds(); 860 rect.MoveBy(-docBounds.X(), -docBounds.Y()); 861 return rect; 862 } 863 return LayoutDeviceIntRect(); 864 }; 865 866 switch (aEvent->GetEventType()) { 867 case nsIAccessibleEvent::EVENT_SHOW: 868 ipcDoc->ShowEvent(downcast_accEvent(aEvent)); 869 break; 870 871 case nsIAccessibleEvent::EVENT_HIDE: 872 ipcDoc->PushMutationEventData( 873 HideEventData{id, aEvent->IsFromUserInput()}); 874 break; 875 876 case nsIAccessibleEvent::EVENT_INNER_REORDER: 877 case nsIAccessibleEvent::EVENT_REORDER: 878 if (IsTable()) { 879 SendCache(CacheDomain::Table, CacheUpdateType::Update, 880 /*aAppendEventData*/ true); 881 } 882 883 #if defined(XP_WIN) 884 if (HasOwnContent() && mContent->IsMathMLElement()) { 885 // For any change in a MathML subtree, update the innerHTML cache on 886 // the root math element. 887 for (LocalAccessible* acc = this; acc; acc = acc->LocalParent()) { 888 if (acc->HasOwnContent() && 889 acc->mContent->IsMathMLElement(nsGkAtoms::math)) { 890 mDoc->QueueCacheUpdate(acc, CacheDomain::InnerHTML); 891 } 892 } 893 } 894 #endif // defined(XP_WIN) 895 896 // reorder events on the application acc aren't necessary to tell the 897 // parent about new top level documents. 898 if (!aEvent->GetAccessible()->IsApplication()) { 899 ipcDoc->PushMutationEventData( 900 ReorderEventData{id, aEvent->GetEventType()}); 901 } 902 break; 903 case nsIAccessibleEvent::EVENT_STATE_CHANGE: { 904 AccStateChangeEvent* event = downcast_accEvent(aEvent); 905 ipcDoc->SendStateChangeEvent(id, event->GetState(), 906 event->IsStateEnabled()); 907 break; 908 } 909 case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: { 910 AccCaretMoveEvent* event = downcast_accEvent(aEvent); 911 ipcDoc->SendCaretMoveEvent( 912 id, getCaretRect(), event->GetCaretOffset(), 913 event->IsSelectionCollapsed(), event->IsAtEndOfLine(), 914 event->GetGranularity(), event->IsFromUserInput()); 915 break; 916 } 917 case nsIAccessibleEvent::EVENT_TEXT_INSERTED: 918 case nsIAccessibleEvent::EVENT_TEXT_REMOVED: { 919 AccTextChangeEvent* event = downcast_accEvent(aEvent); 920 ipcDoc->PushMutationEventData(TextChangeEventData{ 921 id, event->ModifiedText(), event->GetStartOffset(), 922 event->GetLength(), event->IsTextInserted(), 923 event->IsFromUserInput()}); 924 break; 925 } 926 case nsIAccessibleEvent::EVENT_SELECTION: 927 case nsIAccessibleEvent::EVENT_SELECTION_ADD: 928 case nsIAccessibleEvent::EVENT_SELECTION_REMOVE: { 929 AccSelChangeEvent* selEvent = downcast_accEvent(aEvent); 930 ipcDoc->SendSelectionEvent(id, selEvent->Widget()->ID(), 931 aEvent->GetEventType()); 932 break; 933 } 934 case nsIAccessibleEvent::EVENT_FOCUS: 935 ipcDoc->SendFocusEvent(id, getCaretRect()); 936 break; 937 case nsIAccessibleEvent::EVENT_SCROLLING_END: 938 case nsIAccessibleEvent::EVENT_SCROLLING: { 939 AccScrollingEvent* scrollingEvent = downcast_accEvent(aEvent); 940 ipcDoc->SendScrollingEvent( 941 id, aEvent->GetEventType(), scrollingEvent->ScrollX(), 942 scrollingEvent->ScrollY(), scrollingEvent->MaxScrollX(), 943 scrollingEvent->MaxScrollY()); 944 break; 945 } 946 #if !defined(XP_WIN) 947 case nsIAccessibleEvent::EVENT_ANNOUNCEMENT: { 948 AccAnnouncementEvent* announcementEvent = downcast_accEvent(aEvent); 949 ipcDoc->SendAnnouncementEvent(id, announcementEvent->Announcement(), 950 announcementEvent->Priority()); 951 break; 952 } 953 #endif // !defined(XP_WIN) 954 case nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED: { 955 AccTextSelChangeEvent* textSelChangeEvent = downcast_accEvent(aEvent); 956 AutoTArray<TextRange, 1> ranges; 957 textSelChangeEvent->SelectionRanges(&ranges); 958 nsTArray<TextRangeData> textRangeData(ranges.Length()); 959 for (size_t i = 0; i < ranges.Length(); i++) { 960 const TextRange& range = ranges.ElementAt(i); 961 LocalAccessible* start = range.StartContainer()->AsLocal(); 962 LocalAccessible* end = range.EndContainer()->AsLocal(); 963 textRangeData.AppendElement(TextRangeData(start->ID(), end->ID(), 964 range.StartOffset(), 965 range.EndOffset())); 966 } 967 ipcDoc->SendTextSelectionChangeEvent(id, textRangeData); 968 break; 969 } 970 case nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE: 971 case nsIAccessibleEvent::EVENT_NAME_CHANGE: { 972 SendCache(CacheDomain::NameAndDescription, CacheUpdateType::Update); 973 ipcDoc->SendEvent(id, aEvent->GetEventType()); 974 break; 975 } 976 case nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE: 977 case nsIAccessibleEvent::EVENT_VALUE_CHANGE: { 978 SendCache(CacheDomain::Value, CacheUpdateType::Update); 979 ipcDoc->SendEvent(id, aEvent->GetEventType()); 980 break; 981 } 982 default: 983 ipcDoc->SendEvent(id, aEvent->GetEventType()); 984 } 985 } 986 } 987 988 if (nsCoreUtils::AccEventObserversExist()) { 989 nsCoreUtils::DispatchAccEvent(MakeXPCEvent(aEvent)); 990 } 991 992 if (IPCAccessibilityActive()) { 993 return NS_OK; 994 } 995 996 if (IsDefunct()) { 997 // This could happen if there is an XPCOM observer, since script might run 998 // which mutates the tree. 999 return NS_OK; 1000 } 1001 1002 LocalAccessible* target = aEvent->GetAccessible(); 1003 switch (aEvent->GetEventType()) { 1004 case nsIAccessibleEvent::EVENT_SHOW: { 1005 // Scope for PerfStats 1006 AUTO_PROFILER_MARKER_TEXT("a11y::PlatformShowHideEvent", A11Y, {}, ""_ns); 1007 PerfStats::AutoMetricRecording< 1008 PerfStats::Metric::A11Y_PlatformShowHideEvent> 1009 autoRecording; 1010 // WITHIN THIS SCOPE, DO NOT ADD CODE ABOVE THIS BLOCK: 1011 // THIS CODE IS MEASURING TIMINGS. 1012 PlatformShowHideEvent(target, target->LocalParent(), true, 1013 aEvent->IsFromUserInput()); 1014 break; 1015 } 1016 case nsIAccessibleEvent::EVENT_HIDE: { 1017 // Scope for PerfStats 1018 AUTO_PROFILER_MARKER_TEXT("a11y::PlatformShowHideEvent", A11Y, {}, ""_ns); 1019 PerfStats::AutoMetricRecording< 1020 PerfStats::Metric::A11Y_PlatformShowHideEvent> 1021 autoRecording; 1022 // WITHIN THIS SCOPE, DO NOT ADD CODE ABOVE THIS BLOCK: 1023 // THIS CODE IS MEASURING TIMINGS. 1024 PlatformShowHideEvent(target, target->LocalParent(), false, 1025 aEvent->IsFromUserInput()); 1026 break; 1027 } 1028 case nsIAccessibleEvent::EVENT_STATE_CHANGE: { 1029 AccStateChangeEvent* event = downcast_accEvent(aEvent); 1030 PlatformStateChangeEvent(target, event->GetState(), 1031 event->IsStateEnabled()); 1032 break; 1033 } 1034 case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: { 1035 AccCaretMoveEvent* event = downcast_accEvent(aEvent); 1036 PlatformCaretMoveEvent(target, event->GetCaretOffset(), 1037 event->IsSelectionCollapsed(), 1038 event->GetGranularity(), event->IsFromUserInput()); 1039 break; 1040 } 1041 case nsIAccessibleEvent::EVENT_TEXT_INSERTED: 1042 case nsIAccessibleEvent::EVENT_TEXT_REMOVED: { 1043 AccTextChangeEvent* event = downcast_accEvent(aEvent); 1044 const nsString& text = event->ModifiedText(); 1045 PlatformTextChangeEvent(target, text, event->GetStartOffset(), 1046 event->GetLength(), event->IsTextInserted(), 1047 event->IsFromUserInput()); 1048 break; 1049 } 1050 case nsIAccessibleEvent::EVENT_SELECTION: 1051 case nsIAccessibleEvent::EVENT_SELECTION_ADD: 1052 case nsIAccessibleEvent::EVENT_SELECTION_REMOVE: { 1053 AccSelChangeEvent* selEvent = downcast_accEvent(aEvent); 1054 PlatformSelectionEvent(target, selEvent->Widget(), 1055 aEvent->GetEventType()); 1056 break; 1057 } 1058 case nsIAccessibleEvent::EVENT_FOCUS: { 1059 PlatformFocusEvent(target); 1060 break; 1061 } 1062 #if defined(ANDROID) 1063 case nsIAccessibleEvent::EVENT_SCROLLING_END: 1064 case nsIAccessibleEvent::EVENT_SCROLLING: { 1065 AccScrollingEvent* scrollingEvent = downcast_accEvent(aEvent); 1066 PlatformScrollingEvent( 1067 target, aEvent->GetEventType(), scrollingEvent->ScrollX(), 1068 scrollingEvent->ScrollY(), scrollingEvent->MaxScrollX(), 1069 scrollingEvent->MaxScrollY()); 1070 break; 1071 } 1072 case nsIAccessibleEvent::EVENT_ANNOUNCEMENT: { 1073 AccAnnouncementEvent* announcementEvent = downcast_accEvent(aEvent); 1074 PlatformAnnouncementEvent(target, announcementEvent->Announcement(), 1075 announcementEvent->Priority()); 1076 break; 1077 } 1078 #endif // defined(ANDROID) 1079 #if defined(MOZ_WIDGET_COCOA) 1080 case nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED: { 1081 AccTextSelChangeEvent* textSelChangeEvent = downcast_accEvent(aEvent); 1082 AutoTArray<TextRange, 1> ranges; 1083 textSelChangeEvent->SelectionRanges(&ranges); 1084 PlatformTextSelectionChangeEvent(target, ranges); 1085 break; 1086 } 1087 #endif // defined(MOZ_WIDGET_COCOA) 1088 default: 1089 PlatformEvent(target, aEvent->GetEventType()); 1090 } 1091 1092 return NS_OK; 1093 } 1094 1095 already_AddRefed<AccAttributes> LocalAccessible::Attributes() { 1096 RefPtr<AccAttributes> attributes = NativeAttributes(); 1097 if (!HasOwnContent() || !mContent->IsElement()) return attributes.forget(); 1098 1099 // 'xml-roles' attribute coming from ARIA. 1100 nsString xmlRoles; 1101 if (nsAccUtils::GetARIAAttr(mContent->AsElement(), nsGkAtoms::role, 1102 xmlRoles) && 1103 !xmlRoles.IsEmpty()) { 1104 attributes->SetAttribute(nsGkAtoms::xmlroles, std::move(xmlRoles)); 1105 } else if (nsAtom* landmark = LandmarkRole()) { 1106 // 'xml-roles' attribute for landmark. 1107 attributes->SetAttribute(nsGkAtoms::xmlroles, landmark); 1108 } 1109 1110 // Expose object attributes from ARIA attributes. 1111 aria::AttrIterator attribIter(mContent); 1112 while (attribIter.Next()) { 1113 if (attribIter.AttrName() == nsGkAtoms::aria_placeholder && 1114 attributes->HasAttribute(nsGkAtoms::placeholder)) { 1115 // If there is an HTML placeholder attribute exposed by 1116 // HTMLTextFieldAccessible::NativeAttributes, don't expose 1117 // aria-placeholder. 1118 continue; 1119 } 1120 attribIter.ExposeAttr(attributes); 1121 } 1122 1123 if (HasCustomActions()) { 1124 attributes->SetAttribute(nsGkAtoms::hasActions, true); 1125 } 1126 1127 // If there is no aria-live attribute then expose default value of 'live' 1128 // object attribute used for ARIA role of this accessible. 1129 const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); 1130 if (roleMapEntry) { 1131 if (roleMapEntry->Is(nsGkAtoms::searchbox)) { 1132 attributes->SetAttribute(nsGkAtoms::textInputType, nsGkAtoms::search); 1133 } 1134 1135 if (!attributes->HasAttribute(nsGkAtoms::aria_live)) { 1136 nsString live; 1137 if (nsAccUtils::GetLiveAttrValue(roleMapEntry->liveAttRule, live)) { 1138 attributes->SetAttribute(nsGkAtoms::aria_live, std::move(live)); 1139 } 1140 } 1141 } 1142 1143 nsString detailsFrom; 1144 AssociatedElementsIterator iter(mDoc, Elm(), nsGkAtoms::aria_details); 1145 if (iter.Next()) { 1146 detailsFrom.AssignLiteral("aria-details"); 1147 } else if (GetCommandForDetailsRelation()) { 1148 detailsFrom.AssignLiteral("command-for"); 1149 } else if (GetPopoverTargetDetailsRelation()) { 1150 detailsFrom.AssignLiteral("popover-target"); 1151 } else if (GetAnchorPositionTargetDetailsRelation()) { 1152 detailsFrom.AssignLiteral("css-anchor"); 1153 } 1154 1155 if (!detailsFrom.IsEmpty()) { 1156 attributes->SetAttribute(nsGkAtoms::details_from, std::move(detailsFrom)); 1157 } 1158 1159 return attributes.forget(); 1160 } 1161 1162 already_AddRefed<AccAttributes> LocalAccessible::NativeAttributes() { 1163 RefPtr<AccAttributes> attributes = new AccAttributes(); 1164 1165 // We support values, so expose the string value as well, via the valuetext 1166 // object attribute. We test for the value interface because we don't want 1167 // to expose traditional Value() information such as URL's on links and 1168 // documents, or text in an input. 1169 if (HasNumericValue()) { 1170 nsString valuetext; 1171 Value(valuetext); 1172 attributes->SetAttribute(nsGkAtoms::aria_valuetext, std::move(valuetext)); 1173 } 1174 1175 // Expose checkable object attribute if the accessible has checkable state 1176 if (State() & states::CHECKABLE) { 1177 attributes->SetAttribute(nsGkAtoms::checkable, true); 1178 } 1179 1180 // Expose 'explicit-name' attribute. 1181 nsAutoString name; 1182 if (Name(name) != eNameFromSubtree && !name.IsVoid()) { 1183 attributes->SetAttribute(nsGkAtoms::explicit_name, true); 1184 } 1185 1186 bool hierarchical = false; 1187 uint32_t itemCount = AccGroupInfo::TotalItemCount(this, &hierarchical); 1188 if (itemCount) { 1189 attributes->SetAttribute(nsGkAtoms::child_item_count, 1190 static_cast<int32_t>(itemCount)); 1191 } 1192 1193 if (hierarchical) { 1194 attributes->SetAttribute(nsGkAtoms::tree, true); 1195 } 1196 1197 // If the accessible doesn't have own content (such as list item bullet or 1198 // xul tree item) then don't calculate content based attributes. 1199 if (!HasOwnContent()) return attributes.forget(); 1200 1201 // Get container-foo computed live region properties based on the closest 1202 // container with the live region attribute. Inner nodes override outer nodes 1203 // within the same document. The inner nodes can be used to override live 1204 // region behavior on more general outer nodes. 1205 nsAccUtils::SetLiveContainerAttributes(attributes, this); 1206 1207 if (!mContent->IsElement()) return attributes.forget(); 1208 1209 nsString id; 1210 if (nsCoreUtils::GetID(mContent, id)) { 1211 attributes->SetAttribute(nsGkAtoms::id, std::move(id)); 1212 } 1213 1214 // Expose class because it may have useful microformat information. 1215 nsString _class; 1216 if (mContent->AsElement()->GetAttr(nsGkAtoms::_class, _class)) { 1217 attributes->SetAttribute(nsGkAtoms::_class, std::move(_class)); 1218 } 1219 1220 // Expose tag. 1221 attributes->SetAttribute(nsGkAtoms::tag, mContent->NodeInfo()->NameAtom()); 1222 1223 if (auto htmlElement = nsGenericHTMLElement::FromNode(mContent)) { 1224 // Expose draggable object attribute. 1225 if (htmlElement->Draggable()) { 1226 attributes->SetAttribute(nsGkAtoms::draggable, true); 1227 } 1228 nsString popover; 1229 htmlElement->GetPopover(popover); 1230 if (!popover.IsEmpty()) { 1231 attributes->SetAttribute(nsGkAtoms::ispopup, std::move(popover)); 1232 } 1233 } 1234 1235 // Don't calculate CSS-based object attributes when: 1236 // 1. There is no frame (e.g. the accessible is unattached from the tree). 1237 // 2. This is an image map area. CSS is irrelevant here. Furthermore, we won't 1238 // be able to get the computed style if the map is unslotted in a shadow host. 1239 nsIFrame* f = mContent->GetPrimaryFrame(); 1240 if (!f || mContent->IsHTMLElement(nsGkAtoms::area)) { 1241 return attributes.forget(); 1242 } 1243 1244 // Expose 'display' attribute. 1245 if (RefPtr<nsAtom> display = DisplayStyle()) { 1246 attributes->SetAttribute(nsGkAtoms::display, display); 1247 } 1248 1249 const ComputedStyle& style = *f->Style(); 1250 auto Atomize = [&](NonCustomCSSPropertyId aId) -> RefPtr<nsAtom> { 1251 nsAutoCString value; 1252 style.GetComputedPropertyValue(aId, value); 1253 return NS_Atomize(value); 1254 }; 1255 1256 // Expose 'text-align' attribute. 1257 attributes->SetAttribute(nsGkAtoms::textAlign, 1258 Atomize(eCSSProperty_text_align)); 1259 1260 // Expose 'text-indent' attribute. 1261 attributes->SetAttribute(nsGkAtoms::textIndent, 1262 Atomize(eCSSProperty_text_indent)); 1263 1264 auto GetMargin = [&](mozilla::Side aSide) -> CSSCoord { 1265 // This is here only to guarantee that we do the same as getComputedStyle 1266 // does, so that we don't hit precision errors in tests. 1267 const auto margin = 1268 f->StyleMargin()->GetMargin(aSide, AnchorPosResolutionParams::From(f)); 1269 if (margin->ConvertsToLength()) { 1270 return margin->AsLengthPercentage().ToLengthInCSSPixels(); 1271 } 1272 1273 nscoord coordVal = f->GetUsedMargin().Side(aSide); 1274 return CSSPixel::FromAppUnits(coordVal); 1275 }; 1276 1277 // Expose 'margin-left' attribute. 1278 attributes->SetAttribute(nsGkAtoms::marginLeft, GetMargin(eSideLeft)); 1279 1280 // Expose 'margin-right' attribute. 1281 attributes->SetAttribute(nsGkAtoms::marginRight, GetMargin(eSideRight)); 1282 1283 // Expose 'margin-top' attribute. 1284 attributes->SetAttribute(nsGkAtoms::marginTop, GetMargin(eSideTop)); 1285 1286 // Expose 'margin-bottom' attribute. 1287 attributes->SetAttribute(nsGkAtoms::marginBottom, GetMargin(eSideBottom)); 1288 1289 // Expose data-at-shortcutkeys attribute for web applications and virtual 1290 // cursors. Currently mostly used by JAWS. 1291 nsString atShortcutKeys; 1292 if (mContent->AsElement()->GetAttr( 1293 kNameSpaceID_None, nsGkAtoms::dataAtShortcutkeys, atShortcutKeys)) { 1294 attributes->SetAttribute(nsGkAtoms::dataAtShortcutkeys, 1295 std::move(atShortcutKeys)); 1296 } 1297 1298 return attributes.forget(); 1299 } 1300 1301 bool LocalAccessible::AttributeChangesState(nsAtom* aAttribute) { 1302 return aAttribute == nsGkAtoms::aria_disabled || 1303 // The HTML element disabled state gets handled in 1304 // DocAccessible::ElementStateChanged. This matches 1305 // LocalAccessible::NativelyUnavailable. 1306 (aAttribute == nsGkAtoms::disabled && !mContent->IsHTMLElement()) || 1307 aAttribute == nsGkAtoms::tabindex || 1308 aAttribute == nsGkAtoms::aria_required || 1309 aAttribute == nsGkAtoms::aria_invalid || 1310 aAttribute == nsGkAtoms::aria_expanded || 1311 aAttribute == nsGkAtoms::aria_checked || 1312 (aAttribute == nsGkAtoms::aria_pressed && IsButton()) || 1313 aAttribute == nsGkAtoms::aria_readonly || 1314 aAttribute == nsGkAtoms::aria_current || 1315 aAttribute == nsGkAtoms::aria_haspopup || 1316 aAttribute == nsGkAtoms::aria_busy || 1317 aAttribute == nsGkAtoms::aria_multiline || 1318 aAttribute == nsGkAtoms::aria_multiselectable || 1319 // We track this for focusable state update 1320 aAttribute == nsGkAtoms::commandfor || 1321 aAttribute == nsGkAtoms::contenteditable || 1322 aAttribute == nsGkAtoms::popovertarget; 1323 } 1324 1325 void LocalAccessible::DOMAttributeChanged(int32_t aNameSpaceID, 1326 nsAtom* aAttribute, 1327 AttrModType aModType, 1328 const nsAttrValue* aOldValue, 1329 uint64_t aOldState) { 1330 // Fire accessible event after short timer, because we need to wait for 1331 // DOM attribute & resulting layout to actually change. Otherwise, 1332 // assistive technology will retrieve the wrong state/value/selection info. 1333 1334 CssAltContent::HandleAttributeChange(mContent, aNameSpaceID, aAttribute); 1335 1336 // XXX todo 1337 // We still need to handle special HTML cases here 1338 // For example, if an <img>'s usemap attribute is modified 1339 // Otherwise it may just be a state change, for example an object changing 1340 // its visibility 1341 // 1342 // XXX todo: report aria state changes for "undefined" literal value changes 1343 // filed as bug 472142 1344 // 1345 // XXX todo: invalidate accessible when aria state changes affect exposed 1346 // role filed as bug 472143 1347 1348 if (AttributeChangesState(aAttribute)) { 1349 uint64_t currState = State(); 1350 uint64_t diffState = currState ^ aOldState; 1351 if (diffState) { 1352 for (uint64_t state = 1; state <= states::LAST_ENTRY; state <<= 1) { 1353 if (diffState & state) { 1354 RefPtr<AccEvent> stateChangeEvent = 1355 new AccStateChangeEvent(this, state, (currState & state)); 1356 mDoc->FireDelayedEvent(stateChangeEvent); 1357 } 1358 } 1359 } 1360 } 1361 1362 if (aAttribute == nsGkAtoms::_class) { 1363 mDoc->QueueCacheUpdate(this, CacheDomain::DOMNodeIDAndClass); 1364 return; 1365 } 1366 1367 // When a details object has its open attribute changed 1368 // we should fire a state-change event on the accessible of 1369 // its main summary 1370 if (aAttribute == nsGkAtoms::open) { 1371 // FromDetails checks if the given accessible belongs to 1372 // a details frame and also locates the accessible of its 1373 // main summary. 1374 if (HTMLSummaryAccessible* summaryAccessible = 1375 HTMLSummaryAccessible::FromDetails(this)) { 1376 RefPtr<AccEvent> expandedChangeEvent = 1377 new AccStateChangeEvent(summaryAccessible, states::EXPANDED); 1378 mDoc->FireDelayedEvent(expandedChangeEvent); 1379 return; 1380 } 1381 } 1382 1383 // Check for namespaced ARIA attribute 1384 if (aNameSpaceID == kNameSpaceID_None) { 1385 // Check for hyphenated aria-foo property? 1386 if (StringBeginsWith(nsDependentAtomString(aAttribute), u"aria-"_ns)) { 1387 uint8_t attrFlags = aria::AttrCharacteristicsFor(aAttribute); 1388 if (!(attrFlags & ATTR_BYPASSOBJ)) { 1389 mDoc->QueueCacheUpdate(this, CacheDomain::ARIA); 1390 // For aria attributes like drag and drop changes we fire a generic 1391 // attribute change event; at least until native API comes up with a 1392 // more meaningful event. 1393 RefPtr<AccEvent> event = 1394 new AccObjectAttrChangedEvent(this, aAttribute); 1395 mDoc->FireDelayedEvent(event); 1396 } 1397 } 1398 } 1399 1400 if (aAttribute == nsGkAtoms::aria_actions && IsAdditionOrRemoval(aModType)) { 1401 // We only care about the presence of aria-actions, not its value. 1402 mDoc->QueueCacheUpdate(this, CacheDomain::ARIA); 1403 RefPtr<AccEvent> event = 1404 new AccObjectAttrChangedEvent(this, nsGkAtoms::hasActions); 1405 mDoc->FireDelayedEvent(event); 1406 } 1407 1408 dom::Element* elm = Elm(); 1409 1410 if (HasNumericValue() && 1411 (aAttribute == nsGkAtoms::aria_valuemax || 1412 aAttribute == nsGkAtoms::aria_valuemin || aAttribute == nsGkAtoms::min || 1413 aAttribute == nsGkAtoms::max || aAttribute == nsGkAtoms::step)) { 1414 mDoc->QueueCacheUpdate(this, CacheDomain::Value); 1415 return; 1416 } 1417 1418 // Fire text value change event whenever aria-valuetext is changed. 1419 if (aAttribute == nsGkAtoms::aria_valuetext) { 1420 mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE, this); 1421 return; 1422 } 1423 1424 if (aAttribute == nsGkAtoms::aria_valuenow) { 1425 if (!nsAccUtils::HasARIAAttr(elm, nsGkAtoms::aria_valuetext) || 1426 nsAccUtils::ARIAAttrValueIs(elm, nsGkAtoms::aria_valuetext, 1427 nsGkAtoms::_empty, eCaseMatters)) { 1428 // Fire numeric value change event when aria-valuenow is changed and 1429 // aria-valuetext is empty 1430 mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, this); 1431 } else { 1432 // We need to update the cache here since we won't get an event if 1433 // aria-valuenow is shadowed by aria-valuetext. 1434 mDoc->QueueCacheUpdate(this, CacheDomain::Value); 1435 } 1436 return; 1437 } 1438 1439 if (aAttribute == nsGkAtoms::aria_owns) { 1440 mDoc->Controller()->ScheduleRelocation(this); 1441 } 1442 1443 // Fire name change and description change events. 1444 if (aAttribute == nsGkAtoms::aria_label || aAttribute == nsGkAtoms::label) { 1445 // A valid aria-labelledby would take precedence over an aria-label or a xul 1446 // label attribute. So if that relation exists the name won't change. 1447 AssociatedElementsIterator iter(mDoc, elm, nsGkAtoms::aria_labelledby); 1448 if (!iter.NextElem()) { 1449 mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this); 1450 } 1451 return; 1452 } 1453 1454 if (aAttribute == nsGkAtoms::aria_description) { 1455 // A valid aria-describedby would take precedence so an aria-description 1456 // change won't change the description. 1457 AssociatedElementsIterator iter(mDoc, elm, nsGkAtoms::aria_describedby); 1458 if (!iter.NextElem()) { 1459 mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE, 1460 this); 1461 } 1462 return; 1463 } 1464 1465 if (aAttribute == nsGkAtoms::aria_describedby) { 1466 mDoc->QueueCacheUpdate(this, CacheDomain::Relations); 1467 mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE, this); 1468 if (IsAdditionOrModification(aModType)) { 1469 // The subtrees of the new aria-describedby targets might be used to 1470 // compute the description for this. Therefore, we need to set 1471 // the eHasDescriptionDependent flag on all Accessibles in these subtrees. 1472 AssociatedElementsIterator iter(mDoc, elm, nsGkAtoms::aria_describedby); 1473 while (LocalAccessible* target = iter.Next()) { 1474 target->ModifySubtreeContextFlags(eHasDescriptionDependent, true); 1475 } 1476 } 1477 return; 1478 } 1479 1480 if (aAttribute == nsGkAtoms::aria_labelledby) { 1481 // We only queue cache updates for explicit relations. Implicit, reverse 1482 // relations are handled in ApplyCache and stored in a map on the remote 1483 // document itself. 1484 mDoc->QueueCacheUpdate(this, CacheDomain::Relations); 1485 mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this); 1486 if (IsAdditionOrModification(aModType)) { 1487 // The subtrees of the new aria-labelledby targets might be used to 1488 // compute the name for this. Therefore, we need to set 1489 // the eHasNameDependent flag on all Accessibles in these subtrees. 1490 AssociatedElementsIterator iter(mDoc, elm, nsGkAtoms::aria_labelledby); 1491 while (LocalAccessible* target = iter.Next()) { 1492 target->ModifySubtreeContextFlags(eHasNameDependent, true); 1493 } 1494 } 1495 return; 1496 } 1497 1498 if ((aAttribute == nsGkAtoms::aria_expanded || 1499 aAttribute == nsGkAtoms::href) && 1500 IsAdditionOrRemoval(aModType)) { 1501 // The presence of aria-expanded adds an expand/collapse action. 1502 mDoc->QueueCacheUpdate(this, CacheDomain::Actions); 1503 } 1504 1505 if (aAttribute == nsGkAtoms::href || aAttribute == nsGkAtoms::src) { 1506 mDoc->QueueCacheUpdate(this, CacheDomain::Value); 1507 } 1508 1509 if (aAttribute == nsGkAtoms::aria_details) { 1510 mDoc->QueueCacheUpdate(this, CacheDomain::Relations); 1511 // If an aria-details attribute is added or removed from an anchored 1512 // accessible, it will change the validity of its anchor's relation. 1513 mDoc->RefreshAnchorRelationCacheForTarget(this); 1514 } 1515 1516 if (aAttribute == nsGkAtoms::aria_controls || 1517 aAttribute == nsGkAtoms::aria_flowto || 1518 aAttribute == nsGkAtoms::aria_errormessage || 1519 aAttribute == nsGkAtoms::aria_actions) { 1520 mDoc->QueueCacheUpdate(this, CacheDomain::Relations); 1521 } 1522 1523 if (aAttribute == nsGkAtoms::popovertarget) { 1524 mDoc->QueueCacheUpdate(this, CacheDomain::Relations); 1525 return; 1526 } 1527 1528 if (aAttribute == nsGkAtoms::commandfor) { 1529 mDoc->QueueCacheUpdate(this, CacheDomain::Relations); 1530 return; 1531 } 1532 1533 if (aAttribute == nsGkAtoms::alt && 1534 !nsAccUtils::HasARIAAttr(elm, nsGkAtoms::aria_label) && 1535 !elm->HasAttr(nsGkAtoms::aria_labelledby)) { 1536 mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this); 1537 return; 1538 } 1539 1540 if (aAttribute == nsGkAtoms::title) { 1541 nsAutoString name; 1542 if (Name(name) == eNameFromTooltip || name.IsVoid()) { 1543 mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this); 1544 return; 1545 } 1546 1547 if (!elm->HasAttr(nsGkAtoms::aria_describedby)) { 1548 mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE, 1549 this); 1550 } 1551 1552 return; 1553 } 1554 1555 // ARIA or XUL selection 1556 if ((mContent->IsXULElement() && aAttribute == nsGkAtoms::selected) || 1557 aAttribute == nsGkAtoms::aria_selected) { 1558 LocalAccessible* widget = nsAccUtils::GetSelectableContainer(this, State()); 1559 if (widget) { 1560 AccSelChangeEvent::SelChangeType selChangeType; 1561 if (aNameSpaceID != kNameSpaceID_None) { 1562 selChangeType = elm->AttrValueIs(aNameSpaceID, aAttribute, 1563 nsGkAtoms::_true, eCaseMatters) 1564 ? AccSelChangeEvent::eSelectionAdd 1565 : AccSelChangeEvent::eSelectionRemove; 1566 } else { 1567 selChangeType = nsAccUtils::ARIAAttrValueIs( 1568 elm, aAttribute, nsGkAtoms::_true, eCaseMatters) 1569 ? AccSelChangeEvent::eSelectionAdd 1570 : AccSelChangeEvent::eSelectionRemove; 1571 } 1572 1573 RefPtr<AccEvent> event = 1574 new AccSelChangeEvent(widget, this, selChangeType); 1575 mDoc->FireDelayedEvent(event); 1576 if (aAttribute == nsGkAtoms::aria_selected) { 1577 mDoc->QueueCacheUpdate(this, CacheDomain::State); 1578 } 1579 } 1580 1581 return; 1582 } 1583 1584 if (aAttribute == nsGkAtoms::aria_level || 1585 aAttribute == nsGkAtoms::aria_setsize || 1586 aAttribute == nsGkAtoms::aria_posinset) { 1587 mDoc->QueueCacheUpdate(this, CacheDomain::GroupInfo); 1588 return; 1589 } 1590 1591 if (aAttribute == nsGkAtoms::accesskey) { 1592 mDoc->QueueCacheUpdate(this, CacheDomain::Actions); 1593 } 1594 1595 if (aAttribute == nsGkAtoms::name && 1596 (mContent && mContent->IsHTMLElement(nsGkAtoms::a))) { 1597 // If an anchor's name changed, it's possible a LINKS_TO relation 1598 // also changed. Push a cache update for Relations. 1599 mDoc->QueueCacheUpdate(this, CacheDomain::Relations); 1600 } 1601 } 1602 1603 void LocalAccessible::ARIAGroupPosition(int32_t* aLevel, int32_t* aSetSize, 1604 int32_t* aPosInSet) const { 1605 if (!mContent) { 1606 return; 1607 } 1608 1609 if (aLevel) { 1610 nsCoreUtils::GetUIntAttr(mContent, nsGkAtoms::aria_level, aLevel); 1611 } 1612 if (aSetSize) { 1613 nsCoreUtils::GetUIntAttr(mContent, nsGkAtoms::aria_setsize, aSetSize); 1614 } 1615 if (aPosInSet) { 1616 nsCoreUtils::GetUIntAttr(mContent, nsGkAtoms::aria_posinset, aPosInSet); 1617 } 1618 } 1619 1620 uint64_t LocalAccessible::ExplicitState() const { 1621 if (IsDefunct()) return states::DEFUNCT; 1622 1623 uint64_t state = NativeState(); 1624 // Apply ARIA states to be sure accessible states will be overridden. 1625 ApplyARIAState(&state); 1626 1627 if (!(state & states::UNAVAILABLE)) { 1628 // If the object is a current item of container widget then mark it as 1629 // ACTIVE. This allows screen reader virtual buffer modes to know which 1630 // descendant is the current one that would get focus if the user navigates 1631 // to the container widget. 1632 LocalAccessible* widget = ContainerWidget(); 1633 if (widget && widget->CurrentItem() == this) state |= states::ACTIVE; 1634 } 1635 1636 return state; 1637 } 1638 1639 uint64_t LocalAccessible::State() { 1640 uint64_t state = ExplicitState(); 1641 1642 ApplyImplicitState(state); 1643 return state; 1644 } 1645 1646 void LocalAccessible::ApplyARIAState(uint64_t* aState) const { 1647 if (!mContent->IsElement()) return; 1648 1649 dom::Element* element = mContent->AsElement(); 1650 1651 // Test for universal states first 1652 *aState |= aria::UniversalStatesFor(element); 1653 1654 const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); 1655 if (!roleMapEntry && IsHTMLTableCell() && Role() == roles::GRID_CELL) { 1656 // This is a <td> inside a role="grid", so it gets an implicit role of 1657 // GRID_CELL in ARIATransformRole. However, because it's implicit, we 1658 // don't have a role map entry, and without that, we can't apply ARIA states 1659 // below. Therefore, we get the role map entry here. 1660 roleMapEntry = aria::GetRoleMap(nsGkAtoms::gridcell); 1661 MOZ_ASSERT(roleMapEntry, "Should have role map entry for gridcell"); 1662 } 1663 if (roleMapEntry) { 1664 // We only force the readonly bit off if we have a real mapping for the aria 1665 // role. This preserves the ability for screen readers to use readonly 1666 // (primarily on the document) as the hint for creating a virtual buffer. 1667 if (roleMapEntry->role != roles::NOTHING) *aState &= ~states::READONLY; 1668 1669 if (mContent->HasID()) { 1670 // If has a role & ID and aria-activedescendant on the container, assume 1671 // focusable. 1672 const LocalAccessible* ancestor = this; 1673 while ((ancestor = ancestor->LocalParent()) && !ancestor->IsDoc()) { 1674 dom::Element* el = ancestor->Elm(); 1675 if (el && el->HasAttr(nsGkAtoms::aria_activedescendant)) { 1676 *aState |= states::FOCUSABLE; 1677 break; 1678 } 1679 } 1680 } 1681 } 1682 1683 if (!(*aState & states::FOCUSABLE)) { 1684 // Sometimes, we use aria-activedescendant targeting something which isn't 1685 // actually a descendant. This is technically a spec violation, but it's a 1686 // useful hack which makes certain things much easier. For example, we use 1687 // this for "fake focus" for multi select browser tabs and Quantumbar 1688 // autocomplete suggestions. 1689 // In these cases, the aria-activedescendant code above won't make the 1690 // active item focusable. It doesn't make sense for something to have 1691 // focus when it isn't focusable, so fix that here. 1692 if (FocusMgr()->IsActiveItem(this)) { 1693 *aState |= states::FOCUSABLE; 1694 } 1695 } 1696 1697 // special case: A native button element whose role got transformed by ARIA to 1698 // a toggle button Also applies to togglable button menus, like in the Dev 1699 // Tools Web Console. 1700 if (IsButton() || IsMenuButton()) { 1701 aria::MapToState(aria::eARIAPressed, element, aState); 1702 } 1703 1704 if (!IsTextField() && IsEditableRoot()) { 1705 // HTML text fields will have their own multi/single line calcuation in 1706 // NativeState. 1707 aria::MapToState(aria::eARIAMultilineByDefault, element, aState); 1708 } 1709 1710 if (!roleMapEntry) return; 1711 1712 *aState |= roleMapEntry->state; 1713 1714 if (aria::MapToState(roleMapEntry->attributeMap1, element, aState) && 1715 aria::MapToState(roleMapEntry->attributeMap2, element, aState) && 1716 aria::MapToState(roleMapEntry->attributeMap3, element, aState)) { 1717 aria::MapToState(roleMapEntry->attributeMap4, element, aState); 1718 } 1719 1720 // ARIA gridcell inherits readonly state from the grid until it's overridden. 1721 if ((roleMapEntry->Is(nsGkAtoms::gridcell) || 1722 roleMapEntry->Is(nsGkAtoms::columnheader) || 1723 roleMapEntry->Is(nsGkAtoms::rowheader)) && 1724 // Don't recurse infinitely for an authoring error like 1725 // <table role="gridcell">. Without this check, we'd call TableFor(this) 1726 // below, which would return this. 1727 !IsTable() && 1728 !nsAccUtils::HasDefinedARIAToken(mContent, nsGkAtoms::aria_readonly)) { 1729 if (const LocalAccessible* grid = nsAccUtils::TableFor(this)) { 1730 uint64_t gridState = 0; 1731 grid->ApplyARIAState(&gridState); 1732 *aState |= gridState & states::READONLY; 1733 } 1734 } 1735 } 1736 1737 void LocalAccessible::Value(nsString& aValue) const { 1738 if (HasNumericValue()) { 1739 // aria-valuenow is a number, and aria-valuetext is the optional text 1740 // equivalent. For the string value, we will try the optional text 1741 // equivalent first. 1742 if (!mContent->IsElement()) { 1743 return; 1744 } 1745 1746 if (!nsAccUtils::GetARIAAttr(mContent->AsElement(), 1747 nsGkAtoms::aria_valuetext, aValue)) { 1748 if (!NativeHasNumericValue()) { 1749 double checkValue = CurValue(); 1750 if (!std::isnan(checkValue)) { 1751 aValue.AppendFloat(checkValue); 1752 } 1753 } 1754 } 1755 return; 1756 } 1757 1758 const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); 1759 1760 // Value of textbox is a textified subtree. 1761 if ((roleMapEntry && roleMapEntry->Is(nsGkAtoms::textbox)) || 1762 (IsGeneric() && IsEditableRoot())) { 1763 nsTextEquivUtils::GetTextEquivFromSubtree(this, aValue); 1764 return; 1765 } 1766 1767 // Value of combobox is a text of current or selected item. 1768 if (roleMapEntry && roleMapEntry->Is(nsGkAtoms::combobox)) { 1769 LocalAccessible* option = CurrentItem(); 1770 if (!option) { 1771 uint32_t childCount = ChildCount(); 1772 for (uint32_t idx = 0; idx < childCount; idx++) { 1773 LocalAccessible* child = mChildren.ElementAt(idx); 1774 if (child->IsListControl()) { 1775 Accessible* acc = child->GetSelectedItem(0); 1776 option = acc ? acc->AsLocal() : nullptr; 1777 break; 1778 } 1779 } 1780 } 1781 1782 // If there's a selected item, get the value from it. Otherwise, determine 1783 // the value from descendant elements. 1784 nsTextEquivUtils::GetTextEquivFromSubtree(option ? option : this, aValue); 1785 } 1786 } 1787 1788 double LocalAccessible::MaxValue() const { 1789 double checkValue = AttrNumericValue(nsGkAtoms::aria_valuemax); 1790 if (std::isnan(checkValue) && !NativeHasNumericValue()) { 1791 // aria-valuemax isn't present and this element doesn't natively provide a 1792 // maximum value. Use the ARIA default. 1793 const nsRoleMapEntry* roleMap = ARIARoleMap(); 1794 if (roleMap && roleMap->role == roles::SPINBUTTON) { 1795 return UnspecifiedNaN<double>(); 1796 } 1797 return 100; 1798 } 1799 return checkValue; 1800 } 1801 1802 double LocalAccessible::MinValue() const { 1803 double checkValue = AttrNumericValue(nsGkAtoms::aria_valuemin); 1804 if (std::isnan(checkValue) && !NativeHasNumericValue()) { 1805 // aria-valuemin isn't present and this element doesn't natively provide a 1806 // minimum value. Use the ARIA default. 1807 const nsRoleMapEntry* roleMap = ARIARoleMap(); 1808 if (roleMap && roleMap->role == roles::SPINBUTTON) { 1809 return UnspecifiedNaN<double>(); 1810 } 1811 return 0; 1812 } 1813 return checkValue; 1814 } 1815 1816 double LocalAccessible::Step() const { 1817 return UnspecifiedNaN<double>(); // no mimimum increment (step) in ARIA. 1818 } 1819 1820 double LocalAccessible::CurValue() const { 1821 double checkValue = AttrNumericValue(nsGkAtoms::aria_valuenow); 1822 if (std::isnan(checkValue) && !NativeHasNumericValue()) { 1823 // aria-valuenow isn't present and this element doesn't natively provide a 1824 // current value. Use the ARIA default. 1825 const nsRoleMapEntry* roleMap = ARIARoleMap(); 1826 if (roleMap && roleMap->role == roles::SPINBUTTON) { 1827 return UnspecifiedNaN<double>(); 1828 } 1829 double minValue = MinValue(); 1830 return minValue + ((MaxValue() - minValue) / 2); 1831 } 1832 1833 return checkValue; 1834 } 1835 1836 bool LocalAccessible::SetCurValue(double aValue) { return false; } 1837 1838 role LocalAccessible::NativeRole() const { return roles::NOTHING; } 1839 1840 uint8_t LocalAccessible::ActionCount() const { 1841 return HasPrimaryAction() || ActionAncestor() ? 1 : 0; 1842 } 1843 1844 void LocalAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) { 1845 aName.Truncate(); 1846 1847 if (aIndex != 0) return; 1848 1849 uint32_t actionRule = GetActionRule(); 1850 1851 switch (actionRule) { 1852 case eActivateAction: 1853 aName.AssignLiteral("activate"); 1854 return; 1855 1856 case eClickAction: 1857 aName.AssignLiteral("click"); 1858 return; 1859 1860 case ePressAction: 1861 aName.AssignLiteral("press"); 1862 return; 1863 1864 case eCheckUncheckAction: { 1865 uint64_t state = State(); 1866 if (state & states::CHECKED) { 1867 aName.AssignLiteral("uncheck"); 1868 } else if (state & states::MIXED) { 1869 aName.AssignLiteral("cycle"); 1870 } else { 1871 aName.AssignLiteral("check"); 1872 } 1873 return; 1874 } 1875 1876 case eJumpAction: 1877 aName.AssignLiteral("jump"); 1878 return; 1879 1880 case eOpenCloseAction: 1881 if (State() & states::EXPANDED) { 1882 aName.AssignLiteral("close"); 1883 } else { 1884 aName.AssignLiteral("open"); 1885 } 1886 return; 1887 1888 case eSelectAction: 1889 aName.AssignLiteral("select"); 1890 return; 1891 1892 case eSwitchAction: 1893 aName.AssignLiteral("switch"); 1894 return; 1895 1896 case eSortAction: 1897 aName.AssignLiteral("sort"); 1898 return; 1899 1900 case eExpandAction: 1901 if (State() & states::EXPANDED) { 1902 aName.AssignLiteral("collapse"); 1903 } else { 1904 aName.AssignLiteral("expand"); 1905 } 1906 return; 1907 } 1908 1909 if (ActionAncestor()) { 1910 aName.AssignLiteral("clickAncestor"); 1911 return; 1912 } 1913 } 1914 1915 bool LocalAccessible::DoAction(uint8_t aIndex) const { 1916 if (aIndex != 0) return false; 1917 1918 if (HasPrimaryAction() || ActionAncestor()) { 1919 DoCommand(); 1920 return true; 1921 } 1922 1923 return false; 1924 } 1925 1926 bool LocalAccessible::HasPrimaryAction() const { 1927 return GetActionRule() != eNoAction; 1928 } 1929 1930 nsIContent* LocalAccessible::GetAtomicRegion() const { 1931 nsIContent* loopContent = mContent; 1932 nsAutoString atomic; 1933 while (loopContent && 1934 (!loopContent->IsElement() || 1935 !nsAccUtils::GetARIAAttr(loopContent->AsElement(), 1936 nsGkAtoms::aria_atomic, atomic))) { 1937 loopContent = loopContent->GetParent(); 1938 } 1939 1940 return atomic.EqualsLiteral("true") ? loopContent : nullptr; 1941 } 1942 1943 LocalAccessible* LocalAccessible::GetCommandForDetailsRelation() const { 1944 dom::Element* targetEl = mContent->GetEffectiveCommandForElement(); 1945 if (!targetEl) { 1946 return nullptr; 1947 } 1948 LocalAccessible* targetAcc = mDoc->GetAccessible(targetEl); 1949 if (!targetAcc) { 1950 return nullptr; 1951 } 1952 // Relations on Command/CommandFor should only be for ShowPopover & 1953 // TogglePopover commands. 1954 if (const nsAttrValue* actionVal = Elm()->GetParsedAttr(nsGkAtoms::command)) { 1955 if (actionVal && actionVal->Type() != nsAttrValue::eEnum) { 1956 return nullptr; 1957 } 1958 auto command = 1959 static_cast<dom::Element::Command>(actionVal->GetEnumValue()); 1960 if (command != dom::Element::Command::ShowPopover && 1961 command != dom::Element::Command::TogglePopover) { 1962 return nullptr; 1963 } 1964 } 1965 if (targetAcc->NextSibling() == this || targetAcc->PrevSibling() == this) { 1966 return nullptr; 1967 } 1968 return targetAcc; 1969 } 1970 1971 LocalAccessible* LocalAccessible::GetPopoverTargetDetailsRelation() const { 1972 dom::Element* targetEl = mContent->GetEffectivePopoverTargetElement(); 1973 if (!targetEl) { 1974 return nullptr; 1975 } 1976 LocalAccessible* targetAcc = mDoc->GetAccessible(targetEl); 1977 if (!targetAcc) { 1978 return nullptr; 1979 } 1980 // Even if the popovertarget is valid, there are a few cases where we must not 1981 // expose it via the details relation. 1982 if (const nsAttrValue* actionVal = 1983 Elm()->GetParsedAttr(nsGkAtoms::popovertargetaction)) { 1984 if (static_cast<PopoverTargetAction>(actionVal->GetEnumValue()) == 1985 PopoverTargetAction::Hide) { 1986 return nullptr; 1987 } 1988 } 1989 if (targetAcc->NextSibling() == this || targetAcc->PrevSibling() == this) { 1990 return nullptr; 1991 } 1992 return targetAcc; 1993 } 1994 1995 LocalAccessible* LocalAccessible::GetAnchorPositionTargetDetailsRelation() 1996 const { 1997 if (!StaticPrefs::accessibility_anchorPositionedAsDetails_enabled()) { 1998 return nullptr; 1999 } 2000 nsIFrame* positionedFrame = nsCoreUtils::GetPositionedFrameForAnchor( 2001 mDoc->PresShellPtr(), GetFrame()); 2002 if (!positionedFrame) { 2003 return nullptr; 2004 } 2005 2006 if (!nsCoreUtils::GetAnchorForPositionedFrame(mDoc->PresShellPtr(), 2007 positionedFrame)) { 2008 // There is no reciprocal, 1:1, anchor for this positioned frame. 2009 return nullptr; 2010 } 2011 2012 LocalAccessible* targetAcc = 2013 mDoc->GetAccessible(positionedFrame->GetContent()); 2014 2015 if (!targetAcc) { 2016 return nullptr; 2017 } 2018 2019 if (targetAcc->Role() == roles::TOOLTIP) { 2020 // A tooltip is never a valid target for details relation. 2021 return nullptr; 2022 } 2023 2024 AssociatedElementsIterator describedby(mDoc, GetContent(), 2025 nsGkAtoms::aria_describedby); 2026 while (LocalAccessible* target = describedby.Next()) { 2027 if (target == targetAcc) { 2028 // An explicit description relation exists, so we don't want to create a 2029 // details relation. 2030 return nullptr; 2031 } 2032 } 2033 2034 AssociatedElementsIterator labelledby(mDoc, GetContent(), 2035 nsGkAtoms::aria_labelledby); 2036 while (LocalAccessible* target = labelledby.Next()) { 2037 if (target == targetAcc) { 2038 // An explicit label relation exists, so we don't want to create a details 2039 // relation. 2040 return nullptr; 2041 } 2042 } 2043 2044 dom::Element* anchorEl = targetAcc->Elm(); 2045 if (anchorEl && anchorEl->HasAttr(nsGkAtoms::aria_details)) { 2046 // If the anchor has an explicit aria-details attribute, then we don't want 2047 // to create a details relation. 2048 return nullptr; 2049 } 2050 2051 dom::Element* targetEl = Elm(); 2052 if (targetEl && targetEl->HasAttr(nsGkAtoms::aria_details)) { 2053 // If the target has an explicit aria-details attribute, then we don't want 2054 // to create a details relation. 2055 return nullptr; 2056 } 2057 2058 return targetAcc; 2059 } 2060 2061 Relation LocalAccessible::RelationByType(RelationType aType) const { 2062 if (!HasOwnContent()) return Relation(); 2063 2064 const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); 2065 2066 // Relationships are defined on the same content node that the role would be 2067 // defined on. 2068 switch (aType) { 2069 case RelationType::LABELLED_BY: { 2070 Relation rel(new AssociatedElementsIterator(mDoc, mContent, 2071 nsGkAtoms::aria_labelledby)); 2072 if (mContent->IsHTMLElement()) { 2073 rel.AppendIter(new HTMLLabelIterator(Document(), this)); 2074 } 2075 rel.AppendIter(new XULLabelIterator(Document(), mContent)); 2076 2077 return rel; 2078 } 2079 2080 case RelationType::LABEL_FOR: { 2081 Relation rel(new RelatedAccIterator(Document(), mContent, 2082 nsGkAtoms::aria_labelledby)); 2083 if (mContent->IsXULElement(nsGkAtoms::label)) { 2084 rel.AppendIter( 2085 new AssociatedElementsIterator(mDoc, mContent, nsGkAtoms::control)); 2086 } 2087 2088 return rel; 2089 } 2090 2091 case RelationType::DESCRIBED_BY: { 2092 Relation rel(new AssociatedElementsIterator(mDoc, mContent, 2093 nsGkAtoms::aria_describedby)); 2094 if (mContent->IsXULElement()) { 2095 rel.AppendIter(new XULDescriptionIterator(Document(), mContent)); 2096 } 2097 2098 return rel; 2099 } 2100 2101 case RelationType::DESCRIPTION_FOR: { 2102 Relation rel(new RelatedAccIterator(Document(), mContent, 2103 nsGkAtoms::aria_describedby)); 2104 2105 // This affectively adds an optional control attribute to xul:description, 2106 // which only affects accessibility, by allowing the description to be 2107 // tied to a control. 2108 if (mContent->IsXULElement(nsGkAtoms::description)) { 2109 rel.AppendIter( 2110 new AssociatedElementsIterator(mDoc, mContent, nsGkAtoms::control)); 2111 } 2112 2113 return rel; 2114 } 2115 2116 case RelationType::NODE_CHILD_OF: { 2117 Relation rel; 2118 // This is an ARIA tree or treegrid that doesn't use owns, so we need to 2119 // get the parent the hard way. 2120 if (roleMapEntry && (roleMapEntry->role == roles::OUTLINEITEM || 2121 roleMapEntry->role == roles::LISTITEM || 2122 roleMapEntry->role == roles::ROW)) { 2123 AccGroupInfo* groupInfo = 2124 const_cast<LocalAccessible*>(this)->GetOrCreateGroupInfo(); 2125 if (groupInfo) { 2126 Accessible* parent = groupInfo->ConceptualParent(); 2127 if (parent) { 2128 MOZ_ASSERT(parent->IsLocal()); 2129 rel.AppendTarget(parent->AsLocal()); 2130 } 2131 } 2132 } 2133 2134 // If this is an OOP iframe document, we can't support NODE_CHILD_OF 2135 // here, since the iframe resides in a different process. This is fine 2136 // because the client will then request the parent instead, which will be 2137 // correctly handled by platform code. 2138 if (XRE_IsContentProcess() && IsRoot()) { 2139 dom::Document* doc = 2140 const_cast<LocalAccessible*>(this)->AsDoc()->DocumentNode(); 2141 dom::BrowsingContext* bc = doc->GetBrowsingContext(); 2142 MOZ_ASSERT(bc); 2143 if (!bc->Top()->IsInProcess()) { 2144 return rel; 2145 } 2146 } 2147 2148 // If accessible is in its own Window, or is the root of a document, 2149 // then we should provide NODE_CHILD_OF relation so that MSAA clients 2150 // can easily get to true parent instead of getting to oleacc's 2151 // ROLE_WINDOW accessible which will prevent us from going up further 2152 // (because it is system generated and has no idea about the hierarchy 2153 // above it). 2154 if (nsIFrame* frame = GetFrame()) { 2155 ScrollContainerFrame* scrollContainerFrame = do_QueryFrame(frame); 2156 if (scrollContainerFrame || frame->GetOwnWidget() || 2157 !frame->GetParent()) { 2158 rel.AppendTarget(LocalParent()); 2159 } 2160 } 2161 2162 return rel; 2163 } 2164 2165 case RelationType::NODE_PARENT_OF: { 2166 // ARIA tree or treegrid can do the hierarchy by @aria-level, ARIA trees 2167 // also can be organized by groups. 2168 if (roleMapEntry && (roleMapEntry->role == roles::OUTLINEITEM || 2169 roleMapEntry->role == roles::LISTITEM || 2170 roleMapEntry->role == roles::ROW || 2171 roleMapEntry->role == roles::OUTLINE || 2172 roleMapEntry->role == roles::LIST || 2173 roleMapEntry->role == roles::TREE_TABLE)) { 2174 return Relation(new ItemIterator(this)); 2175 } 2176 2177 return Relation(); 2178 } 2179 2180 case RelationType::CONTROLLED_BY: { 2181 Relation rel(new RelatedAccIterator(Document(), mContent, 2182 nsGkAtoms::aria_controls)); 2183 2184 RelatedAccIterator owners(Document(), mContent, nsGkAtoms::aria_owns); 2185 if (LocalAccessible* owner = owners.Next()) { 2186 if (nsAccUtils::IsEditableARIACombobox(owner)) { 2187 MOZ_ASSERT(!IsRelocated(), 2188 "Child is not relocated to editable combobox"); 2189 rel.AppendTarget(owner); 2190 } 2191 } 2192 2193 return rel; 2194 } 2195 case RelationType::CONTROLLER_FOR: { 2196 Relation rel(new AssociatedElementsIterator(mDoc, mContent, 2197 nsGkAtoms::aria_controls)); 2198 rel.AppendIter(new HTMLOutputIterator(Document(), mContent)); 2199 if (nsAccUtils::IsEditableARIACombobox(this)) { 2200 AssociatedElementsIterator iter(mDoc, mContent, nsGkAtoms::aria_owns); 2201 while (Accessible* owned_child = iter.Next()) { 2202 MOZ_ASSERT(!owned_child->AsLocal()->IsRelocated()); 2203 rel.AppendTarget(owned_child->AsLocal()); 2204 } 2205 } 2206 return rel; 2207 } 2208 2209 case RelationType::FLOWS_TO: 2210 return Relation(new AssociatedElementsIterator(mDoc, mContent, 2211 nsGkAtoms::aria_flowto)); 2212 2213 case RelationType::FLOWS_FROM: 2214 return Relation( 2215 new RelatedAccIterator(Document(), mContent, nsGkAtoms::aria_flowto)); 2216 2217 case RelationType::MEMBER_OF: { 2218 if (Role() == roles::RADIOBUTTON) { 2219 /* If we see a radio button role here, we're dealing with an aria 2220 * radio button (because input=radio buttons are 2221 * HTMLRadioButtonAccessibles) */ 2222 Relation rel = Relation(); 2223 LocalAccessible* currParent = LocalParent(); 2224 while (currParent && currParent->Role() != roles::RADIO_GROUP) { 2225 currParent = currParent->LocalParent(); 2226 } 2227 2228 if (currParent && currParent->Role() == roles::RADIO_GROUP) { 2229 /* If we found a radiogroup parent, search for all 2230 * roles::RADIOBUTTON children and add them to our relation. 2231 * This search will include the radio button this method 2232 * was called from, which is expected. */ 2233 Pivot p = Pivot(currParent); 2234 PivotRoleRule rule(roles::RADIOBUTTON); 2235 Accessible* match = p.Next(currParent, rule); 2236 while (match) { 2237 MOZ_ASSERT(match->IsLocal(), 2238 "We shouldn't find any remote accs while building our " 2239 "relation!"); 2240 rel.AppendTarget(match->AsLocal()); 2241 match = p.Next(match, rule); 2242 } 2243 } 2244 2245 /* By webkit's standard, aria radio buttons do not get grouped 2246 * if they lack a group parent, so we return an empty 2247 * relation here if the above check fails. */ 2248 2249 return rel; 2250 } 2251 2252 return Relation(mDoc, GetAtomicRegion()); 2253 } 2254 2255 case RelationType::LINKS_TO: { 2256 Relation rel = Relation(); 2257 if (Role() == roles::LINK) { 2258 dom::HTMLAnchorElement* anchor = 2259 dom::HTMLAnchorElement::FromNode(mContent); 2260 if (!anchor) { 2261 return rel; 2262 } 2263 // If this node is an anchor element, query its hash to find the 2264 // target. 2265 nsAutoCString hash; 2266 anchor->GetHash(hash); 2267 if (hash.IsEmpty()) { 2268 return rel; 2269 } 2270 2271 // GetHash returns an ID or name with a leading '#', trim it so we can 2272 // search the doc by ID or name alone. 2273 NS_ConvertUTF8toUTF16 hash16(Substring(hash, 1)); 2274 if (dom::Element* elm = mContent->OwnerDoc()->GetElementById(hash16)) { 2275 rel.AppendTarget(mDoc->GetAccessibleOrContainer(elm)); 2276 } else if (nsCOMPtr<nsINodeList> list = 2277 mContent->OwnerDoc()->GetElementsByName(hash16)) { 2278 // Loop through the named nodes looking for the first anchor 2279 uint32_t length = list->Length(); 2280 for (uint32_t i = 0; i < length; i++) { 2281 nsIContent* node = list->Item(i); 2282 if (node->IsHTMLElement(nsGkAtoms::a)) { 2283 rel.AppendTarget(mDoc->GetAccessibleOrContainer(node)); 2284 break; 2285 } 2286 } 2287 } 2288 } 2289 2290 return rel; 2291 } 2292 2293 case RelationType::SUBWINDOW_OF: 2294 case RelationType::EMBEDS: 2295 case RelationType::EMBEDDED_BY: 2296 case RelationType::POPUP_FOR: 2297 case RelationType::PARENT_WINDOW_OF: 2298 return Relation(); 2299 2300 case RelationType::DEFAULT_BUTTON: { 2301 if (mContent->IsHTMLElement()) { 2302 // HTML form controls implements nsIFormControl interface. 2303 if (auto* control = nsIFormControl::FromNode(mContent)) { 2304 if (dom::HTMLFormElement* form = control->GetForm()) { 2305 return Relation(mDoc, form->GetDefaultSubmitElement()); 2306 } 2307 } 2308 } else { 2309 // In XUL, use first <button default="true" .../> in the document 2310 dom::Document* doc = mContent->OwnerDoc(); 2311 nsIContent* buttonEl = nullptr; 2312 if (doc->AllowXULXBL()) { 2313 nsCOMPtr<nsIHTMLCollection> possibleDefaultButtons = 2314 doc->GetElementsByAttribute(u"default"_ns, u"true"_ns); 2315 if (possibleDefaultButtons) { 2316 uint32_t length = possibleDefaultButtons->Length(); 2317 // Check for button in list of default="true" elements 2318 for (uint32_t count = 0; count < length && !buttonEl; count++) { 2319 nsIContent* item = possibleDefaultButtons->Item(count); 2320 RefPtr<nsIDOMXULButtonElement> button = 2321 item->IsElement() ? item->AsElement()->AsXULButton() 2322 : nullptr; 2323 if (button) { 2324 buttonEl = item; 2325 } 2326 } 2327 } 2328 return Relation(mDoc, buttonEl); 2329 } 2330 } 2331 return Relation(); 2332 } 2333 2334 case RelationType::CONTAINING_DOCUMENT: 2335 return Relation(mDoc); 2336 2337 case RelationType::CONTAINING_TAB_PANE: { 2338 nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(GetNode()); 2339 if (docShell) { 2340 // Walk up the parent chain without crossing the boundary at which item 2341 // types change, preventing us from walking up out of tab content. 2342 nsCOMPtr<nsIDocShellTreeItem> root; 2343 docShell->GetInProcessSameTypeRootTreeItem(getter_AddRefs(root)); 2344 if (root) { 2345 // If the item type is typeContent, we assume we are in browser tab 2346 // content. Note, this includes content such as about:addons, 2347 // for consistency. 2348 if (root->ItemType() == nsIDocShellTreeItem::typeContent) { 2349 return Relation(nsAccUtils::GetDocAccessibleFor(root)); 2350 } 2351 } 2352 } 2353 return Relation(); 2354 } 2355 2356 case RelationType::CONTAINING_APPLICATION: 2357 return Relation(ApplicationAcc()); 2358 2359 case RelationType::DETAILS: { 2360 if (mContent->IsElement() && 2361 nsAccUtils::HasARIAAttr(mContent->AsElement(), 2362 nsGkAtoms::aria_details)) { 2363 return Relation(new AssociatedElementsIterator( 2364 mDoc, mContent, nsGkAtoms::aria_details)); 2365 } 2366 if (LocalAccessible* target = GetCommandForDetailsRelation()) { 2367 return Relation(target); 2368 } 2369 if (LocalAccessible* target = GetPopoverTargetDetailsRelation()) { 2370 return Relation(target); 2371 } 2372 if (LocalAccessible* target = GetAnchorPositionTargetDetailsRelation()) { 2373 if (nsAccUtils::IsValidDetailsTargetForAnchor(target, this)) { 2374 return Relation(target); 2375 } 2376 } 2377 2378 return Relation(); 2379 } 2380 2381 case RelationType::DETAILS_FOR: { 2382 Relation rel( 2383 new RelatedAccIterator(mDoc, mContent, nsGkAtoms::aria_details)); 2384 RelatedAccIterator popover_invokers(mDoc, mContent, 2385 nsGkAtoms::popovertarget); 2386 while (Accessible* invoker = popover_invokers.Next()) { 2387 // We should only expose DETAILS_FOR if DETAILS was exposed on the 2388 // invoker. However, DETAILS exposure on popover invokers is 2389 // conditional. 2390 if (invoker->AsLocal()->GetPopoverTargetDetailsRelation()) { 2391 MOZ_ASSERT(invoker->AsLocal()->GetPopoverTargetDetailsRelation() == 2392 this); 2393 rel.AppendTarget(invoker); 2394 } 2395 } 2396 RelatedAccIterator command_invokers(mDoc, mContent, 2397 nsGkAtoms::commandfor); 2398 while (Accessible* invoker = command_invokers.Next()) { 2399 // We should only expose DETAILS_FOR if DETAILS was exposed on the 2400 // invoker. However, DETAILS exposure on popover invokers is 2401 // conditional. 2402 if (invoker->AsLocal()->GetCommandForDetailsRelation()) { 2403 MOZ_ASSERT(invoker->AsLocal()->GetCommandForDetailsRelation() == 2404 this); 2405 rel.AppendTarget(invoker); 2406 } 2407 } 2408 2409 // Check early if the accessible is a tooltip. If so, it can never be a 2410 // valid target for an anchor's details relation. 2411 if (StaticPrefs::accessibility_anchorPositionedAsDetails_enabled() && 2412 Role() != roles::TOOLTIP) { 2413 if (const nsIFrame* anchorFrame = 2414 nsCoreUtils::GetAnchorForPositionedFrame(mDoc->PresShellPtr(), 2415 GetFrame())) { 2416 LocalAccessible* anchorAcc = 2417 mDoc->GetAccessible(anchorFrame->GetContent()); 2418 if (anchorAcc && 2419 anchorAcc->GetAnchorPositionTargetDetailsRelation() == this && 2420 nsAccUtils::IsValidDetailsTargetForAnchor(this, anchorAcc)) { 2421 rel.AppendTarget(anchorAcc); 2422 } 2423 } 2424 } 2425 2426 return rel; 2427 } 2428 2429 case RelationType::ERRORMSG: 2430 return Relation(new AssociatedElementsIterator( 2431 mDoc, mContent, nsGkAtoms::aria_errormessage)); 2432 2433 case RelationType::ERRORMSG_FOR: 2434 return Relation( 2435 new RelatedAccIterator(mDoc, mContent, nsGkAtoms::aria_errormessage)); 2436 2437 case RelationType::ACTION: 2438 return Relation(new AssociatedElementsIterator(mDoc, mContent, 2439 nsGkAtoms::aria_actions)); 2440 2441 case RelationType::ACTION_FOR: 2442 return Relation( 2443 new RelatedAccIterator(mDoc, mContent, nsGkAtoms::aria_actions)); 2444 2445 default: 2446 return Relation(); 2447 } 2448 } 2449 2450 void LocalAccessible::GetNativeInterface(void** aNativeAccessible) {} 2451 2452 void LocalAccessible::DoCommand(uint32_t aActionIndex) const { 2453 NS_DispatchToMainThread(NS_NewRunnableFunction( 2454 "LocalAccessible::DispatchClickEvent", 2455 [aActionIndex, acc = RefPtr{this}]() MOZ_CAN_RUN_SCRIPT_BOUNDARY { 2456 acc->DispatchClickEvent(aActionIndex); 2457 })); 2458 } 2459 2460 void LocalAccessible::DispatchClickEvent(uint32_t aActionIndex) const { 2461 if (IsDefunct()) return; 2462 MOZ_ASSERT(mContent); 2463 2464 RefPtr<PresShell> presShell = mDoc->PresShellPtr(); 2465 2466 // Scroll into view. 2467 presShell->ScrollContentIntoView(mContent, ScrollAxis(), ScrollAxis(), 2468 ScrollFlags::ScrollOverflowHidden); 2469 2470 AutoWeakFrame frame = GetFrame(); 2471 if (!frame) { 2472 return; 2473 } 2474 2475 // We use RelativeBounds rather than querying the frame directly because of 2476 // special cases like image map areas which don't have their own frame. 2477 // RelativeBounds overrides handle these special cases. 2478 nsIFrame* boundingFrame = nullptr; 2479 nsRect rect = RelativeBounds(&boundingFrame); 2480 MOZ_ASSERT(boundingFrame); 2481 2482 // Compute x and y coordinates in dev pixels relative to the widget. 2483 nsPoint offsetToWidget; 2484 nsCOMPtr<nsIWidget> widget = boundingFrame->GetNearestWidget(offsetToWidget); 2485 if (!widget) { 2486 return; 2487 } 2488 2489 rect += offsetToWidget; 2490 RefPtr<nsPresContext> presContext = presShell->GetPresContext(); 2491 int32_t x = presContext->AppUnitsToDevPixels(rect.x + rect.width / 2); 2492 int32_t y = presContext->AppUnitsToDevPixels(rect.y + rect.height / 2); 2493 2494 // Simulate a touch interaction by dispatching touch events with mouse events. 2495 // Even though we calculated x and y using the bounding frame, we must use the 2496 // primary frame for the event. In most cases, these two frames will be 2497 // different, but they are the same for special cases such as image map areas 2498 // which don't have their own frame. 2499 nsCoreUtils::DispatchTouchEvent(eTouchStart, x, y, mContent, frame, presShell, 2500 widget); 2501 2502 // This isn't needed once bug 1924790 is fixed. 2503 mContent->OwnerDoc()->NotifyUserGestureActivation(); 2504 2505 nsCoreUtils::DispatchMouseEvent(eMouseDown, x, y, mContent, frame, presShell, 2506 widget); 2507 nsCoreUtils::DispatchTouchEvent(eTouchEnd, x, y, mContent, frame, presShell, 2508 widget); 2509 nsCoreUtils::DispatchMouseEvent(eMouseUp, x, y, mContent, frame, presShell, 2510 widget); 2511 } 2512 2513 void LocalAccessible::ScrollToPoint(uint32_t aCoordinateType, int32_t aX, 2514 int32_t aY) { 2515 nsIFrame* frame = GetFrame(); 2516 if (!frame) return; 2517 2518 LayoutDeviceIntPoint coords = 2519 nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordinateType, this); 2520 2521 nsIFrame* parentFrame = frame; 2522 while ((parentFrame = parentFrame->GetParent())) { 2523 nsCoreUtils::ScrollFrameToPoint(parentFrame, frame, coords); 2524 } 2525 } 2526 2527 bool LocalAccessible::IsScrollable() const { 2528 const auto [scrollPosition, scrollRange] = mDoc->ComputeScrollData(this); 2529 return scrollRange.width > 0 || scrollRange.height > 0; 2530 } 2531 2532 bool LocalAccessible::IsPopover() const { 2533 dom::Element* el = Elm(); 2534 return el && el->IsHTMLElement() && el->HasAttr(nsGkAtoms::popover); 2535 } 2536 2537 bool LocalAccessible::IsEditable() const { 2538 dom::Element* el = Elm(); 2539 return el && el->State().HasState(dom::ElementState::READWRITE); 2540 } 2541 2542 void LocalAccessible::AppendTextTo(nsAString& aText, uint32_t aStartOffset, 2543 uint32_t aLength) { 2544 // Return text representation of non-text accessible within hypertext 2545 // accessible. Text accessible overrides this method to return enclosed text. 2546 if (aStartOffset != 0 || aLength == 0) return; 2547 2548 MOZ_ASSERT(mParent, 2549 "Called on accessible unbound from tree. Result can be wrong."); 2550 nsIFrame* frame = GetFrame(); 2551 // We handle something becoming display: none async, which means we won't have 2552 // a frame when we're queuing text removed events. Thus, it's important that 2553 // we produce text here even if there's no frame. Otherwise, we won't fire a 2554 // text removed event at all, which might leave client caches (e.g. NVDA 2555 // virtual buffers) with dead nodes. 2556 if (IsHTMLBr() || (frame && frame->IsBrFrame())) { 2557 aText += kForcedNewLineChar; 2558 } else if (mParent && nsAccUtils::MustPrune(mParent)) { 2559 // Expose the embedded object accessible as imaginary embedded object 2560 // character if its parent hypertext accessible doesn't expose children to 2561 // AT. 2562 aText += kImaginaryEmbeddedObjectChar; 2563 } else { 2564 aText += kEmbeddedObjectChar; 2565 } 2566 } 2567 2568 void LocalAccessible::Shutdown() { 2569 // Mark the accessible as defunct, invalidate the child count and pointers to 2570 // other accessibles, also make sure none of its children point to this 2571 // parent 2572 mStateFlags |= eIsDefunct; 2573 2574 int32_t childCount = mChildren.Length(); 2575 for (int32_t childIdx = 0; childIdx < childCount; childIdx++) { 2576 mChildren.ElementAt(childIdx)->UnbindFromParent(); 2577 } 2578 mChildren.Clear(); 2579 2580 mEmbeddedObjCollector = nullptr; 2581 2582 if (mParent) mParent->RemoveChild(this); 2583 2584 mContent = nullptr; 2585 mDoc = nullptr; 2586 if (SelectionMgr() && SelectionMgr()->AccessibleWithCaret(nullptr) == this) { 2587 SelectionMgr()->ResetCaretOffset(); 2588 } 2589 } 2590 2591 // LocalAccessible protected 2592 ENameValueFlag LocalAccessible::ARIAName(nsString& aName) const { 2593 // 'slot' elements should ignore aria-label and aria-labelledby. 2594 if (mContent->IsHTMLElement(nsGkAtoms::slot)) { 2595 return eNameOK; 2596 } 2597 // aria-labelledby now takes precedence over aria-label 2598 bool notSimpleRelation = nsTextEquivUtils::GetTextEquivFromIDRefs( 2599 this, nsGkAtoms::aria_labelledby, aName); 2600 aName.CompressWhitespace(); 2601 2602 if (!aName.IsEmpty()) { 2603 return notSimpleRelation ? eNameOK : eNameFromRelations; 2604 } 2605 2606 if (mContent->IsElement() && 2607 nsAccUtils::GetARIAAttr(mContent->AsElement(), nsGkAtoms::aria_label, 2608 aName)) { 2609 aName.CompressWhitespace(); 2610 } 2611 2612 return eNameOK; 2613 } 2614 2615 // LocalAccessible protected 2616 bool LocalAccessible::ARIADescription(nsString& aDescription) const { 2617 // aria-describedby takes precedence over aria-description 2618 nsTextEquivUtils::GetTextEquivFromIDRefs(this, nsGkAtoms::aria_describedby, 2619 aDescription); 2620 aDescription.CompressWhitespace(); 2621 2622 if (aDescription.IsEmpty() && mContent->IsElement() && 2623 nsAccUtils::GetARIAAttr(mContent->AsElement(), 2624 nsGkAtoms::aria_description, aDescription)) { 2625 aDescription.CompressWhitespace(); 2626 } 2627 2628 return !aDescription.IsEmpty(); 2629 } 2630 2631 // LocalAccessible protected 2632 bool LocalAccessible::Tooltip(nsString& aTooltip) const { 2633 if (!HasOwnContent()) { 2634 return false; 2635 } 2636 2637 if (mContent->IsHTMLElement()) { 2638 mContent->AsElement()->GetAttr(nsGkAtoms::title, aTooltip); 2639 aTooltip.CompressWhitespace(); 2640 return !aTooltip.IsEmpty(); 2641 } else if (mContent->IsXULElement()) { 2642 mContent->AsElement()->GetAttr(nsGkAtoms::tooltiptext, aTooltip); 2643 aTooltip.CompressWhitespace(); 2644 return !aTooltip.IsEmpty(); 2645 } else if (mContent->IsSVGElement()) { 2646 // If user agents need to choose among multiple 'desc' or 'title' 2647 // elements for processing, the user agent shall choose the first one. 2648 for (nsIContent* childElm = mContent->GetFirstChild(); childElm; 2649 childElm = childElm->GetNextSibling()) { 2650 if (childElm->IsSVGElement(nsGkAtoms::desc)) { 2651 nsTextEquivUtils::AppendTextEquivFromContent(this, childElm, &aTooltip); 2652 aTooltip.CompressWhitespace(); 2653 return !aTooltip.IsEmpty(); 2654 } 2655 } 2656 } 2657 return false; 2658 } 2659 2660 // LocalAccessible protected 2661 ENameValueFlag LocalAccessible::NativeName(nsString& aName) const { 2662 if (mContent->IsHTMLElement()) { 2663 LocalAccessible* label = nullptr; 2664 HTMLLabelIterator iter(Document(), this); 2665 bool notSimpleRelation = false; 2666 while ((label = iter.Next())) { 2667 notSimpleRelation |= nsTextEquivUtils::AppendTextEquivFromContent( 2668 this, label->GetContent(), &aName); 2669 aName.CompressWhitespace(); 2670 } 2671 2672 if (!aName.IsEmpty()) { 2673 return notSimpleRelation ? eNameOK : eNameFromRelations; 2674 } 2675 2676 NameFromAssociatedXULLabel(mDoc, mContent, aName); 2677 if (!aName.IsEmpty()) { 2678 return eNameOK; 2679 } 2680 2681 // We return eNameFromSubtree here to indicate that if there is no native 2682 // name we will calculate the name from the subtree next. This is useful for 2683 // noting where the name will come from in cases like name change 2684 // notifications. 2685 return eNameFromSubtree; 2686 } 2687 2688 if (mContent->IsXULElement()) { 2689 XULElmName(mDoc, mContent, aName); 2690 if (!aName.IsEmpty()) return eNameOK; 2691 2692 // We return eNameFromSubtree here to indicate that if there is no native 2693 // name we will calculate the name from the subtree next. See above. 2694 return eNameFromSubtree; 2695 } 2696 2697 if (mContent->IsSVGElement()) { 2698 // If user agents need to choose among multiple 'desc' or 'title' 2699 // elements for processing, the user agent shall choose the first one. 2700 for (nsIContent* childElm = mContent->GetFirstChild(); childElm; 2701 childElm = childElm->GetNextSibling()) { 2702 if (childElm->IsSVGElement(nsGkAtoms::title)) { 2703 nsTextEquivUtils::AppendTextEquivFromContent(this, childElm, &aName); 2704 return eNameOK; 2705 } 2706 } 2707 } 2708 2709 return eNameOK; 2710 } 2711 2712 // LocalAccessible protected 2713 void LocalAccessible::NativeDescription(nsString& aDescription) const { 2714 bool isXUL = mContent->IsXULElement(); 2715 if (isXUL) { 2716 // Try XUL <description control="[id]">description text</description> 2717 XULDescriptionIterator iter(Document(), mContent); 2718 LocalAccessible* descr = nullptr; 2719 while ((descr = iter.Next())) { 2720 nsTextEquivUtils::AppendTextEquivFromContent(this, descr->GetContent(), 2721 &aDescription); 2722 } 2723 } 2724 } 2725 2726 // LocalAccessible protected 2727 void LocalAccessible::BindToParent(LocalAccessible* aParent, 2728 uint32_t aIndexInParent) { 2729 MOZ_ASSERT(aParent, "This method isn't used to set null parent"); 2730 MOZ_ASSERT(!mParent, "The child was expected to be moved"); 2731 2732 #ifdef A11Y_LOG 2733 if (mParent) { 2734 logging::TreeInfo("BindToParent: stealing accessible", 0, "old parent", 2735 mParent, "new parent", aParent, "child", this, nullptr); 2736 } 2737 #endif 2738 2739 mParent = aParent; 2740 mIndexInParent = aIndexInParent; 2741 2742 if (mParent->HasNameDependent() || mParent->IsXULListItem() || 2743 RelationByType(RelationType::LABEL_FOR).Next() || 2744 nsTextEquivUtils::HasNameRule(mParent, eNameFromSubtreeRule)) { 2745 mContextFlags |= eHasNameDependent; 2746 } else { 2747 mContextFlags &= ~eHasNameDependent; 2748 } 2749 if (mParent->HasDescriptionDependent() || 2750 RelationByType(RelationType::DESCRIPTION_FOR).Next()) { 2751 mContextFlags |= eHasDescriptionDependent; 2752 } else { 2753 mContextFlags &= ~eHasDescriptionDependent; 2754 } 2755 2756 // Add name/description dependent flags for dependent content once 2757 // a name/description provider is added to doc. 2758 Relation rel = RelationByType(RelationType::LABELLED_BY); 2759 LocalAccessible* relTarget = nullptr; 2760 while ((relTarget = rel.LocalNext())) { 2761 if (!relTarget->HasNameDependent()) { 2762 relTarget->ModifySubtreeContextFlags(eHasNameDependent, true); 2763 } 2764 } 2765 2766 rel = RelationByType(RelationType::DESCRIBED_BY); 2767 while ((relTarget = rel.LocalNext())) { 2768 if (!relTarget->HasDescriptionDependent()) { 2769 relTarget->ModifySubtreeContextFlags(eHasDescriptionDependent, true); 2770 } 2771 } 2772 2773 mContextFlags |= 2774 static_cast<uint32_t>((mParent->IsAlert() || mParent->IsInsideAlert())) & 2775 eInsideAlert; 2776 2777 if (IsTableCell()) { 2778 CachedTableAccessible::Invalidate(this); 2779 } 2780 } 2781 2782 // LocalAccessible protected 2783 void LocalAccessible::UnbindFromParent() { 2784 // We do this here to handle document shutdown and an Accessible being moved. 2785 // We do this for subtree removal in DocAccessible::UncacheChildrenInSubtree. 2786 if (IsTable() || IsTableCell()) { 2787 CachedTableAccessible::Invalidate(this); 2788 } 2789 2790 mParent = nullptr; 2791 mIndexInParent = -1; 2792 mIndexOfEmbeddedChild = -1; 2793 2794 delete mGroupInfo; 2795 mGroupInfo = nullptr; 2796 mContextFlags &= ~eHasNameDependent & ~eInsideAlert; 2797 } 2798 2799 //////////////////////////////////////////////////////////////////////////////// 2800 // LocalAccessible public methods 2801 2802 RootAccessible* LocalAccessible::RootAccessible() const { 2803 nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(GetNode()); 2804 NS_ASSERTION(docShell, "No docshell for mContent"); 2805 if (!docShell) { 2806 return nullptr; 2807 } 2808 2809 nsCOMPtr<nsIDocShellTreeItem> root; 2810 docShell->GetInProcessRootTreeItem(getter_AddRefs(root)); 2811 NS_ASSERTION(root, "No root content tree item"); 2812 if (!root) { 2813 return nullptr; 2814 } 2815 2816 DocAccessible* docAcc = nsAccUtils::GetDocAccessibleFor(root); 2817 return docAcc ? docAcc->AsRoot() : nullptr; 2818 } 2819 2820 nsIFrame* LocalAccessible::GetFrame() const { 2821 return mContent ? mContent->GetPrimaryFrame() : nullptr; 2822 } 2823 2824 nsINode* LocalAccessible::GetNode() const { return mContent; } 2825 2826 dom::Element* LocalAccessible::Elm() const { 2827 return dom::Element::FromNodeOrNull(mContent); 2828 } 2829 2830 void LocalAccessible::Language(nsAString& aLanguage) { 2831 aLanguage.Truncate(); 2832 2833 if (!mDoc) return; 2834 2835 nsCoreUtils::GetLanguageFor(mContent, nullptr, aLanguage); 2836 if (aLanguage.IsEmpty()) { // Nothing found, so use document's language 2837 mDoc->DocumentNode()->GetHeaderData(nsGkAtoms::headerContentLanguage, 2838 aLanguage); 2839 } 2840 } 2841 2842 bool LocalAccessible::InsertChildAt(uint32_t aIndex, LocalAccessible* aChild) { 2843 if (!aChild) return false; 2844 2845 if (aIndex == mChildren.Length()) { 2846 // XXX(Bug 1631371) Check if this should use a fallible operation as it 2847 // pretended earlier. 2848 mChildren.AppendElement(aChild); 2849 } else { 2850 // XXX(Bug 1631371) Check if this should use a fallible operation as it 2851 // pretended earlier. 2852 mChildren.InsertElementAt(aIndex, aChild); 2853 2854 MOZ_ASSERT(mStateFlags & eKidsMutating, "Illicit children change"); 2855 2856 for (uint32_t idx = aIndex + 1; idx < mChildren.Length(); idx++) { 2857 mChildren[idx]->mIndexInParent = idx; 2858 } 2859 } 2860 2861 if (aChild->IsText()) { 2862 mStateFlags |= eHasTextKids; 2863 } 2864 2865 aChild->BindToParent(this, aIndex); 2866 return true; 2867 } 2868 2869 bool LocalAccessible::RemoveChild(LocalAccessible* aChild) { 2870 MOZ_DIAGNOSTIC_ASSERT(aChild, "No child was given"); 2871 MOZ_DIAGNOSTIC_ASSERT(aChild->mParent, "No parent"); 2872 MOZ_DIAGNOSTIC_ASSERT(aChild->mParent == this, "Wrong parent"); 2873 MOZ_DIAGNOSTIC_ASSERT(aChild->mIndexInParent != -1, 2874 "Unbound child was given"); 2875 MOZ_DIAGNOSTIC_ASSERT((mStateFlags & eKidsMutating) || aChild->IsDefunct() || 2876 aChild->IsDoc() || IsApplication(), 2877 "Illicit children change"); 2878 2879 int32_t index = static_cast<uint32_t>(aChild->mIndexInParent); 2880 if (mChildren.SafeElementAt(index) != aChild) { 2881 MOZ_ASSERT_UNREACHABLE("A wrong child index"); 2882 index = mChildren.IndexOf(aChild); 2883 if (index == -1) { 2884 MOZ_ASSERT_UNREACHABLE("No child was found"); 2885 return false; 2886 } 2887 } 2888 2889 aChild->UnbindFromParent(); 2890 mChildren.RemoveElementAt(index); 2891 2892 for (uint32_t idx = index; idx < mChildren.Length(); idx++) { 2893 mChildren[idx]->mIndexInParent = idx; 2894 } 2895 2896 return true; 2897 } 2898 2899 void LocalAccessible::RelocateChild(uint32_t aNewIndex, 2900 LocalAccessible* aChild) { 2901 MOZ_DIAGNOSTIC_ASSERT(aChild, "No child was given"); 2902 MOZ_DIAGNOSTIC_ASSERT(aChild->mParent == this, 2903 "A child from different subtree was given"); 2904 MOZ_DIAGNOSTIC_ASSERT(aChild->mIndexInParent != -1, 2905 "Unbound child was given"); 2906 MOZ_DIAGNOSTIC_ASSERT( 2907 aChild->mParent->LocalChildAt(aChild->mIndexInParent) == aChild, 2908 "Wrong index in parent"); 2909 MOZ_DIAGNOSTIC_ASSERT( 2910 static_cast<uint32_t>(aChild->mIndexInParent) != aNewIndex, 2911 "No move, same index"); 2912 MOZ_DIAGNOSTIC_ASSERT(aNewIndex <= mChildren.Length(), 2913 "Wrong new index was given"); 2914 2915 RefPtr<AccHideEvent> hideEvent = new AccHideEvent(aChild, false); 2916 if (mDoc->Controller()->QueueMutationEvent(hideEvent)) { 2917 aChild->SetHideEventTarget(true); 2918 } 2919 2920 mEmbeddedObjCollector = nullptr; 2921 mChildren.RemoveElementAt(aChild->mIndexInParent); 2922 2923 uint32_t startIdx = aNewIndex, endIdx = aChild->mIndexInParent; 2924 2925 // If the child is moved after its current position. 2926 if (static_cast<uint32_t>(aChild->mIndexInParent) < aNewIndex) { 2927 startIdx = aChild->mIndexInParent; 2928 if (aNewIndex == mChildren.Length() + 1) { 2929 // The child is moved to the end. 2930 mChildren.AppendElement(aChild); 2931 endIdx = mChildren.Length() - 1; 2932 } else { 2933 mChildren.InsertElementAt(aNewIndex - 1, aChild); 2934 endIdx = aNewIndex; 2935 } 2936 } else { 2937 // The child is moved prior its current position. 2938 mChildren.InsertElementAt(aNewIndex, aChild); 2939 } 2940 2941 for (uint32_t idx = startIdx; idx <= endIdx; idx++) { 2942 mChildren[idx]->mIndexInParent = idx; 2943 mChildren[idx]->mIndexOfEmbeddedChild = -1; 2944 } 2945 2946 for (uint32_t idx = 0; idx < mChildren.Length(); idx++) { 2947 mChildren[idx]->mStateFlags |= eGroupInfoDirty; 2948 } 2949 2950 RefPtr<AccShowEvent> showEvent = new AccShowEvent(aChild); 2951 DebugOnly<bool> added = mDoc->Controller()->QueueMutationEvent(showEvent); 2952 MOZ_ASSERT(added); 2953 aChild->SetShowEventTarget(true); 2954 } 2955 2956 LocalAccessible* LocalAccessible::LocalChildAt(uint32_t aIndex) const { 2957 LocalAccessible* child = mChildren.SafeElementAt(aIndex, nullptr); 2958 if (!child) return nullptr; 2959 2960 #ifdef DEBUG 2961 LocalAccessible* realParent = child->mParent; 2962 NS_ASSERTION(!realParent || realParent == this, 2963 "Two accessibles have the same first child accessible!"); 2964 #endif 2965 2966 return child; 2967 } 2968 2969 uint32_t LocalAccessible::ChildCount() const { return mChildren.Length(); } 2970 2971 int32_t LocalAccessible::IndexInParent() const { return mIndexInParent; } 2972 2973 uint32_t LocalAccessible::EmbeddedChildCount() { 2974 if (mStateFlags & eHasTextKids) { 2975 if (!mEmbeddedObjCollector) { 2976 mEmbeddedObjCollector.reset(new EmbeddedObjCollector(this)); 2977 } 2978 return mEmbeddedObjCollector->Count(); 2979 } 2980 2981 return ChildCount(); 2982 } 2983 2984 Accessible* LocalAccessible::EmbeddedChildAt(uint32_t aIndex) { 2985 if (mStateFlags & eHasTextKids) { 2986 if (!mEmbeddedObjCollector) { 2987 mEmbeddedObjCollector.reset(new EmbeddedObjCollector(this)); 2988 } 2989 return mEmbeddedObjCollector.get() 2990 ? mEmbeddedObjCollector->GetAccessibleAt(aIndex) 2991 : nullptr; 2992 } 2993 2994 return ChildAt(aIndex); 2995 } 2996 2997 int32_t LocalAccessible::IndexOfEmbeddedChild(Accessible* aChild) { 2998 MOZ_ASSERT(aChild->IsLocal()); 2999 if (mStateFlags & eHasTextKids) { 3000 if (!mEmbeddedObjCollector) { 3001 mEmbeddedObjCollector.reset(new EmbeddedObjCollector(this)); 3002 } 3003 return mEmbeddedObjCollector.get() 3004 ? mEmbeddedObjCollector->GetIndexAt(aChild->AsLocal()) 3005 : -1; 3006 } 3007 3008 return GetIndexOf(aChild->AsLocal()); 3009 } 3010 3011 //////////////////////////////////////////////////////////////////////////////// 3012 // HyperLinkAccessible methods 3013 3014 bool LocalAccessible::IsLink() const { 3015 // Every embedded accessible within hypertext accessible implements 3016 // hyperlink interface. 3017 return mParent && mParent->IsHyperText() && !IsText(); 3018 } 3019 3020 //////////////////////////////////////////////////////////////////////////////// 3021 // SelectAccessible 3022 3023 void LocalAccessible::SelectedItems(nsTArray<Accessible*>* aItems) { 3024 AccIterator iter(this, filters::GetSelected); 3025 LocalAccessible* selected = nullptr; 3026 while ((selected = iter.Next())) aItems->AppendElement(selected); 3027 } 3028 3029 uint32_t LocalAccessible::SelectedItemCount() { 3030 uint32_t count = 0; 3031 AccIterator iter(this, filters::GetSelected); 3032 LocalAccessible* selected = nullptr; 3033 while ((selected = iter.Next())) ++count; 3034 3035 return count; 3036 } 3037 3038 Accessible* LocalAccessible::GetSelectedItem(uint32_t aIndex) { 3039 AccIterator iter(this, filters::GetSelected); 3040 LocalAccessible* selected = nullptr; 3041 3042 uint32_t index = 0; 3043 while ((selected = iter.Next()) && index < aIndex) index++; 3044 3045 return selected; 3046 } 3047 3048 bool LocalAccessible::IsItemSelected(uint32_t aIndex) { 3049 uint32_t index = 0; 3050 AccIterator iter(this, filters::GetSelectable); 3051 LocalAccessible* selected = nullptr; 3052 while ((selected = iter.Next()) && index < aIndex) index++; 3053 3054 return selected && selected->State() & states::SELECTED; 3055 } 3056 3057 bool LocalAccessible::AddItemToSelection(uint32_t aIndex) { 3058 uint32_t index = 0; 3059 AccIterator iter(this, filters::GetSelectable); 3060 LocalAccessible* selected = nullptr; 3061 while ((selected = iter.Next()) && index < aIndex) index++; 3062 3063 if (selected) selected->SetSelected(true); 3064 3065 return static_cast<bool>(selected); 3066 } 3067 3068 bool LocalAccessible::RemoveItemFromSelection(uint32_t aIndex) { 3069 uint32_t index = 0; 3070 AccIterator iter(this, filters::GetSelectable); 3071 LocalAccessible* selected = nullptr; 3072 while ((selected = iter.Next()) && index < aIndex) index++; 3073 3074 if (selected) selected->SetSelected(false); 3075 3076 return static_cast<bool>(selected); 3077 } 3078 3079 bool LocalAccessible::SelectAll() { 3080 bool success = false; 3081 LocalAccessible* selectable = nullptr; 3082 3083 AccIterator iter(this, filters::GetSelectable); 3084 while ((selectable = iter.Next())) { 3085 success = true; 3086 selectable->SetSelected(true); 3087 } 3088 return success; 3089 } 3090 3091 bool LocalAccessible::UnselectAll() { 3092 bool success = false; 3093 LocalAccessible* selected = nullptr; 3094 3095 AccIterator iter(this, filters::GetSelected); 3096 while ((selected = iter.Next())) { 3097 success = true; 3098 selected->SetSelected(false); 3099 } 3100 return success; 3101 } 3102 3103 //////////////////////////////////////////////////////////////////////////////// 3104 // Widgets 3105 3106 bool LocalAccessible::IsWidget() const { return false; } 3107 3108 bool LocalAccessible::IsActiveWidget() const { 3109 if (FocusMgr()->HasDOMFocus(mContent)) return true; 3110 3111 // If text entry of combobox widget has a focus then the combobox widget is 3112 // active. 3113 const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); 3114 if (roleMapEntry && roleMapEntry->Is(nsGkAtoms::combobox)) { 3115 uint32_t childCount = ChildCount(); 3116 for (uint32_t idx = 0; idx < childCount; idx++) { 3117 LocalAccessible* child = mChildren.ElementAt(idx); 3118 if (child->Role() == roles::ENTRY) { 3119 return FocusMgr()->HasDOMFocus(child->GetContent()); 3120 } 3121 } 3122 } 3123 3124 return false; 3125 } 3126 3127 bool LocalAccessible::AreItemsOperable() const { 3128 return HasOwnContent() && mContent->IsElement() && 3129 mContent->AsElement()->HasAttr(nsGkAtoms::aria_activedescendant); 3130 } 3131 3132 LocalAccessible* LocalAccessible::CurrentItem() const { 3133 // Check for aria-activedescendant, which changes which element has focus. 3134 // For activedescendant, the ARIA spec does not require that the user agent 3135 // checks whether pointed node is actually a DOM descendant of the element 3136 // with the aria-activedescendant attribute. 3137 if (HasOwnContent() && mContent->IsElement()) { 3138 if (dom::Element* activeDescendantElm = 3139 nsCoreUtils::GetAriaActiveDescendantElement( 3140 mContent->AsElement())) { 3141 if (mContent->IsInclusiveDescendantOf(activeDescendantElm)) { 3142 // Don't want a cyclical descendant relationship. That would be bad. 3143 return nullptr; 3144 } 3145 3146 DocAccessible* document = Document(); 3147 if (document) return document->GetAccessible(activeDescendantElm); 3148 } 3149 } 3150 return nullptr; 3151 } 3152 3153 void LocalAccessible::SetCurrentItem(const LocalAccessible* aItem) {} 3154 3155 LocalAccessible* LocalAccessible::ContainerWidget() const { 3156 if (HasARIARole() && mContent->HasID()) { 3157 for (LocalAccessible* parent = LocalParent(); parent; 3158 parent = parent->LocalParent()) { 3159 nsIContent* parentContent = parent->GetContent(); 3160 if (parentContent && parentContent->IsElement() && 3161 nsCoreUtils::GetAriaActiveDescendantElement( 3162 parentContent->AsElement())) { 3163 return parent; 3164 } 3165 3166 // Don't cross DOM document boundaries. 3167 if (parent->IsDoc()) break; 3168 } 3169 } 3170 return nullptr; 3171 } 3172 3173 bool LocalAccessible::IsActiveDescendant(LocalAccessible** aWidget) const { 3174 RelatedAccIterator widgets(mDoc, mContent, nsGkAtoms::aria_activedescendant); 3175 if (LocalAccessible* widget = widgets.Next()) { 3176 if (aWidget) { 3177 *aWidget = widget; 3178 } 3179 return true; 3180 } 3181 3182 return false; 3183 } 3184 3185 void LocalAccessible::Announce(const nsAString& aAnnouncement, 3186 uint16_t aPriority) { 3187 RefPtr<AccAnnouncementEvent> event = 3188 new AccAnnouncementEvent(this, aAnnouncement, aPriority); 3189 nsEventShell::FireEvent(event); 3190 } 3191 3192 //////////////////////////////////////////////////////////////////////////////// 3193 // LocalAccessible protected methods 3194 3195 void LocalAccessible::LastRelease() { 3196 // First cleanup if needed... 3197 if (mDoc) { 3198 Shutdown(); 3199 NS_ASSERTION(!mDoc, 3200 "A Shutdown() impl forgot to call its parent's Shutdown?"); 3201 } 3202 // ... then die. 3203 delete this; 3204 } 3205 3206 LocalAccessible* LocalAccessible::GetSiblingAtOffset(int32_t aOffset, 3207 nsresult* aError) const { 3208 if (!mParent || mIndexInParent == -1) { 3209 if (aError) *aError = NS_ERROR_UNEXPECTED; 3210 3211 return nullptr; 3212 } 3213 3214 if (aError && 3215 mIndexInParent + aOffset >= static_cast<int32_t>(mParent->ChildCount())) { 3216 *aError = NS_OK; // fail peacefully 3217 return nullptr; 3218 } 3219 3220 LocalAccessible* child = mParent->LocalChildAt(mIndexInParent + aOffset); 3221 if (aError && !child) *aError = NS_ERROR_UNEXPECTED; 3222 3223 return child; 3224 } 3225 3226 void LocalAccessible::ModifySubtreeContextFlags(uint32_t aContextFlags, 3227 bool aAdd) { 3228 Pivot pivot(this); 3229 LocalAccInSameDocRule rule; 3230 for (Accessible* anchor = this; anchor; anchor = pivot.Next(anchor, rule)) { 3231 MOZ_ASSERT(anchor->IsLocal()); 3232 LocalAccessible* acc = anchor->AsLocal(); 3233 if (aAdd) { 3234 acc->mContextFlags |= aContextFlags; 3235 } else { 3236 acc->mContextFlags &= ~aContextFlags; 3237 } 3238 } 3239 } 3240 3241 double LocalAccessible::AttrNumericValue(nsAtom* aAttr) const { 3242 const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); 3243 if (!roleMapEntry || roleMapEntry->valueRule == eNoValue) { 3244 return UnspecifiedNaN<double>(); 3245 } 3246 3247 nsAutoString attrValue; 3248 if (!mContent->IsElement() || 3249 !nsAccUtils::GetARIAAttr(mContent->AsElement(), aAttr, attrValue)) { 3250 return UnspecifiedNaN<double>(); 3251 } 3252 3253 nsresult error = NS_OK; 3254 double value = attrValue.ToDouble(&error); 3255 return NS_FAILED(error) ? UnspecifiedNaN<double>() : value; 3256 } 3257 3258 uint32_t LocalAccessible::GetActionRule() const { 3259 if (!HasOwnContent() || (InteractiveState() & states::UNAVAILABLE)) { 3260 return eNoAction; 3261 } 3262 3263 // Return "click" action on elements that have an attached popup menu. 3264 if (mContent->IsXULElement()) { 3265 if (mContent->AsElement()->HasAttr(nsGkAtoms::popup)) { 3266 return eClickAction; 3267 } 3268 } 3269 3270 // Has registered 'click' event handler. 3271 bool isOnclick = nsCoreUtils::HasClickListener(mContent); 3272 3273 if (isOnclick) return eClickAction; 3274 3275 // Get an action based on ARIA role. 3276 const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); 3277 if (roleMapEntry && roleMapEntry->actionRule != eNoAction) { 3278 return roleMapEntry->actionRule; 3279 } 3280 3281 // Get an action based on ARIA attribute. 3282 if (nsAccUtils::HasDefinedARIAToken(mContent, nsGkAtoms::aria_expanded)) { 3283 return eExpandAction; 3284 } 3285 3286 return eNoAction; 3287 } 3288 3289 AccGroupInfo* LocalAccessible::GetGroupInfo() const { 3290 if (mGroupInfo && !(mStateFlags & eGroupInfoDirty)) { 3291 return mGroupInfo; 3292 } 3293 3294 return nullptr; 3295 } 3296 3297 AccGroupInfo* LocalAccessible::GetOrCreateGroupInfo() { 3298 if (mGroupInfo) { 3299 if (mStateFlags & eGroupInfoDirty) { 3300 mGroupInfo->Update(); 3301 mStateFlags &= ~eGroupInfoDirty; 3302 } 3303 3304 return mGroupInfo; 3305 } 3306 3307 mGroupInfo = AccGroupInfo::CreateGroupInfo(this); 3308 mStateFlags &= ~eGroupInfoDirty; 3309 return mGroupInfo; 3310 } 3311 3312 void LocalAccessible::SendCache(uint64_t aCacheDomain, 3313 CacheUpdateType aUpdateType, 3314 bool aAppendEventData) { 3315 PerfStats::AutoMetricRecording<PerfStats::Metric::A11Y_SendCache> 3316 autoRecording; 3317 // DO NOT ADD CODE ABOVE THIS BLOCK: THIS CODE IS MEASURING TIMINGS. 3318 3319 if (!IPCAccessibilityActive() || !Document()) { 3320 return; 3321 } 3322 3323 // Only send cache updates for domains that are active. 3324 const uint64_t domainsToSend = 3325 nsAccessibilityService::GetActiveCacheDomains() & aCacheDomain; 3326 3327 // Avoid sending cache updates if we have no domains to update. 3328 if (domainsToSend == CacheDomain::None) { 3329 return; 3330 } 3331 3332 DocAccessibleChild* ipcDoc = mDoc->IPCDoc(); 3333 if (!ipcDoc) { 3334 // This means DocAccessible::DoInitialUpdate hasn't been called yet, which 3335 // means the a11y tree hasn't been built yet. Therefore, this should only 3336 // be possible if this is a DocAccessible. 3337 MOZ_ASSERT(IsDoc(), "Called on a non-DocAccessible but IPCDoc is null"); 3338 return; 3339 } 3340 3341 RefPtr<AccAttributes> fields = 3342 BundleFieldsForCache(domainsToSend, aUpdateType); 3343 if (!fields->Count()) { 3344 return; 3345 } 3346 nsTArray<CacheData> data; 3347 data.AppendElement(CacheData(ID(), fields)); 3348 if (aAppendEventData) { 3349 ipcDoc->PushMutationEventData( 3350 CacheEventData{std::move(aUpdateType), std::move(data)}); 3351 } else { 3352 ipcDoc->SendCache(aUpdateType, data); 3353 } 3354 3355 if (profiler_thread_is_being_profiled_for_markers()) { 3356 nsAutoCString updateTypeStr; 3357 if (aUpdateType == CacheUpdateType::Initial) { 3358 updateTypeStr = "Initial"; 3359 } else if (aUpdateType == CacheUpdateType::Update) { 3360 updateTypeStr = "Update"; 3361 } else { 3362 updateTypeStr = "Other"; 3363 } 3364 PROFILER_MARKER_TEXT("LocalAccessible::SendCache", A11Y, {}, updateTypeStr); 3365 } 3366 } 3367 3368 already_AddRefed<AccAttributes> LocalAccessible::BundleFieldsForCache( 3369 uint64_t aCacheDomain, CacheUpdateType aUpdateType, 3370 uint64_t aInitialDomains) { 3371 MOZ_ASSERT((~aCacheDomain & aInitialDomains) == CacheDomain::None, 3372 "Initial domain pushes without domains requested!"); 3373 RefPtr<AccAttributes> fields = new AccAttributes(); 3374 3375 if (aUpdateType == CacheUpdateType::Initial) { 3376 aInitialDomains = CacheDomain::All; 3377 } 3378 // Pass a single cache domain in to query whether this is the initial push for 3379 // this domain. 3380 auto IsInitialPush = [aInitialDomains](uint64_t aCacheDomain) { 3381 return (aCacheDomain & aInitialDomains) == aCacheDomain; 3382 }; 3383 auto IsUpdatePush = [aInitialDomains](uint64_t aCacheDomain) { 3384 return (aCacheDomain & aInitialDomains) == CacheDomain::None; 3385 }; 3386 3387 // Caching name for text leaf Accessibles is redundant, since their name is 3388 // always their text. Text gets handled below. 3389 if (aCacheDomain & CacheDomain::NameAndDescription && !IsText()) { 3390 nsString name; 3391 ENameValueFlag nameFlag = DirectName(name); 3392 3393 if (IsTextField()) { 3394 MOZ_ASSERT(mContent); 3395 nsString placeholder; 3396 // Only cache the placeholder separately if it isn't used as the name. 3397 if (Elm()->GetAttr(nsGkAtoms::placeholder, placeholder) && 3398 name != placeholder) { 3399 fields->SetAttribute(CacheKey::HTMLPlaceholder, std::move(placeholder)); 3400 } else if (IsUpdatePush(CacheDomain::NameAndDescription)) { 3401 fields->SetAttribute(CacheKey::HTMLPlaceholder, DeleteEntry()); 3402 } 3403 } 3404 3405 if (nameFlag != eNameFromRelations && !name.IsEmpty()) { 3406 fields->SetAttribute(CacheKey::Name, std::move(name)); 3407 } else if (IsUpdatePush(CacheDomain::NameAndDescription)) { 3408 fields->SetAttribute(CacheKey::Name, DeleteEntry()); 3409 } 3410 3411 nsString tooltip; 3412 if (Tooltip(tooltip)) { 3413 fields->SetAttribute(CacheKey::Tooltip, std::move(tooltip)); 3414 } else if (IsUpdatePush(CacheDomain::NameAndDescription)) { 3415 fields->SetAttribute(CacheKey::Tooltip, DeleteEntry()); 3416 } 3417 3418 nsString cssAltContent; 3419 if (auto cssAlt = CssAltContent(mContent)) { 3420 cssAlt.AppendToString(cssAltContent); 3421 fields->SetAttribute(CacheKey::CssAltContent, std::move(cssAltContent)); 3422 } else if (IsUpdatePush(CacheDomain::NameAndDescription)) { 3423 fields->SetAttribute(CacheKey::CssAltContent, DeleteEntry()); 3424 } 3425 3426 nsString description; 3427 int32_t descFlag = Description(description); 3428 if (!description.IsEmpty()) { 3429 fields->SetAttribute(CacheKey::Description, std::move(description)); 3430 } else if (IsUpdatePush(CacheDomain::NameAndDescription)) { 3431 fields->SetAttribute(CacheKey::Description, DeleteEntry()); 3432 } 3433 3434 if (descFlag != eDescriptionOK) { 3435 fields->SetAttribute(CacheKey::DescriptionValueFlag, descFlag); 3436 } else if (IsUpdatePush(CacheDomain::NameAndDescription)) { 3437 fields->SetAttribute(CacheKey::DescriptionValueFlag, DeleteEntry()); 3438 } 3439 } 3440 3441 if (aCacheDomain & CacheDomain::Value) { 3442 // We cache the text value in 3 cases: 3443 // 1. Accessible is an HTML input type that holds a number. 3444 // 2. Accessible has a numeric value and an aria-valuetext. 3445 // 3. Accessible is an HTML input type that holds text. 3446 // 4. Accessible is a link, in which case value is the target URL. 3447 // ... for all other cases we divine the value remotely. 3448 bool cacheValueText = false; 3449 if (HasNumericValue()) { 3450 fields->SetAttribute(CacheKey::NumericValue, CurValue()); 3451 fields->SetAttribute(CacheKey::MaxValue, MaxValue()); 3452 fields->SetAttribute(CacheKey::MinValue, MinValue()); 3453 fields->SetAttribute(CacheKey::Step, Step()); 3454 cacheValueText = NativeHasNumericValue() || 3455 (mContent->IsElement() && 3456 nsAccUtils::HasARIAAttr(mContent->AsElement(), 3457 nsGkAtoms::aria_valuetext)); 3458 } else { 3459 cacheValueText = IsTextField() || IsHTMLLink(); 3460 } 3461 3462 if (cacheValueText) { 3463 nsString value; 3464 Value(value); 3465 if (!value.IsEmpty()) { 3466 fields->SetAttribute(CacheKey::TextValue, std::move(value)); 3467 } else if (IsUpdatePush(CacheDomain::Value)) { 3468 fields->SetAttribute(CacheKey::TextValue, DeleteEntry()); 3469 } 3470 } 3471 3472 if (IsImage()) { 3473 // Cache the src of images. This is used by some clients to help remediate 3474 // inaccessible images. 3475 MOZ_ASSERT(mContent, "Image must have mContent"); 3476 nsString src; 3477 mContent->AsElement()->GetAttr(nsGkAtoms::src, src); 3478 if (!src.IsEmpty()) { 3479 fields->SetAttribute(CacheKey::SrcURL, std::move(src)); 3480 } else if (IsUpdatePush(CacheDomain::Value)) { 3481 fields->SetAttribute(CacheKey::SrcURL, DeleteEntry()); 3482 } 3483 } 3484 3485 if (TagName() == nsGkAtoms::meter) { 3486 // We should only cache value region for HTML meter elements. A meter 3487 // should always have a value region, so this attribute should never 3488 // be empty (i.e. there is no DeleteEntry() clause here). 3489 HTMLMeterAccessible* meter = static_cast<HTMLMeterAccessible*>(this); 3490 fields->SetAttribute(CacheKey::ValueRegion, meter->ValueRegion()); 3491 } 3492 } 3493 3494 if (aCacheDomain & CacheDomain::Viewport && IsDoc()) { 3495 // Construct the viewport cache for this document. This cache domain will 3496 // only be requested after we finish painting. 3497 DocAccessible* doc = AsDoc(); 3498 PresShell* presShell = doc->PresShellPtr(); 3499 3500 if (nsIFrame* rootFrame = presShell->GetRootFrame()) { 3501 nsTArray<nsIFrame*> frames; 3502 ScrollContainerFrame* sf = presShell->GetRootScrollContainerFrame(); 3503 nsRect scrollPort = sf ? sf->GetScrollPortRect() : rootFrame->GetRect(); 3504 3505 nsLayoutUtils::GetFramesForArea( 3506 RelativeTo{rootFrame}, scrollPort, frames, 3507 {{// We don't add the ::OnlyVisible option here, because 3508 // it means we always consider frames with pointer-events: none. 3509 // See usage of HitTestIsForVisibility in nsDisplayList::HitTest. 3510 // This flag ensures the display lists are built, even if 3511 // the page hasn't finished loading. 3512 nsLayoutUtils::FrameForPointOption::IgnorePaintSuppression, 3513 // Each doc should have its own viewport cache, so we can 3514 // ignore cross-doc content as an optimization. 3515 nsLayoutUtils::FrameForPointOption::IgnoreCrossDoc}}); 3516 3517 nsTHashSet<LocalAccessible*> inViewAccs; 3518 nsTArray<uint64_t> viewportCache(frames.Length()); 3519 // Layout considers table rows fully occluded by their containing cells. 3520 // This means they don't have their own display list items, and they won't 3521 // show up in the list returned from GetFramesForArea. To prevent table 3522 // rows from appearing offscreen, we manually add any rows for which we 3523 // have on-screen cells. 3524 LocalAccessible* prevParentRow = nullptr; 3525 for (nsIFrame* frame : frames) { 3526 if (frame->IsInlineFrame() && !frame->IsPrimaryFrame()) { 3527 // This is a line other than the first line in an inline element. Even 3528 // though there are multiple frames for this element (one per line), 3529 // there is only a single Accessible with bounds encompassing all the 3530 // frames. We don't have any additional information about the 3531 // individual continuation frames in our cache. Thus, we don't want 3532 // this Accessible to appear before leaves on other lines which are 3533 // later in the `frames` array. Otherwise, when hit testing, this 3534 // Accessible will match instead of those leaves. We will add this 3535 // Accessible when we get to its primary frame later. 3536 continue; 3537 } 3538 nsIContent* content = frame->GetContent(); 3539 if (!content) { 3540 continue; 3541 } 3542 3543 LocalAccessible* acc = doc->GetAccessible(content); 3544 // The document should always be present at the end of the list, so 3545 // including it is unnecessary and wasteful. We skip the document here 3546 // and handle it as a fallback when hit testing. 3547 if (!acc || acc == mDoc) { 3548 continue; 3549 } 3550 3551 if (acc->IsTextLeaf() && nsAccUtils::MustPrune(acc->LocalParent())) { 3552 acc = acc->LocalParent(); 3553 } 3554 if (acc->IsTableCell()) { 3555 LocalAccessible* parent = acc->LocalParent(); 3556 if (parent && parent->IsTableRow() && parent != prevParentRow) { 3557 // If we've entered a new row since the last cell we saw, add the 3558 // previous parent row to our viewport cache here to maintain 3559 // hittesting order. Keep track of the current parent row. 3560 if (prevParentRow && inViewAccs.EnsureInserted(prevParentRow)) { 3561 viewportCache.AppendElement(prevParentRow->ID()); 3562 } 3563 prevParentRow = parent; 3564 } 3565 } else if (acc->IsTable()) { 3566 // If we've encountered a table, we know we've already 3567 // handled all of this table's content (because we're traversing 3568 // in hittesting order). Add our table's final row to the viewport 3569 // cache before adding the table itself. Reset our marker for the next 3570 // table. 3571 if (prevParentRow && inViewAccs.EnsureInserted(prevParentRow)) { 3572 viewportCache.AppendElement(prevParentRow->ID()); 3573 } 3574 prevParentRow = nullptr; 3575 } else if (acc->IsImageMap()) { 3576 // Layout doesn't walk image maps, so we do that 3577 // manually here. We do this before adding the map itself 3578 // so the children come earlier in the hittesting order. 3579 for (uint32_t i = 0; i < acc->ChildCount(); i++) { 3580 LocalAccessible* child = acc->LocalChildAt(i); 3581 MOZ_ASSERT(child); 3582 if (inViewAccs.EnsureInserted(child)) { 3583 MOZ_ASSERT(!child->IsDoc()); 3584 viewportCache.AppendElement(child->ID()); 3585 } 3586 } 3587 } else if (acc->IsHTMLCombobox()) { 3588 // Layout doesn't consider combobox lists (or their 3589 // currently selected items) to be onscreen, but we do. 3590 // Add those things manually here. 3591 HTMLComboboxAccessible* combobox = 3592 static_cast<HTMLComboboxAccessible*>(acc); 3593 HTMLComboboxListAccessible* list = combobox->List(); 3594 LocalAccessible* currItem = combobox->SelectedOption(); 3595 // Preserve hittesting order by adding the item, then 3596 // the list, and finally the combobox itself. 3597 if (currItem && inViewAccs.EnsureInserted(currItem)) { 3598 viewportCache.AppendElement(currItem->ID()); 3599 } 3600 if (list && inViewAccs.EnsureInserted(list)) { 3601 viewportCache.AppendElement(list->ID()); 3602 } 3603 } 3604 3605 if (inViewAccs.EnsureInserted(acc)) { 3606 MOZ_ASSERT(!acc->IsDoc()); 3607 viewportCache.AppendElement(acc->ID()); 3608 } 3609 } 3610 3611 // Always send the viewport cache, even if we have no accessibles 3612 // in it. We don't want to send a delete entry because the viewport 3613 // cache _does_ exist, it is simply representing an empty screen. 3614 fields->SetAttribute(CacheKey::Viewport, std::move(viewportCache)); 3615 } 3616 } 3617 3618 if (aCacheDomain & CacheDomain::APZ && IsDoc()) { 3619 PresShell* presShell = AsDoc()->PresShellPtr(); 3620 MOZ_ASSERT(presShell, "Can't get APZ factor for null presShell"); 3621 nsPoint viewportOffset = 3622 presShell->GetVisualViewportOffsetRelativeToLayoutViewport(); 3623 if (viewportOffset.x || viewportOffset.y) { 3624 nsTArray<int32_t> offsetArray(2); 3625 offsetArray.AppendElement(viewportOffset.x); 3626 offsetArray.AppendElement(viewportOffset.y); 3627 fields->SetAttribute(CacheKey::VisualViewportOffset, 3628 std::move(offsetArray)); 3629 } else if (IsUpdatePush(CacheDomain::APZ)) { 3630 fields->SetAttribute(CacheKey::VisualViewportOffset, DeleteEntry()); 3631 } 3632 } 3633 3634 bool boundsChanged = false; 3635 nsIFrame* frame = GetFrame(); 3636 if (aCacheDomain & CacheDomain::Bounds) { 3637 nsRect newBoundsRect = ParentRelativeBounds(); 3638 3639 // 1. Layout might notify us of a possible bounds change when the bounds 3640 // haven't really changed. Therefore, we cache the last bounds we sent 3641 // and don't send an update if they haven't changed. 3642 // 2. For an initial cache push, we ignore 1) and always send the bounds. 3643 // This handles the case where this LocalAccessible was moved (but not 3644 // re-created). In that case, we will have cached bounds, but we currently 3645 // do an initial cache push. 3646 MOZ_ASSERT(IsInitialPush(CacheDomain::Bounds) || mBounds.isSome(), 3647 "Incremental cache push but mBounds is not set!"); 3648 3649 if (OuterDocAccessible* doc = AsOuterDoc()) { 3650 if (nsIFrame* docFrame = doc->GetFrame()) { 3651 const nsMargin& newOffset = docFrame->GetUsedBorderAndPadding(); 3652 Maybe<nsMargin> currOffset = doc->GetCrossDocOffset(); 3653 if (!currOffset || *currOffset != newOffset) { 3654 // OOP iframe docs can't compute their position within their 3655 // cross-proc parent, so we have to manually cache that offset 3656 // on the parent (outer doc) itself. For simplicity and consistency, 3657 // we do this here for both OOP and in-process iframes. For in-process 3658 // iframes, this also avoids the need to push a cache update for the 3659 // embedded document when the iframe changes its padding, gets 3660 // re-created, etc. Similar to bounds, we maintain a local cache and a 3661 // remote cache to avoid sending redundant updates. 3662 doc->SetCrossDocOffset(newOffset); 3663 nsTArray<int32_t> offsetArray(2); 3664 offsetArray.AppendElement(newOffset.Side(eSideLeft)); // X offset 3665 offsetArray.AppendElement(newOffset.Side(eSideTop)); // Y offset 3666 fields->SetAttribute(CacheKey::CrossDocOffset, 3667 std::move(offsetArray)); 3668 } 3669 } 3670 } 3671 3672 // mBounds should never be Nothing, but sometimes it is (see Bug 1922691). 3673 // We null check mBounds here to avoid a crash while we figure out how this 3674 // can happen. 3675 boundsChanged = IsInitialPush(CacheDomain::Bounds) || !mBounds || 3676 !newBoundsRect.IsEqualEdges(mBounds.value()); 3677 if (boundsChanged) { 3678 mBounds = Some(newBoundsRect); 3679 3680 UniquePtr<nsRect> ptr = MakeUnique<nsRect>(newBoundsRect); 3681 fields->SetAttribute(CacheKey::ParentRelativeBounds, std::move(ptr)); 3682 } 3683 3684 if (frame && frame->ScrollableOverflowRect().IsEmpty()) { 3685 fields->SetAttribute(CacheKey::IsClipped, true); 3686 } else if (IsUpdatePush(CacheDomain::Bounds)) { 3687 fields->SetAttribute(CacheKey::IsClipped, DeleteEntry()); 3688 } 3689 } 3690 3691 if (aCacheDomain & CacheDomain::Text) { 3692 if (!HasChildren()) { 3693 // We only cache text and line offsets on leaf Accessibles. 3694 // Only text Accessibles can have actual text. 3695 if (IsText()) { 3696 nsString text; 3697 AppendTextTo(text); 3698 fields->SetAttribute(CacheKey::Text, std::move(text)); 3699 TextLeafPoint point(this, 0); 3700 RefPtr<AccAttributes> attrs = point.GetTextAttributesLocalAcc( 3701 /* aIncludeDefaults */ false); 3702 if (attrs->Count()) { 3703 fields->SetAttribute(CacheKey::TextAttributes, std::move(attrs)); 3704 } else if (IsUpdatePush(CacheDomain::Text)) { 3705 fields->SetAttribute(CacheKey::TextAttributes, DeleteEntry()); 3706 } 3707 } 3708 } 3709 if (HyperTextAccessible* ht = AsHyperText()) { 3710 RefPtr<AccAttributes> attrs = ht->DefaultTextAttributes(); 3711 LocalAccessible* parent = LocalParent(); 3712 if (HyperTextAccessible* htParent = 3713 parent ? parent->AsHyperText() : nullptr) { 3714 if (RefPtr<AccAttributes> parentAttrs = 3715 htParent->DefaultTextAttributes()) { 3716 // Discard any entries that our parent already has. 3717 attrs->RemoveIdentical(parentAttrs); 3718 } 3719 } 3720 if (attrs->Count()) { 3721 fields->SetAttribute(CacheKey::TextAttributes, std::move(attrs)); 3722 } else if (IsUpdatePush(CacheDomain::Text)) { 3723 fields->SetAttribute(CacheKey::TextAttributes, DeleteEntry()); 3724 } 3725 } else if (!IsText()) { 3726 // Language is normally cached in text attributes, but Accessibles that 3727 // aren't HyperText or Text (e.g. <img>, <input type="radio">) don't have 3728 // text attributes. The Text domain isn't a great fit, but the kinds of 3729 // clients (e.g. screen readers) that care about language are likely to 3730 // care about text as well. 3731 nsString language; 3732 Language(language); 3733 if (!language.IsEmpty()) { 3734 fields->SetAttribute(CacheKey::Language, std::move(language)); 3735 } else if (IsUpdatePush(CacheDomain::Text)) { 3736 fields->SetAttribute(CacheKey::Language, DeleteEntry()); 3737 } 3738 } 3739 } 3740 3741 // If text changes, we must also update text offset attributes. 3742 if (aCacheDomain & (CacheDomain::TextOffsetAttributes | CacheDomain::Text) && 3743 IsTextLeaf()) { 3744 auto offsetAttrs = TextLeafPoint::GetTextOffsetAttributes(this); 3745 if (!offsetAttrs.IsEmpty()) { 3746 fields->SetAttribute(CacheKey::TextOffsetAttributes, 3747 std::move(offsetAttrs)); 3748 } else if (IsUpdatePush(CacheDomain::TextOffsetAttributes) || 3749 IsUpdatePush(CacheDomain::Text)) { 3750 fields->SetAttribute(CacheKey::TextOffsetAttributes, DeleteEntry()); 3751 } 3752 } 3753 3754 if (aCacheDomain & (CacheDomain::TextBounds) && !HasChildren()) { 3755 // We cache line start offsets for both text and non-text leaf Accessibles 3756 // because non-text leaf Accessibles can still start a line. 3757 TextLeafPoint lineStart = 3758 TextLeafPoint(this, 0).FindNextLineStartSameLocalAcc( 3759 /* aIncludeOrigin */ true); 3760 int32_t lineStartOffset = lineStart ? lineStart.mOffset : -1; 3761 // We push line starts and text bounds in two cases: 3762 // 1. TextBounds is pushed initially. 3763 // 2. CacheDomain::Bounds was requested (indicating that the frame was 3764 // reflowed) but the bounds didn't actually change. This can happen when 3765 // the spanned text is non-rectangular. For example, an Accessible might 3766 // cover two characters on one line and a single character on another line. 3767 // An insertion in a previous text node might cause it to shift such that it 3768 // now covers a single character on the first line and two characters on the 3769 // second line. Its bounding rect will be the same both before and after the 3770 // insertion. In this case, we use the first line start to determine whether 3771 // there was a change. This should be safe because the text didn't change in 3772 // this Accessible, so if the first line start doesn't shift, none of them 3773 // should shift. 3774 if (IsInitialPush(CacheDomain::TextBounds) || 3775 aCacheDomain & CacheDomain::Text || boundsChanged || 3776 mFirstLineStart != lineStartOffset) { 3777 mFirstLineStart = lineStartOffset; 3778 nsTArray<int32_t> lineStarts; 3779 for (; lineStart; 3780 lineStart = lineStart.FindNextLineStartSameLocalAcc(false)) { 3781 lineStarts.AppendElement(lineStart.mOffset); 3782 } 3783 if (!lineStarts.IsEmpty()) { 3784 fields->SetAttribute(CacheKey::TextLineStarts, std::move(lineStarts)); 3785 } else if (IsUpdatePush(CacheDomain::TextBounds)) { 3786 fields->SetAttribute(CacheKey::TextLineStarts, DeleteEntry()); 3787 } 3788 3789 if (frame && frame->IsTextFrame()) { 3790 if (nsTextFrame* currTextFrame = do_QueryFrame(frame)) { 3791 nsTArray<int32_t> charData(nsAccUtils::TextLength(this) * 3792 kNumbersInRect); 3793 // Continuation offsets are calculated relative to the primary frame. 3794 // However, the acc's bounds are calculated using 3795 // GetAllInFlowRectsUnion. For wrapped text which starts part way 3796 // through a line, this might mean the top left of the acc is 3797 // different to the top left of the primary frame. This also happens 3798 // when the primary frame is empty (e.g. a blank line at the start of 3799 // pre-formatted text), since the union rect will exclude the origin 3800 // in that case. Calculate the offset from the acc's rect to the 3801 // primary frame's rect. 3802 nsRect accOffset = 3803 nsLayoutUtils::GetAllInFlowRectsUnion(frame, frame); 3804 while (currTextFrame) { 3805 nsPoint contOffset = currTextFrame->GetOffsetTo(frame); 3806 contOffset -= accOffset.TopLeft(); 3807 int32_t length = currTextFrame->GetContentLength(); 3808 nsTArray<nsRect> charBounds(length); 3809 currTextFrame->GetCharacterRectsInRange( 3810 currTextFrame->GetContentOffset(), length, charBounds); 3811 for (nsRect& charRect : charBounds) { 3812 if (charRect.width == 0 && 3813 !currTextFrame->StyleText()->WhiteSpaceIsSignificant()) { 3814 // GetCharacterRectsInRange gives us one rect per content 3815 // offset. However, TextLeafAccessibles use rendered offsets; 3816 // e.g. they might exclude some content white space. If we get 3817 // a 0 width rect and it's white space, skip this rect, since 3818 // this character isn't in the rendered text. We do have 3819 // a way to convert between content and rendered offsets, but 3820 // doing this for every character is expensive. 3821 const char16_t contentChar = 3822 mContent->GetCharacterDataBuffer()->CharAt( 3823 charData.Length() / kNumbersInRect); 3824 if (contentChar == u' ' || contentChar == u'\t' || 3825 contentChar == u'\n') { 3826 continue; 3827 } 3828 } 3829 // We expect each char rect to be relative to the text leaf 3830 // acc this text lives in. Unfortunately, GetCharacterRectsInRange 3831 // returns rects relative to their continuation. Add the 3832 // continuation's relative position here to make our final 3833 // rect relative to the text leaf acc. 3834 charRect.MoveBy(contOffset); 3835 charData.AppendElement(charRect.x); 3836 charData.AppendElement(charRect.y); 3837 charData.AppendElement(charRect.width); 3838 charData.AppendElement(charRect.height); 3839 } 3840 currTextFrame = currTextFrame->GetNextContinuation(); 3841 } 3842 if (charData.Length()) { 3843 fields->SetAttribute(CacheKey::TextBounds, std::move(charData)); 3844 } 3845 } 3846 } 3847 } 3848 } 3849 3850 if (aCacheDomain & CacheDomain::TransformMatrix) { 3851 bool transformed = false; 3852 if (frame && frame->IsTransformed()) { 3853 // This matrix is only valid when applied to CSSPixel points/rects 3854 // in the coordinate space of `frame`. 3855 gfx::Matrix4x4 mtx = nsDisplayTransform::GetResultingTransformMatrix( 3856 frame, nsPoint(0, 0), AppUnitsPerCSSPixel(), 3857 nsDisplayTransform::INCLUDE_PERSPECTIVE); 3858 // We might get back the identity matrix. This can happen if there is no 3859 // actual transform. For example, if an element has 3860 // will-change: transform, nsIFrame::IsTransformed will return true, but 3861 // this doesn't necessarily mean there is a transform right now. 3862 // Applying the identity matrix is effectively a no-op, so there's no 3863 // point caching it. 3864 transformed = !mtx.IsIdentity(); 3865 if (transformed) { 3866 UniquePtr<gfx::Matrix4x4> ptr = MakeUnique<gfx::Matrix4x4>(mtx); 3867 fields->SetAttribute(CacheKey::TransformMatrix, std::move(ptr)); 3868 } 3869 } 3870 if (!transformed && IsUpdatePush(CacheDomain::TransformMatrix)) { 3871 // Otherwise, if we're bundling a transform update but this 3872 // frame isn't transformed (or doesn't exist), we need 3873 // to send a DeleteEntry() to remove any 3874 // transform that was previously cached for this frame. 3875 fields->SetAttribute(CacheKey::TransformMatrix, DeleteEntry()); 3876 } 3877 } 3878 3879 if (aCacheDomain & CacheDomain::ScrollPosition && frame) { 3880 // We request these values unscaled when caching because the scaled values 3881 // have resolution multiplied in. We can encounter a race condition when 3882 // using APZ where the resolution is not propogated back to content in time 3883 // for it to be multipled into the scroll position calculation. Even if we 3884 // end up with the correct resolution cached in parent, our final bounds 3885 // will be incorrect. Instead of scaling here, we scale in parent with our 3886 // cached resolution so any incorrectness will be consistent and dependent 3887 // on a single cache update (Resolution) instead of two (Resolution and 3888 // ScrollPosition). 3889 const auto [scrollPosition, scrollRange] = 3890 mDoc->ComputeScrollData(this, /* aShouldScaleByResolution */ false); 3891 if (scrollRange.width || scrollRange.height) { 3892 // If the scroll range is 0 by 0, this acc is not scrollable. We 3893 // can't simply check scrollPosition != 0, since it's valid for scrollable 3894 // frames to have a (0, 0) position. We also can't check IsEmpty or 3895 // ZeroArea because frames with only one scrollable dimension will return 3896 // a height/width of zero for the non-scrollable dimension, yielding zero 3897 // area even if the width/height for the scrollable dimension is nonzero. 3898 // We also cache (0, 0) for accs with overflow:auto or overflow:scroll, 3899 // even if the content is not currently large enough to be scrollable 3900 // right now -- these accs have a non-zero scroll range. 3901 nsTArray<int32_t> positionArr(2); 3902 positionArr.AppendElement(scrollPosition.x); 3903 positionArr.AppendElement(scrollPosition.y); 3904 fields->SetAttribute(CacheKey::ScrollPosition, std::move(positionArr)); 3905 } else if (IsUpdatePush(CacheDomain::ScrollPosition)) { 3906 fields->SetAttribute(CacheKey::ScrollPosition, DeleteEntry()); 3907 } 3908 } 3909 3910 if (aCacheDomain & CacheDomain::DOMNodeIDAndClass && mContent) { 3911 nsAtom* id = mContent->GetID(); 3912 if (id) { 3913 fields->SetAttribute(CacheKey::DOMNodeID, id); 3914 } else if (IsUpdatePush(CacheDomain::DOMNodeIDAndClass)) { 3915 fields->SetAttribute(CacheKey::DOMNodeID, DeleteEntry()); 3916 } 3917 3918 if (dom::Element* el = Elm()) { 3919 nsTArray<RefPtr<nsAtom>> classes; 3920 if (const nsAttrValue* attr = el->GetClasses()) { 3921 for (uint32_t i = 0; i < attr->GetAtomCount(); i++) { 3922 classes.AppendElement(attr->AtomAt(i)); 3923 } 3924 } 3925 if (!classes.IsEmpty()) { 3926 fields->SetAttribute(CacheKey::DOMNodeClass, std::move(classes)); 3927 } else if (IsUpdatePush(CacheDomain::DOMNodeIDAndClass)) { 3928 fields->SetAttribute(CacheKey::DOMNodeClass, DeleteEntry()); 3929 } 3930 } 3931 } 3932 3933 // State is only included in the initial push. Thereafter, cached state is 3934 // updated via events. 3935 if (aCacheDomain & CacheDomain::State) { 3936 if (IsInitialPush(CacheDomain::State)) { 3937 // Most states are updated using state change events, so we only send 3938 // these for the initial cache push. 3939 uint64_t state = ExplicitState(); 3940 // Exclude states which must be calculated by RemoteAccessible. 3941 state &= ~kRemoteCalculatedStates; 3942 fields->SetAttribute(CacheKey::State, state); 3943 } 3944 // If aria-selected isn't specified, there may be no SELECTED state. 3945 // However, aria-selected can be implicit in some cases when an item is 3946 // focused. We don't want to do this if aria-selected is explicitly 3947 // set to "false", so we need to differentiate between false and unset. 3948 if (auto ariaSelected = ARIASelected()) { 3949 fields->SetAttribute(CacheKey::ARIASelected, *ariaSelected); 3950 } else if (IsUpdatePush(CacheDomain::State)) { 3951 fields->SetAttribute(CacheKey::ARIASelected, DeleteEntry()); // Unset. 3952 } 3953 } 3954 3955 if (aCacheDomain & CacheDomain::GroupInfo && mContent) { 3956 for (nsAtom* attr : {nsGkAtoms::aria_level, nsGkAtoms::aria_setsize, 3957 nsGkAtoms::aria_posinset}) { 3958 int32_t value = 0; 3959 if (nsCoreUtils::GetUIntAttr(mContent, attr, &value)) { 3960 fields->SetAttribute(attr, value); 3961 } else if (IsUpdatePush(CacheDomain::GroupInfo)) { 3962 fields->SetAttribute(attr, DeleteEntry()); 3963 } 3964 } 3965 } 3966 3967 if (aCacheDomain & CacheDomain::Actions) { 3968 if (HasPrimaryAction()) { 3969 // Here we cache the primary action. 3970 nsAutoString actionName; 3971 ActionNameAt(0, actionName); 3972 RefPtr<nsAtom> actionAtom = NS_Atomize(actionName); 3973 fields->SetAttribute(CacheKey::PrimaryAction, actionAtom); 3974 } else if (IsUpdatePush(CacheDomain::Actions)) { 3975 fields->SetAttribute(CacheKey::PrimaryAction, DeleteEntry()); 3976 } 3977 3978 if (ImageAccessible* imgAcc = AsImage()) { 3979 // Here we cache the showlongdesc action. 3980 if (imgAcc->HasLongDesc()) { 3981 fields->SetAttribute(CacheKey::HasLongdesc, true); 3982 } else if (IsUpdatePush(CacheDomain::Actions)) { 3983 fields->SetAttribute(CacheKey::HasLongdesc, DeleteEntry()); 3984 } 3985 } 3986 3987 KeyBinding accessKey = AccessKey(); 3988 if (!accessKey.IsEmpty()) { 3989 fields->SetAttribute(CacheKey::AccessKey, accessKey.Serialize()); 3990 } else if (IsUpdatePush(CacheDomain::Actions)) { 3991 fields->SetAttribute(CacheKey::AccessKey, DeleteEntry()); 3992 } 3993 } 3994 3995 if (aCacheDomain & CacheDomain::Style) { 3996 if (RefPtr<nsAtom> display = DisplayStyle()) { 3997 fields->SetAttribute(CacheKey::CSSDisplay, display); 3998 } 3999 4000 float opacity = Opacity(); 4001 if (opacity != 1.0f) { 4002 fields->SetAttribute(CacheKey::Opacity, opacity); 4003 } else if (IsUpdatePush(CacheDomain::Style)) { 4004 fields->SetAttribute(CacheKey::Opacity, DeleteEntry()); 4005 } 4006 4007 WritingMode wm = GetWritingMode(); 4008 if (wm.GetBits()) { 4009 fields->SetAttribute(CacheKey::WritingMode, wm); 4010 } else if (IsUpdatePush(CacheDomain::Style)) { 4011 fields->SetAttribute(CacheKey::WritingMode, DeleteEntry()); 4012 } 4013 4014 if (frame && 4015 frame->StyleDisplay()->mPosition == StylePositionProperty::Fixed && 4016 nsLayoutUtils::IsReallyFixedPos(frame)) { 4017 fields->SetAttribute(CacheKey::CssPosition, nsGkAtoms::fixed); 4018 } else if (IsUpdatePush(CacheDomain::Style)) { 4019 fields->SetAttribute(CacheKey::CssPosition, DeleteEntry()); 4020 } 4021 4022 if (frame) { 4023 nsAutoCString overflow; 4024 frame->Style()->GetComputedPropertyValue(eCSSProperty_overflow, overflow); 4025 RefPtr<nsAtom> overflowAtom = NS_Atomize(overflow); 4026 if (overflowAtom == nsGkAtoms::hidden) { 4027 fields->SetAttribute(CacheKey::CSSOverflow, nsGkAtoms::hidden); 4028 } else if (IsUpdatePush(CacheDomain::Style)) { 4029 fields->SetAttribute(CacheKey::CSSOverflow, DeleteEntry()); 4030 } 4031 } 4032 } 4033 4034 if (aCacheDomain & CacheDomain::Table) { 4035 if (auto* table = HTMLTableAccessible::GetFrom(this)) { 4036 if (table->IsProbablyLayoutTable()) { 4037 fields->SetAttribute(CacheKey::TableLayoutGuess, true); 4038 } else if (IsUpdatePush(CacheDomain::Table)) { 4039 fields->SetAttribute(CacheKey::TableLayoutGuess, DeleteEntry()); 4040 } 4041 } else if (auto* cell = HTMLTableCellAccessible::GetFrom(this)) { 4042 // For HTML table cells, we must use the HTMLTableCellAccessible 4043 // GetRow/ColExtent methods rather than using the DOM attributes directly. 4044 // This is because of things like rowspan="0" which depend on knowing 4045 // about thead, tbody, etc., which is info we don't have in the a11y tree. 4046 int32_t value = static_cast<int32_t>(cell->RowExtent()); 4047 MOZ_ASSERT(value > 0); 4048 if (value > 1) { 4049 fields->SetAttribute(CacheKey::RowSpan, value); 4050 } else if (IsUpdatePush(CacheDomain::Table)) { 4051 fields->SetAttribute(CacheKey::RowSpan, DeleteEntry()); 4052 } 4053 value = static_cast<int32_t>(cell->ColExtent()); 4054 MOZ_ASSERT(value > 0); 4055 if (value > 1) { 4056 fields->SetAttribute(CacheKey::ColSpan, value); 4057 } else if (IsUpdatePush(CacheDomain::Table)) { 4058 fields->SetAttribute(CacheKey::ColSpan, DeleteEntry()); 4059 } 4060 if (mContent->AsElement()->HasAttr(nsGkAtoms::headers)) { 4061 nsTArray<uint64_t> headers; 4062 AssociatedElementsIterator iter(mDoc, mContent, nsGkAtoms::headers); 4063 while (LocalAccessible* cell = iter.Next()) { 4064 if (cell->IsTableCell()) { 4065 headers.AppendElement(cell->ID()); 4066 } 4067 } 4068 fields->SetAttribute(CacheKey::CellHeaders, std::move(headers)); 4069 } else if (IsUpdatePush(CacheDomain::Table)) { 4070 fields->SetAttribute(CacheKey::CellHeaders, DeleteEntry()); 4071 } 4072 } 4073 } 4074 4075 if (aCacheDomain & CacheDomain::ARIA && mContent && mContent->IsElement()) { 4076 // We use a nested AccAttributes to make cache updates simpler. Rather than 4077 // managing individual removals, we just replace or remove the entire set of 4078 // ARIA attributes. 4079 RefPtr<AccAttributes> ariaAttrs; 4080 aria::AttrIterator attrIt(mContent); 4081 while (attrIt.Next()) { 4082 if (!ariaAttrs) { 4083 ariaAttrs = new AccAttributes(); 4084 } 4085 attrIt.ExposeAttr(ariaAttrs); 4086 } 4087 if (ariaAttrs) { 4088 fields->SetAttribute(CacheKey::ARIAAttributes, std::move(ariaAttrs)); 4089 } else if (IsUpdatePush(CacheDomain::ARIA)) { 4090 fields->SetAttribute(CacheKey::ARIAAttributes, DeleteEntry()); 4091 } 4092 4093 if (HasCustomActions()) { 4094 fields->SetAttribute(CacheKey::HasActions, true); 4095 } else if (IsUpdatePush(CacheDomain::ARIA)) { 4096 fields->SetAttribute(CacheKey::HasActions, DeleteEntry()); 4097 } 4098 } 4099 4100 if (aCacheDomain & CacheDomain::Relations && mContent) { 4101 if (IsHTMLRadioButton() || 4102 (mContent->IsElement() && 4103 mContent->AsElement()->IsHTMLElement(nsGkAtoms::a))) { 4104 // HTML radio buttons with the same name should be grouped 4105 // and returned together when their MEMBER_OF relation is 4106 // requested. Computing LINKS_TO also requires we cache `name` on 4107 // anchor elements. 4108 nsString name; 4109 mContent->AsElement()->GetAttr(nsGkAtoms::name, name); 4110 if (!name.IsEmpty()) { 4111 fields->SetAttribute(CacheKey::DOMName, std::move(name)); 4112 } else if (IsUpdatePush(CacheDomain::Relations)) { 4113 // It's possible we used to have a name and it's since been 4114 // removed. Send a delete entry. 4115 fields->SetAttribute(CacheKey::DOMName, DeleteEntry()); 4116 } 4117 } 4118 4119 for (auto const& data : kRelationTypeAtoms) { 4120 nsTArray<uint64_t> ids; 4121 nsStaticAtom* const relAtom = data.mAtom; 4122 if (data.mValidTag && !mContent->IsHTMLElement(data.mValidTag)) { 4123 continue; 4124 } 4125 4126 Relation rel; 4127 if (data.mType == RelationType::LABEL_FOR) { 4128 // Labels are a special case -- we need to validate that the target of 4129 // their `for` attribute is in fact labelable. DOM checks this when we 4130 // call GetControl(). If a label contains an element we will return it 4131 // here. 4132 if (dom::HTMLLabelElement* labelEl = 4133 dom::HTMLLabelElement::FromNode(mContent)) { 4134 rel.AppendTarget(mDoc, labelEl->GetControl()); 4135 } 4136 } else if (data.mType == RelationType::DETAILS) { 4137 if (relAtom == nsGkAtoms::aria_details) { 4138 rel.AppendIter( 4139 new AssociatedElementsIterator(mDoc, mContent, relAtom)); 4140 } else if (relAtom == nsGkAtoms::commandfor) { 4141 if (LocalAccessible* target = GetCommandForDetailsRelation()) { 4142 rel.AppendTarget(target); 4143 } 4144 } else if (relAtom == nsGkAtoms::popovertarget) { 4145 if (LocalAccessible* target = GetPopoverTargetDetailsRelation()) { 4146 rel.AppendTarget(target); 4147 } 4148 } else if (relAtom == nsGkAtoms::target) { 4149 if (LocalAccessible* target = 4150 GetAnchorPositionTargetDetailsRelation()) { 4151 rel.AppendTarget(target); 4152 } 4153 } else { 4154 MOZ_ASSERT_UNREACHABLE("Unknown details relAtom"); 4155 } 4156 } else if (data.mType == RelationType::CONTROLLER_FOR) { 4157 // We need to use RelationByType for controls because it might include 4158 // failed aria-owned relocations or it may be an output element. 4159 // Nothing exposes an implicit reverse controls relation, so using 4160 // RelationByType here is fine. 4161 rel = RelationByType(data.mType); 4162 } else { 4163 // We use an AssociatedElementsIterator here instead of calling 4164 // RelationByType directly because we only want to cache explicit 4165 // relations. Implicit relations (e.g. LABEL_FOR exposed on the target 4166 // of aria-labelledby) will be computed and stored separately in the 4167 // parent process. 4168 rel.AppendIter(new AssociatedElementsIterator(mDoc, mContent, relAtom)); 4169 } 4170 4171 while (LocalAccessible* acc = rel.LocalNext()) { 4172 ids.AppendElement(acc->ID()); 4173 } 4174 if (ids.Length()) { 4175 fields->SetAttribute(relAtom, std::move(ids)); 4176 } else if (IsUpdatePush(CacheDomain::Relations)) { 4177 fields->SetAttribute(relAtom, DeleteEntry()); 4178 } 4179 } 4180 } 4181 4182 #if defined(XP_WIN) 4183 if (aCacheDomain & CacheDomain::InnerHTML && HasOwnContent() && 4184 mContent->IsMathMLElement(nsGkAtoms::math)) { 4185 nsString innerHTML; 4186 mContent->AsElement()->GetInnerHTML(innerHTML, IgnoreErrors()); 4187 fields->SetAttribute(CacheKey::InnerHTML, std::move(innerHTML)); 4188 } 4189 #endif // defined(XP_WIN) 4190 4191 if (aUpdateType == CacheUpdateType::Initial) { 4192 // Add fields which never change and thus only need to be included in the 4193 // initial cache push. 4194 if (mContent && mContent->IsElement()) { 4195 fields->SetAttribute(CacheKey::TagName, mContent->NodeInfo()->NameAtom()); 4196 4197 dom::Element* el = mContent->AsElement(); 4198 if (IsTextField() || IsDateTimeField()) { 4199 // Cache text input types. Accessible is recreated if this changes, 4200 // so it is considered immutable. 4201 if (const nsAttrValue* attr = el->GetParsedAttr(nsGkAtoms::type)) { 4202 RefPtr<nsAtom> inputType = attr->GetAsAtom(); 4203 if (inputType) { 4204 fields->SetAttribute(CacheKey::InputType, inputType); 4205 } 4206 } 4207 } 4208 4209 // Changing the role attribute currently re-creates the Accessible, so 4210 // it's immutable in the cache. 4211 if (const nsRoleMapEntry* roleMap = ARIARoleMap()) { 4212 // Most of the time, the role attribute is a single, known role. We 4213 // already send the map index, so we don't need to double up. 4214 if (!nsAccUtils::ARIAAttrValueIs(el, nsGkAtoms::role, roleMap->roleAtom, 4215 eIgnoreCase)) { 4216 // Multiple roles or unknown roles are rare, so just send them as a 4217 // string. 4218 nsAutoString role; 4219 nsAccUtils::GetARIAAttr(el, nsGkAtoms::role, role); 4220 fields->SetAttribute(CacheKey::ARIARole, std::move(role)); 4221 } 4222 } 4223 4224 if (auto* htmlEl = nsGenericHTMLElement::FromNode(mContent)) { 4225 // Changing popover recreates the Accessible, so it's immutable in the 4226 // cache. 4227 nsAutoString popover; 4228 htmlEl->GetPopover(popover); 4229 if (!popover.IsEmpty()) { 4230 fields->SetAttribute(CacheKey::PopupType, 4231 RefPtr{NS_Atomize(popover)}); 4232 } 4233 } 4234 } 4235 4236 if (frame) { 4237 // Note our frame's current computed style so we can track style changes 4238 // later on. 4239 mOldComputedStyle = frame->Style(); 4240 if (frame->IsTransformed()) { 4241 mStateFlags |= eOldFrameHasValidTransformStyle; 4242 } else { 4243 mStateFlags &= ~eOldFrameHasValidTransformStyle; 4244 } 4245 } 4246 4247 if (IsDoc()) { 4248 if (PresShell* presShell = AsDoc()->PresShellPtr()) { 4249 // Send the initial resolution of the document. When this changes, we 4250 // will ne notified via nsAS::NotifyOfResolutionChange 4251 float resolution = presShell->GetResolution(); 4252 fields->SetAttribute(CacheKey::Resolution, resolution); 4253 int32_t appUnitsPerDevPixel = 4254 presShell->GetPresContext()->AppUnitsPerDevPixel(); 4255 fields->SetAttribute(CacheKey::AppUnitsPerDevPixel, 4256 appUnitsPerDevPixel); 4257 } 4258 4259 nsString mimeType; 4260 AsDoc()->MimeType(mimeType); 4261 fields->SetAttribute(CacheKey::MimeType, std::move(mimeType)); 4262 } 4263 } 4264 4265 if ((aCacheDomain & (CacheDomain::Text | CacheDomain::ScrollPosition | 4266 CacheDomain::APZ) || 4267 boundsChanged) && 4268 mDoc) { 4269 mDoc->SetViewportCacheDirty(true); 4270 } 4271 4272 return fields.forget(); 4273 } 4274 4275 void LocalAccessible::MaybeQueueCacheUpdateForStyleChanges() { 4276 // mOldComputedStyle might be null if the initial cache hasn't been sent yet. 4277 // In that case, there is nothing to do here. 4278 if (!IPCAccessibilityActive() || !mOldComputedStyle) { 4279 return; 4280 } 4281 4282 if (nsIFrame* frame = GetFrame()) { 4283 const ComputedStyle* newStyle = frame->Style(); 4284 4285 const auto overflowProps = 4286 nsCSSPropertyIDSet({eCSSProperty_overflow_x, eCSSProperty_overflow_y}); 4287 4288 for (NonCustomCSSPropertyId overflowProp : overflowProps) { 4289 nsAutoCString oldOverflow, newOverflow; 4290 mOldComputedStyle->GetComputedPropertyValue(overflowProp, oldOverflow); 4291 newStyle->GetComputedPropertyValue(overflowProp, newOverflow); 4292 4293 if (oldOverflow != newOverflow) { 4294 if (oldOverflow.Equals("hidden"_ns) || 4295 newOverflow.Equals("hidden"_ns)) { 4296 mDoc->QueueCacheUpdate(this, CacheDomain::Style); 4297 } 4298 if (oldOverflow.Equals("auto"_ns) || newOverflow.Equals("auto"_ns) || 4299 oldOverflow.Equals("scroll"_ns) || 4300 newOverflow.Equals("scroll"_ns)) { 4301 // We cache a (0,0) scroll position for frames that have overflow 4302 // styling which means they _could_ become scrollable, even if the 4303 // content within them doesn't currently scroll. 4304 mDoc->QueueCacheUpdate(this, CacheDomain::ScrollPosition); 4305 } 4306 } else { 4307 ScrollContainerFrame* scrollContainerFrame = do_QueryFrame(frame); 4308 if (!scrollContainerFrame && (newOverflow.Equals("auto"_ns) || 4309 newOverflow.Equals("scroll"_ns))) { 4310 // A document's body element can lose its scroll frame if the root 4311 // element (eg. <html>) is restyled to overflow scroll/auto. In that 4312 // case we will not get any useful notifications for the body element 4313 // except for a reframe to a non-scrolling frame. 4314 mDoc->QueueCacheUpdate(this, CacheDomain::ScrollPosition); 4315 } 4316 } 4317 } 4318 4319 nsAutoCString oldDisplay, newDisplay; 4320 mOldComputedStyle->GetComputedPropertyValue(eCSSProperty_display, 4321 oldDisplay); 4322 newStyle->GetComputedPropertyValue(eCSSProperty_display, newDisplay); 4323 4324 nsAutoCString oldOpacity, newOpacity; 4325 mOldComputedStyle->GetComputedPropertyValue(eCSSProperty_opacity, 4326 oldOpacity); 4327 newStyle->GetComputedPropertyValue(eCSSProperty_opacity, newOpacity); 4328 4329 if (oldDisplay != newDisplay || oldOpacity != newOpacity) { 4330 // CacheDomain::Style covers both display and opacity, so if 4331 // either property has changed, send an update for the entire domain. 4332 mDoc->QueueCacheUpdate(this, CacheDomain::Style); 4333 } 4334 4335 if (mOldComputedStyle->StyleDisplay()->mAnchorName != 4336 newStyle->StyleDisplay()->mAnchorName) { 4337 // Anchor name changes can affect the result of 4338 // details relations. 4339 mDoc->QueueCacheUpdate(this, CacheDomain::Relations); 4340 } 4341 4342 if (mOldComputedStyle->MaybeAnchorPosReferencesDiffer(newStyle)) { 4343 // Refresh the cache for details on current target (ie. the old style) 4344 mDoc->RefreshAnchorRelationCacheForTarget(this); 4345 // Refresh the cache for details on new target asynchronously after the 4346 // next layout tick for new style. 4347 mDoc->Controller()->ScheduleNotification<DocAccessible>( 4348 mDoc, &DocAccessible::RefreshAnchorRelationCacheForTarget, this); 4349 } 4350 4351 nsAutoCString oldPosition, newPosition; 4352 mOldComputedStyle->GetComputedPropertyValue(eCSSProperty_position, 4353 oldPosition); 4354 newStyle->GetComputedPropertyValue(eCSSProperty_position, newPosition); 4355 4356 if (oldPosition != newPosition) { 4357 RefPtr<nsAtom> oldAtom = NS_Atomize(oldPosition); 4358 RefPtr<nsAtom> newAtom = NS_Atomize(newPosition); 4359 if (oldAtom == nsGkAtoms::fixed || newAtom == nsGkAtoms::fixed) { 4360 mDoc->QueueCacheUpdate(this, CacheDomain::Style); 4361 } 4362 } 4363 4364 bool newHasValidTransformStyle = 4365 newStyle->StyleDisplay()->HasTransform(frame); 4366 bool oldHasValidTransformStyle = 4367 (mStateFlags & eOldFrameHasValidTransformStyle) != 0; 4368 4369 // We should send a transform update if we're adding or 4370 // removing transform styling altogether. 4371 bool sendTransformUpdate = 4372 newHasValidTransformStyle || oldHasValidTransformStyle; 4373 4374 if (newHasValidTransformStyle && oldHasValidTransformStyle) { 4375 // If we continue to have transform styling, verify 4376 // our transform has actually changed. 4377 nsChangeHint transformHint = 4378 newStyle->StyleDisplay()->CalcTransformPropertyDifference( 4379 *mOldComputedStyle->StyleDisplay()); 4380 // If this hint exists, it implies we found a property difference 4381 sendTransformUpdate = !!transformHint; 4382 } 4383 4384 if (sendTransformUpdate) { 4385 // If our transform matrix has changed, it's possible our 4386 // viewport cache has also changed. 4387 mDoc->SetViewportCacheDirty(true); 4388 // Queuing a cache update for the TransformMatrix domain doesn't 4389 // necessarily mean we'll send the matrix itself, we may 4390 // send a DeleteEntry() instead. See BundleFieldsForCache for 4391 // more information. 4392 mDoc->QueueCacheUpdate(this, CacheDomain::TransformMatrix); 4393 } 4394 4395 mOldComputedStyle = newStyle; 4396 if (newHasValidTransformStyle) { 4397 mStateFlags |= eOldFrameHasValidTransformStyle; 4398 } else { 4399 mStateFlags &= ~eOldFrameHasValidTransformStyle; 4400 } 4401 } 4402 } 4403 4404 nsAtom* LocalAccessible::TagName() const { 4405 return mContent && mContent->IsElement() ? mContent->NodeInfo()->NameAtom() 4406 : nullptr; 4407 } 4408 4409 already_AddRefed<nsAtom> LocalAccessible::DisplayStyle() const { 4410 dom::Element* elm = Elm(); 4411 if (!elm) { 4412 return nullptr; 4413 } 4414 if (elm->IsHTMLElement(nsGkAtoms::area)) { 4415 // This is an image map area. CSS is irrelevant here. 4416 return nullptr; 4417 } 4418 static const dom::Element::AttrValuesArray presentationRoles[] = { 4419 nsGkAtoms::none, nsGkAtoms::presentation, nullptr}; 4420 if (nsAccUtils::FindARIAAttrValueIn(elm, nsGkAtoms::role, presentationRoles, 4421 eIgnoreCase) != AttrArray::ATTR_MISSING && 4422 IsGeneric()) { 4423 // This Accessible has been marked presentational, but we forced a generic 4424 // Accessible for some reason; e.g. CSS transform. Don't expose display in 4425 // this case, as the author might be explicitly trying to avoid said 4426 // exposure. 4427 return nullptr; 4428 } 4429 RefPtr<const ComputedStyle> style = 4430 nsComputedDOMStyle::GetComputedStyleNoFlush(elm); 4431 if (!style) { 4432 // The element is not styled, maybe not in the flat tree? 4433 return nullptr; 4434 } 4435 nsAutoCString value; 4436 style->GetComputedPropertyValue(eCSSProperty_display, value); 4437 return NS_Atomize(value); 4438 } 4439 4440 float LocalAccessible::Opacity() const { 4441 if (nsIFrame* frame = GetFrame()) { 4442 return frame->StyleEffects()->mOpacity; 4443 } 4444 4445 return 1.0f; 4446 } 4447 4448 WritingMode LocalAccessible::GetWritingMode() const { 4449 if (nsIFrame* frame = GetFrame()) { 4450 return WritingMode(frame->Style()); 4451 } 4452 4453 return WritingMode(); 4454 } 4455 4456 void LocalAccessible::DOMNodeID(nsString& aID) const { 4457 aID.Truncate(); 4458 if (mContent) { 4459 if (nsAtom* id = mContent->GetID()) { 4460 id->ToString(aID); 4461 } 4462 } 4463 } 4464 4465 void LocalAccessible::DOMNodeClass(nsString& aClass) const { 4466 aClass.Truncate(); 4467 if (auto* el = dom::Element::FromNodeOrNull(mContent)) { 4468 el->GetClassName(aClass); 4469 } 4470 } 4471 4472 void LocalAccessible::LiveRegionAttributes(nsAString* aLive, 4473 nsAString* aRelevant, 4474 Maybe<bool>* aAtomic, 4475 nsAString* aBusy) const { 4476 dom::Element* el = Elm(); 4477 if (!el) { 4478 return; 4479 } 4480 if (aLive) { 4481 nsAccUtils::GetARIAAttr(el, nsGkAtoms::aria_live, *aLive); 4482 } 4483 if (aRelevant) { 4484 nsAccUtils::GetARIAAttr(el, nsGkAtoms::aria_relevant, *aRelevant); 4485 } 4486 if (aAtomic) { 4487 // XXX We ignore aria-atomic="false", but this probably doesn't conform to 4488 // the spec. 4489 if (nsAccUtils::ARIAAttrValueIs(el, nsGkAtoms::aria_atomic, 4490 nsGkAtoms::_true, eCaseMatters)) { 4491 *aAtomic = Some(true); 4492 } 4493 } 4494 if (aBusy) { 4495 nsAccUtils::GetARIAAttr(el, nsGkAtoms::aria_busy, *aBusy); 4496 } 4497 } 4498 4499 Maybe<bool> LocalAccessible::ARIASelected() const { 4500 if (dom::Element* el = Elm()) { 4501 nsStaticAtom* atom = 4502 nsAccUtils::NormalizeARIAToken(el, nsGkAtoms::aria_selected); 4503 if (atom == nsGkAtoms::_true) { 4504 return Some(true); 4505 } 4506 if (atom == nsGkAtoms::_false) { 4507 return Some(false); 4508 } 4509 } 4510 return Nothing(); 4511 } 4512 4513 void LocalAccessible::StaticAsserts() const { 4514 static_assert( 4515 eLastStateFlag <= (1 << kStateFlagsBits) - 1, 4516 "LocalAccessible::mStateFlags was oversized by eLastStateFlag!"); 4517 static_assert( 4518 eLastContextFlag <= (1 << kContextFlagsBits) - 1, 4519 "LocalAccessible::mContextFlags was oversized by eLastContextFlag!"); 4520 } 4521 4522 TableAccessible* LocalAccessible::AsTable() { 4523 if (IsTable() && !mContent->IsXULElement()) { 4524 return CachedTableAccessible::GetFrom(this); 4525 } 4526 return nullptr; 4527 } 4528 4529 TableCellAccessible* LocalAccessible::AsTableCell() { 4530 if (IsTableCell() && !mContent->IsXULElement()) { 4531 return CachedTableCellAccessible::GetFrom(this); 4532 } 4533 return nullptr; 4534 } 4535 4536 Maybe<int32_t> LocalAccessible::GetIntARIAAttr(nsAtom* aAttrName) const { 4537 if (mContent) { 4538 int32_t val; 4539 if (nsCoreUtils::GetUIntAttr(mContent, aAttrName, &val)) { 4540 return Some(val); 4541 } 4542 // XXX Handle attributes that allow -1; e.g. aria-row/colcount. 4543 } 4544 return Nothing(); 4545 } 4546 4547 bool LocalAccessible::GetStringARIAAttr(nsAtom* aAttrName, 4548 nsAString& aAttrValue) const { 4549 if (dom::Element* elm = Elm()) { 4550 return nsAccUtils::GetARIAAttr(elm, aAttrName, aAttrValue); 4551 } 4552 return false; 4553 } 4554 4555 bool LocalAccessible::ARIAAttrValueIs(nsAtom* aAttrName, 4556 nsAtom* aAttrValue) const { 4557 if (dom::Element* elm = Elm()) { 4558 return nsAccUtils::ARIAAttrValueIs(elm, aAttrName, aAttrValue, 4559 eCaseMatters); 4560 } 4561 return false; 4562 } 4563 4564 bool LocalAccessible::HasARIAAttr(nsAtom* aAttrName) const { 4565 return mContent ? nsAccUtils::HasDefinedARIAToken(mContent, aAttrName) 4566 : false; 4567 } 4568 4569 bool LocalAccessible::HasCustomActions() const { 4570 dom::Element* el = Elm(); 4571 return el && nsAccUtils::HasARIAAttr(el, nsGkAtoms::aria_actions); 4572 }