Bug 1833114 - Simplify marking code now |stack| represents the mark stack for the...
[gecko.git] / accessible / base / nsAccUtils.cpp
blobf6756431e98f0287473eee500a6005ed8e45bb0d
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/. */
6 #include "nsAccUtils.h"
8 #include "LocalAccessible-inl.h"
9 #include "AccAttributes.h"
10 #include "ARIAMap.h"
11 #include "nsAccessibilityService.h"
12 #include "nsCoreUtils.h"
13 #include "nsGenericHTMLElement.h"
14 #include "DocAccessible.h"
15 #include "DocAccessibleParent.h"
16 #include "HyperTextAccessible.h"
17 #include "nsIAccessibleTypes.h"
18 #include "Role.h"
19 #include "States.h"
20 #include "TextLeafAccessible.h"
22 #include "nsIBaseWindow.h"
23 #include "nsIDocShellTreeOwner.h"
24 #include "nsIDOMXULContainerElement.h"
25 #include "nsISimpleEnumerator.h"
26 #include "mozilla/a11y/PDocAccessibleChild.h"
27 #include "mozilla/a11y/RemoteAccessible.h"
28 #include "mozilla/dom/Document.h"
29 #include "mozilla/dom/Element.h"
30 #include "mozilla/dom/ElementInternals.h"
31 #include "nsAccessibilityService.h"
33 using namespace mozilla;
34 using namespace mozilla::a11y;
36 void nsAccUtils::SetAccGroupAttrs(AccAttributes* aAttributes, int32_t aLevel,
37 int32_t aSetSize, int32_t aPosInSet) {
38 nsAutoString value;
40 if (aLevel) {
41 aAttributes->SetAttribute(nsGkAtoms::level, aLevel);
44 if (aSetSize && aPosInSet) {
45 aAttributes->SetAttribute(nsGkAtoms::posinset, aPosInSet);
46 aAttributes->SetAttribute(nsGkAtoms::setsize, aSetSize);
50 int32_t nsAccUtils::GetLevelForXULContainerItem(nsIContent* aContent) {
51 nsCOMPtr<nsIDOMXULContainerItemElement> item =
52 aContent->AsElement()->AsXULContainerItem();
53 if (!item) return 0;
55 nsCOMPtr<dom::Element> containerElement;
56 item->GetParentContainer(getter_AddRefs(containerElement));
57 nsCOMPtr<nsIDOMXULContainerElement> container =
58 containerElement ? containerElement->AsXULContainer() : nullptr;
59 if (!container) return 0;
61 // Get level of the item.
62 int32_t level = -1;
63 while (container) {
64 level++;
66 container->GetParentContainer(getter_AddRefs(containerElement));
67 container = containerElement ? containerElement->AsXULContainer() : nullptr;
70 return level;
73 void nsAccUtils::SetLiveContainerAttributes(AccAttributes* aAttributes,
74 Accessible* aStartAcc) {
75 nsAutoString live, relevant, busy;
76 nsStaticAtom* role = nullptr;
77 Maybe<bool> atomic;
78 for (Accessible* acc = aStartAcc; acc; acc = acc->Parent()) {
79 // We only want the nearest value for each attribute. If we already got a
80 // value, don't bother fetching it from further ancestors.
81 const bool wasLiveEmpty = live.IsEmpty();
82 acc->LiveRegionAttributes(wasLiveEmpty ? &live : nullptr,
83 relevant.IsEmpty() ? &relevant : nullptr,
84 atomic ? nullptr : &atomic,
85 busy.IsEmpty() ? &busy : nullptr);
86 if (wasLiveEmpty) {
87 const nsRoleMapEntry* roleMap = acc->ARIARoleMap();
88 if (live.IsEmpty()) {
89 // aria-live wasn't explicitly set. See if an aria-live value is implied
90 // by an ARIA role or markup element.
91 if (roleMap) {
92 GetLiveAttrValue(roleMap->liveAttRule, live);
93 } else if (nsStaticAtom* value = GetAccService()->MarkupAttribute(
94 acc, nsGkAtoms::aria_live)) {
95 value->ToString(live);
98 if (!live.IsEmpty() && roleMap &&
99 roleMap->roleAtom != nsGkAtoms::_empty) {
100 role = roleMap->roleAtom;
103 if (acc->IsDoc()) {
104 break;
107 if (!live.IsEmpty()) {
108 aAttributes->SetAttribute(nsGkAtoms::containerLive, std::move(live));
110 if (role) {
111 aAttributes->SetAttribute(nsGkAtoms::containerLiveRole, std::move(role));
113 if (!relevant.IsEmpty()) {
114 aAttributes->SetAttribute(nsGkAtoms::containerRelevant,
115 std::move(relevant));
117 if (atomic) {
118 aAttributes->SetAttribute(nsGkAtoms::containerAtomic, *atomic);
120 if (!busy.IsEmpty()) {
121 aAttributes->SetAttribute(nsGkAtoms::containerBusy, std::move(busy));
125 bool nsAccUtils::HasDefinedARIAToken(nsIContent* aContent, nsAtom* aAtom) {
126 NS_ASSERTION(aContent, "aContent is null in call to HasDefinedARIAToken!");
128 if (!aContent->IsElement()) return false;
130 dom::Element* element = aContent->AsElement();
131 if (auto* htmlElement = nsGenericHTMLElement::FromNode(element);
132 htmlElement && !element->HasAttr(aAtom)) {
133 const auto* defaults = GetARIADefaults(htmlElement);
134 if (!defaults) {
135 return false;
137 return HasDefinedARIAToken(defaults, aAtom);
139 return HasDefinedARIAToken(&element->GetAttrs(), aAtom);
142 bool nsAccUtils::HasDefinedARIAToken(const AttrArray* aAttrs, nsAtom* aAtom) {
143 return aAttrs->HasAttr(kNameSpaceID_None, aAtom) &&
144 !aAttrs->AttrValueIs(kNameSpaceID_None, aAtom, nsGkAtoms::_empty,
145 eCaseMatters) &&
146 !aAttrs->AttrValueIs(kNameSpaceID_None, aAtom, nsGkAtoms::_undefined,
147 eCaseMatters);
150 nsStaticAtom* nsAccUtils::NormalizeARIAToken(const AttrArray* aAttrs,
151 nsAtom* aAttr) {
152 if (!HasDefinedARIAToken(aAttrs, aAttr)) {
153 return nsGkAtoms::_empty;
156 if (aAttr == nsGkAtoms::aria_current) {
157 static AttrArray::AttrValuesArray tokens[] = {
158 nsGkAtoms::page, nsGkAtoms::step, nsGkAtoms::location_,
159 nsGkAtoms::date, nsGkAtoms::time, nsGkAtoms::_true,
160 nullptr};
161 int32_t idx =
162 aAttrs->FindAttrValueIn(kNameSpaceID_None, aAttr, tokens, eCaseMatters);
163 // If the token is present, return it, otherwise TRUE as per spec.
164 return (idx >= 0) ? tokens[idx] : nsGkAtoms::_true;
167 static AttrArray::AttrValuesArray tokens[] = {
168 nsGkAtoms::_false, nsGkAtoms::_true, nsGkAtoms::mixed, nullptr};
169 int32_t idx =
170 aAttrs->FindAttrValueIn(kNameSpaceID_None, aAttr, tokens, eCaseMatters);
171 if (idx >= 0) {
172 return tokens[idx];
175 return nullptr;
178 nsStaticAtom* nsAccUtils::NormalizeARIAToken(dom::Element* aElement,
179 nsAtom* aAttr) {
180 if (auto* htmlElement = nsGenericHTMLElement::FromNode(aElement);
181 htmlElement && !aElement->HasAttr(aAttr)) {
182 const auto* defaults = GetARIADefaults(htmlElement);
183 if (!defaults) {
184 return nsGkAtoms::_empty;
186 return NormalizeARIAToken(defaults, aAttr);
188 return NormalizeARIAToken(&aElement->GetAttrs(), aAttr);
191 Accessible* nsAccUtils::GetSelectableContainer(const Accessible* aAccessible,
192 uint64_t aState) {
193 if (!aAccessible) return nullptr;
195 if (!(aState & states::SELECTABLE)) return nullptr;
196 MOZ_ASSERT(!aAccessible->IsDoc());
198 const Accessible* parent = aAccessible;
199 while ((parent = parent->Parent()) && !parent->IsSelect()) {
200 if (parent->IsDoc() || parent->Role() == roles::PANE) {
201 return nullptr;
204 return const_cast<Accessible*>(parent);
207 LocalAccessible* nsAccUtils::GetSelectableContainer(
208 LocalAccessible* aAccessible, uint64_t aState) {
209 Accessible* selectable =
210 GetSelectableContainer(static_cast<Accessible*>(aAccessible), aState);
211 return selectable ? selectable->AsLocal() : nullptr;
214 bool nsAccUtils::IsDOMAttrTrue(const LocalAccessible* aAccessible,
215 nsAtom* aAttr) {
216 dom::Element* el = aAccessible->Elm();
217 return el && ARIAAttrValueIs(el, aAttr, nsGkAtoms::_true, eCaseMatters);
220 Accessible* nsAccUtils::TableFor(Accessible* aAcc) {
221 if (!aAcc ||
222 (!aAcc->IsTable() && !aAcc->IsTableRow() && !aAcc->IsTableCell())) {
223 return nullptr;
225 Accessible* table = aAcc;
226 for (; table && !table->IsTable(); table = table->Parent()) {
228 // We don't assert (table && table->IsTable()) here because
229 // it's possible for this tree walk to yield no table at all
230 // ex. because a table part has been moved in the tree
231 // using aria-owns.
232 return table;
235 LocalAccessible* nsAccUtils::TableFor(LocalAccessible* aRow) {
236 Accessible* table = TableFor(static_cast<Accessible*>(aRow));
237 return table ? table->AsLocal() : nullptr;
240 HyperTextAccessible* nsAccUtils::GetTextContainer(nsINode* aNode) {
241 // Get text accessible containing the result node.
242 DocAccessible* doc = GetAccService()->GetDocAccessible(aNode->OwnerDoc());
243 LocalAccessible* accessible =
244 doc ? doc->GetAccessibleOrContainer(aNode) : nullptr;
245 if (!accessible) return nullptr;
247 do {
248 HyperTextAccessible* textAcc = accessible->AsHyperText();
249 if (textAcc) return textAcc;
251 accessible = accessible->LocalParent();
252 } while (accessible);
254 return nullptr;
257 LayoutDeviceIntPoint nsAccUtils::ConvertToScreenCoords(
258 int32_t aX, int32_t aY, uint32_t aCoordinateType, Accessible* aAccessible) {
259 LayoutDeviceIntPoint coords(aX, aY);
261 switch (aCoordinateType) {
262 // Regardless of coordinate type, the coords returned
263 // are in dev pixels.
264 case nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE:
265 break;
267 case nsIAccessibleCoordinateType::COORDTYPE_WINDOW_RELATIVE: {
268 coords += GetScreenCoordsForWindow(aAccessible);
269 break;
272 case nsIAccessibleCoordinateType::COORDTYPE_PARENT_RELATIVE: {
273 coords += GetScreenCoordsForParent(aAccessible);
274 break;
277 default:
278 MOZ_ASSERT_UNREACHABLE("invalid coord type!");
281 return coords;
284 void nsAccUtils::ConvertScreenCoordsTo(int32_t* aX, int32_t* aY,
285 uint32_t aCoordinateType,
286 Accessible* aAccessible) {
287 switch (aCoordinateType) {
288 // Regardless of coordinate type, the values returned for
289 // aX and aY are in dev pixels.
290 case nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE:
291 break;
293 case nsIAccessibleCoordinateType::COORDTYPE_WINDOW_RELATIVE: {
294 LayoutDeviceIntPoint coords = GetScreenCoordsForWindow(aAccessible);
295 *aX -= coords.x;
296 *aY -= coords.y;
297 break;
300 case nsIAccessibleCoordinateType::COORDTYPE_PARENT_RELATIVE: {
301 LayoutDeviceIntPoint coords = GetScreenCoordsForParent(aAccessible);
302 *aX -= coords.x;
303 *aY -= coords.y;
304 break;
307 default:
308 MOZ_ASSERT_UNREACHABLE("invalid coord type!");
312 LayoutDeviceIntPoint nsAccUtils::GetScreenCoordsForParent(
313 Accessible* aAccessible) {
314 if (!aAccessible) return LayoutDeviceIntPoint();
316 if (Accessible* parent = aAccessible->Parent()) {
317 LayoutDeviceIntRect parentBounds = parent->Bounds();
318 // The rect returned from Bounds() is already in dev
319 // pixels, so we don't need to do any conversion here.
320 return parentBounds.TopLeft();
323 return LayoutDeviceIntPoint();
326 LayoutDeviceIntPoint nsAccUtils::GetScreenCoordsForWindow(
327 Accessible* aAccessible) {
328 a11y::LocalAccessible* localAcc = aAccessible->AsLocal();
329 if (!localAcc) {
330 localAcc = aAccessible->AsRemote()->OuterDocOfRemoteBrowser();
333 LayoutDeviceIntPoint coords(0, 0);
334 nsCOMPtr<nsIDocShellTreeItem> treeItem(
335 nsCoreUtils::GetDocShellFor(localAcc->GetNode()));
336 if (!treeItem) return coords;
338 nsCOMPtr<nsIDocShellTreeOwner> treeOwner;
339 treeItem->GetTreeOwner(getter_AddRefs(treeOwner));
340 if (!treeOwner) return coords;
342 nsCOMPtr<nsIBaseWindow> baseWindow = do_QueryInterface(treeOwner);
343 if (baseWindow) {
344 baseWindow->GetPosition(&coords.x.value,
345 &coords.y.value); // in device pixels
348 return coords;
351 bool nsAccUtils::GetLiveAttrValue(uint32_t aRule, nsAString& aValue) {
352 switch (aRule) {
353 case eOffLiveAttr:
354 aValue = u"off"_ns;
355 return true;
356 case ePoliteLiveAttr:
357 aValue = u"polite"_ns;
358 return true;
359 case eAssertiveLiveAttr:
360 aValue = u"assertive"_ns;
361 return true;
364 return false;
367 #ifdef DEBUG
369 bool nsAccUtils::IsTextInterfaceSupportCorrect(LocalAccessible* aAccessible) {
370 // Don't test for accessible docs, it makes us create accessibles too
371 // early and fire mutation events before we need to
372 if (aAccessible->IsDoc()) return true;
374 bool foundText = false;
375 uint32_t childCount = aAccessible->ChildCount();
376 for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) {
377 LocalAccessible* child = aAccessible->LocalChildAt(childIdx);
378 if (child && child->IsText()) {
379 foundText = true;
380 break;
384 return !foundText || aAccessible->IsHyperText();
386 #endif
388 uint32_t nsAccUtils::TextLength(Accessible* aAccessible) {
389 if (!aAccessible->IsText()) {
390 return 1;
393 if (LocalAccessible* localAcc = aAccessible->AsLocal()) {
394 TextLeafAccessible* textLeaf = localAcc->AsTextLeaf();
395 if (textLeaf) {
396 return textLeaf->Text().Length();
398 } else if (aAccessible->IsText()) {
399 MOZ_ASSERT(a11y::IsCacheActive(),
400 "Shouldn't be called on a RemoteAccessible unless the cache is "
401 "enabled");
402 RemoteAccessible* remoteAcc = aAccessible->AsRemote();
403 MOZ_ASSERT(remoteAcc);
404 return remoteAcc->GetCachedTextLength();
407 // For list bullets (or anything other accessible which would compute its own
408 // text. They don't have their own frame.
409 // XXX In the future, list bullets may have frame and anon content, so
410 // we should be able to remove this at that point
411 nsAutoString text;
412 aAccessible->AppendTextTo(text); // Get all the text
413 return text.Length();
416 bool nsAccUtils::MustPrune(Accessible* aAccessible) {
417 MOZ_ASSERT(aAccessible);
418 roles::Role role = aAccessible->Role();
420 if (role == roles::SLIDER || role == roles::PROGRESSBAR) {
421 // Always prune the tree for sliders and progressbars, as it doesn't make
422 // sense for either to have descendants. Per the ARIA spec, children of
423 // these elements are presentational. They also confuse NVDA.
424 return true;
427 if (role != roles::MENUITEM && role != roles::COMBOBOX_OPTION &&
428 role != roles::OPTION && role != roles::ENTRY &&
429 role != roles::FLAT_EQUATION && role != roles::PASSWORD_TEXT &&
430 role != roles::PUSHBUTTON && role != roles::TOGGLE_BUTTON &&
431 role != roles::GRAPHIC && role != roles::SEPARATOR) {
432 // If it doesn't match any of these roles, don't prune its children.
433 return false;
436 if (aAccessible->ChildCount() != 1) {
437 // If the accessible has more than one child, don't prune it.
438 return false;
441 roles::Role childRole = aAccessible->FirstChild()->Role();
442 // If the accessible's child is a text leaf, prune the accessible.
443 return childRole == roles::TEXT_LEAF || childRole == roles::STATICTEXT;
446 bool nsAccUtils::IsARIALive(const LocalAccessible* aAccessible) {
447 // Get computed aria-live property based on the closest container with the
448 // attribute. Inner nodes override outer nodes within the same
449 // document.
450 // This should be the same as the container-live attribute, but we don't need
451 // the other container-* attributes, so we can't use the same function.
452 nsIContent* ancestor = aAccessible->GetContent();
453 if (!ancestor) {
454 return false;
456 dom::Document* doc = ancestor->GetComposedDoc();
457 if (!doc) {
458 return false;
460 dom::Element* topEl = doc->GetRootElement();
461 while (ancestor) {
462 const nsRoleMapEntry* role = nullptr;
463 if (ancestor->IsElement()) {
464 role = aria::GetRoleMap(ancestor->AsElement());
466 nsAutoString live;
467 if (HasDefinedARIAToken(ancestor, nsGkAtoms::aria_live)) {
468 GetARIAAttr(ancestor->AsElement(), nsGkAtoms::aria_live, live);
469 } else if (role) {
470 GetLiveAttrValue(role->liveAttRule, live);
471 } else if (nsStaticAtom* value = GetAccService()->MarkupAttribute(
472 ancestor, nsGkAtoms::aria_live)) {
473 value->ToString(live);
475 if (!live.IsEmpty() && !live.EqualsLiteral("off")) {
476 return true;
479 if (ancestor == topEl) {
480 break;
483 ancestor = ancestor->GetParent();
484 if (!ancestor) {
485 ancestor = topEl; // Use <body>/<frameset>
489 return false;
492 Accessible* nsAccUtils::DocumentFor(Accessible* aAcc) {
493 if (!aAcc) {
494 return nullptr;
496 if (LocalAccessible* localAcc = aAcc->AsLocal()) {
497 return localAcc->Document();
499 return aAcc->AsRemote()->Document();
502 Accessible* nsAccUtils::GetAccessibleByID(Accessible* aDoc, uint64_t aID) {
503 if (!aDoc) {
504 return nullptr;
506 if (LocalAccessible* localAcc = aDoc->AsLocal()) {
507 if (DocAccessible* doc = localAcc->AsDoc()) {
508 if (!aID) {
509 // GetAccessibleByUniqueID doesn't treat 0 as the document.
510 return aDoc;
512 return doc->GetAccessibleByUniqueID(
513 reinterpret_cast<void*>(static_cast<uintptr_t>(aID)));
515 } else if (DocAccessibleParent* doc = aDoc->AsRemote()->AsDoc()) {
516 return doc->GetAccessible(aID);
518 return nullptr;
521 void nsAccUtils::DocumentURL(Accessible* aDoc, nsAString& aURL) {
522 MOZ_ASSERT(aDoc && aDoc->IsDoc());
523 if (LocalAccessible* localAcc = aDoc->AsLocal()) {
524 return localAcc->AsDoc()->URL(aURL);
526 return aDoc->AsRemote()->AsDoc()->URL(aURL);
529 // ARIA Accessibility Default Accessors
530 const AttrArray* nsAccUtils::GetARIADefaults(dom::Element* aElement) {
531 auto* element = nsGenericHTMLElement::FromNode(aElement);
532 if (!element) {
533 return nullptr;
535 auto* internals = element->GetInternals();
536 if (!internals) {
537 return nullptr;
539 return &internals->GetAttrs();
542 bool nsAccUtils::HasARIAAttr(dom::Element* aElement, const nsAtom* aName) {
543 if (aElement->HasAttr(kNameSpaceID_None, aName)) {
544 return true;
546 const auto* defaults = GetARIADefaults(aElement);
547 if (!defaults) {
548 return false;
550 return defaults->HasAttr(kNameSpaceID_None, aName);
553 bool nsAccUtils::GetARIAAttr(dom::Element* aElement, const nsAtom* aName,
554 nsAString& aResult) {
555 if (aElement->GetAttr(kNameSpaceID_None, aName, aResult)) {
556 return true;
558 const auto* defaults = GetARIADefaults(aElement);
559 if (!defaults) {
560 return false;
562 return defaults->GetAttr(kNameSpaceID_None, aName, aResult);
565 const nsAttrValue* nsAccUtils::GetARIAAttr(dom::Element* aElement,
566 const nsAtom* aName) {
567 if (const auto* val = aElement->GetParsedAttr(aName, kNameSpaceID_None)) {
568 return val;
570 const auto* defaults = GetARIADefaults(aElement);
571 if (!defaults) {
572 return nullptr;
574 return defaults->GetAttr(aName, kNameSpaceID_None);
577 bool nsAccUtils::ARIAAttrValueIs(dom::Element* aElement, const nsAtom* aName,
578 const nsAString& aValue,
579 nsCaseTreatment aCaseSensitive) {
580 if (aElement->AttrValueIs(kNameSpaceID_None, aName, aValue, aCaseSensitive)) {
581 return true;
583 const auto* defaults = GetARIADefaults(aElement);
584 if (!defaults) {
585 return false;
587 return defaults->AttrValueIs(kNameSpaceID_None, aName, aValue,
588 aCaseSensitive);
591 bool nsAccUtils::ARIAAttrValueIs(dom::Element* aElement, const nsAtom* aName,
592 const nsAtom* aValue,
593 nsCaseTreatment aCaseSensitive) {
594 if (aElement->AttrValueIs(kNameSpaceID_None, aName, aValue, aCaseSensitive)) {
595 return true;
597 const auto* defaults = GetARIADefaults(aElement);
598 if (!defaults) {
599 return false;
601 return defaults->AttrValueIs(kNameSpaceID_None, aName, aValue,
602 aCaseSensitive);
605 int32_t nsAccUtils::FindARIAAttrValueIn(dom::Element* aElement,
606 const nsAtom* aName,
607 AttrArray::AttrValuesArray* aValues,
608 nsCaseTreatment aCaseSensitive) {
609 int32_t index = aElement->FindAttrValueIn(kNameSpaceID_None, aName, aValues,
610 aCaseSensitive);
611 if (index == AttrArray::ATTR_MISSING) {
612 const auto* defaults = GetARIADefaults(aElement);
613 if (!defaults) {
614 return index;
616 index = defaults->FindAttrValueIn(kNameSpaceID_None, aName, aValues,
617 aCaseSensitive);
619 return index;