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"
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"
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
) {
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();
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.
66 container
->GetParentContainer(getter_AddRefs(containerElement
));
67 container
= containerElement
? containerElement
->AsXULContainer() : nullptr;
73 void nsAccUtils::SetLiveContainerAttributes(AccAttributes
* aAttributes
,
74 Accessible
* aStartAcc
) {
75 nsAutoString live
, relevant
, busy
;
76 nsStaticAtom
* role
= nullptr;
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);
87 const nsRoleMapEntry
* roleMap
= acc
->ARIARoleMap();
89 // aria-live wasn't explicitly set. See if an aria-live value is implied
90 // by an ARIA role or markup element.
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
;
107 if (!live
.IsEmpty()) {
108 aAttributes
->SetAttribute(nsGkAtoms::containerLive
, std::move(live
));
111 aAttributes
->SetAttribute(nsGkAtoms::containerLiveRole
, std::move(role
));
113 if (!relevant
.IsEmpty()) {
114 aAttributes
->SetAttribute(nsGkAtoms::containerRelevant
,
115 std::move(relevant
));
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
);
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
,
146 !aAttrs
->AttrValueIs(kNameSpaceID_None
, aAtom
, nsGkAtoms::_undefined
,
150 nsStaticAtom
* nsAccUtils::NormalizeARIAToken(const AttrArray
* aAttrs
,
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
,
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};
170 aAttrs
->FindAttrValueIn(kNameSpaceID_None
, aAttr
, tokens
, eCaseMatters
);
178 nsStaticAtom
* nsAccUtils::NormalizeARIAToken(dom::Element
* aElement
,
180 if (auto* htmlElement
= nsGenericHTMLElement::FromNode(aElement
);
181 htmlElement
&& !aElement
->HasAttr(aAttr
)) {
182 const auto* defaults
= GetARIADefaults(htmlElement
);
184 return nsGkAtoms::_empty
;
186 return NormalizeARIAToken(defaults
, aAttr
);
188 return NormalizeARIAToken(&aElement
->GetAttrs(), aAttr
);
191 Accessible
* nsAccUtils::GetSelectableContainer(const Accessible
* aAccessible
,
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
) {
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
,
216 dom::Element
* el
= aAccessible
->Elm();
217 return el
&& ARIAAttrValueIs(el
, aAttr
, nsGkAtoms::_true
, eCaseMatters
);
220 Accessible
* nsAccUtils::TableFor(Accessible
* aAcc
) {
222 (!aAcc
->IsTable() && !aAcc
->IsTableRow() && !aAcc
->IsTableCell())) {
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
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;
248 HyperTextAccessible
* textAcc
= accessible
->AsHyperText();
249 if (textAcc
) return textAcc
;
251 accessible
= accessible
->LocalParent();
252 } while (accessible
);
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
:
267 case nsIAccessibleCoordinateType::COORDTYPE_WINDOW_RELATIVE
: {
268 coords
+= GetScreenCoordsForWindow(aAccessible
);
272 case nsIAccessibleCoordinateType::COORDTYPE_PARENT_RELATIVE
: {
273 coords
+= GetScreenCoordsForParent(aAccessible
);
278 MOZ_ASSERT_UNREACHABLE("invalid coord type!");
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
:
293 case nsIAccessibleCoordinateType::COORDTYPE_WINDOW_RELATIVE
: {
294 LayoutDeviceIntPoint coords
= GetScreenCoordsForWindow(aAccessible
);
300 case nsIAccessibleCoordinateType::COORDTYPE_PARENT_RELATIVE
: {
301 LayoutDeviceIntPoint coords
= GetScreenCoordsForParent(aAccessible
);
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();
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
);
344 baseWindow
->GetPosition(&coords
.x
.value
,
345 &coords
.y
.value
); // in device pixels
351 bool nsAccUtils::GetLiveAttrValue(uint32_t aRule
, nsAString
& aValue
) {
356 case ePoliteLiveAttr
:
357 aValue
= u
"polite"_ns
;
359 case eAssertiveLiveAttr
:
360 aValue
= u
"assertive"_ns
;
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()) {
384 return !foundText
|| aAccessible
->IsHyperText();
388 uint32_t nsAccUtils::TextLength(Accessible
* aAccessible
) {
389 if (!aAccessible
->IsText()) {
393 if (LocalAccessible
* localAcc
= aAccessible
->AsLocal()) {
394 TextLeafAccessible
* textLeaf
= localAcc
->AsTextLeaf();
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 "
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
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.
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.
436 if (aAccessible
->ChildCount() != 1) {
437 // If the accessible has more than one child, don't prune it.
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
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();
456 dom::Document
* doc
= ancestor
->GetComposedDoc();
460 dom::Element
* topEl
= doc
->GetRootElement();
462 const nsRoleMapEntry
* role
= nullptr;
463 if (ancestor
->IsElement()) {
464 role
= aria::GetRoleMap(ancestor
->AsElement());
467 if (HasDefinedARIAToken(ancestor
, nsGkAtoms::aria_live
)) {
468 GetARIAAttr(ancestor
->AsElement(), nsGkAtoms::aria_live
, live
);
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")) {
479 if (ancestor
== topEl
) {
483 ancestor
= ancestor
->GetParent();
485 ancestor
= topEl
; // Use <body>/<frameset>
492 Accessible
* nsAccUtils::DocumentFor(Accessible
* aAcc
) {
496 if (LocalAccessible
* localAcc
= aAcc
->AsLocal()) {
497 return localAcc
->Document();
499 return aAcc
->AsRemote()->Document();
502 Accessible
* nsAccUtils::GetAccessibleByID(Accessible
* aDoc
, uint64_t aID
) {
506 if (LocalAccessible
* localAcc
= aDoc
->AsLocal()) {
507 if (DocAccessible
* doc
= localAcc
->AsDoc()) {
509 // GetAccessibleByUniqueID doesn't treat 0 as the document.
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
);
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
);
535 auto* internals
= element
->GetInternals();
539 return &internals
->GetAttrs();
542 bool nsAccUtils::HasARIAAttr(dom::Element
* aElement
, const nsAtom
* aName
) {
543 if (aElement
->HasAttr(kNameSpaceID_None
, aName
)) {
546 const auto* defaults
= GetARIADefaults(aElement
);
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
)) {
558 const auto* defaults
= GetARIADefaults(aElement
);
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
)) {
570 const auto* defaults
= GetARIADefaults(aElement
);
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
)) {
583 const auto* defaults
= GetARIADefaults(aElement
);
587 return defaults
->AttrValueIs(kNameSpaceID_None
, aName
, aValue
,
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
)) {
597 const auto* defaults
= GetARIADefaults(aElement
);
601 return defaults
->AttrValueIs(kNameSpaceID_None
, aName
, aValue
,
605 int32_t nsAccUtils::FindARIAAttrValueIn(dom::Element
* aElement
,
607 AttrArray::AttrValuesArray
* aValues
,
608 nsCaseTreatment aCaseSensitive
) {
609 int32_t index
= aElement
->FindAttrValueIn(kNameSpaceID_None
, aName
, aValues
,
611 if (index
== AttrArray::ATTR_MISSING
) {
612 const auto* defaults
= GetARIADefaults(aElement
);
616 index
= defaults
->FindAttrValueIn(kNameSpaceID_None
, aName
, aValues
,