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 "AccAttributes.h"
10 #include "nsCoreUtils.h"
11 #include "nsGenericHTMLElement.h"
12 #include "DocAccessible.h"
13 #include "DocAccessibleParent.h"
14 #include "HyperTextAccessible.h"
15 #include "nsIAccessibleTypes.h"
16 #include "mozilla/a11y/Role.h"
18 #include "TextLeafAccessible.h"
20 #include "nsIBaseWindow.h"
21 #include "nsIDocShellTreeOwner.h"
22 #include "nsIDOMXULContainerElement.h"
23 #include "mozilla/a11y/RemoteAccessible.h"
24 #include "mozilla/dom/Document.h"
25 #include "mozilla/dom/Element.h"
26 #include "mozilla/dom/ElementInternals.h"
27 #include "nsAccessibilityService.h"
29 using namespace mozilla
;
30 using namespace mozilla::a11y
;
32 void nsAccUtils::SetAccGroupAttrs(AccAttributes
* aAttributes
, int32_t aLevel
,
33 int32_t aSetSize
, int32_t aPosInSet
) {
37 aAttributes
->SetAttribute(nsGkAtoms::level
, aLevel
);
40 if (aSetSize
&& aPosInSet
) {
41 aAttributes
->SetAttribute(nsGkAtoms::posinset
, aPosInSet
);
42 aAttributes
->SetAttribute(nsGkAtoms::setsize
, aSetSize
);
46 int32_t nsAccUtils::GetLevelForXULContainerItem(nsIContent
* aContent
) {
47 nsCOMPtr
<nsIDOMXULContainerItemElement
> item
=
48 aContent
->AsElement()->AsXULContainerItem();
51 nsCOMPtr
<dom::Element
> containerElement
;
52 item
->GetParentContainer(getter_AddRefs(containerElement
));
53 nsCOMPtr
<nsIDOMXULContainerElement
> container
=
54 containerElement
? containerElement
->AsXULContainer() : nullptr;
55 if (!container
) return 0;
57 // Get level of the item.
62 container
->GetParentContainer(getter_AddRefs(containerElement
));
63 container
= containerElement
? containerElement
->AsXULContainer() : nullptr;
69 void nsAccUtils::SetLiveContainerAttributes(AccAttributes
* aAttributes
,
70 Accessible
* aStartAcc
) {
71 nsAutoString live
, relevant
, busy
;
72 nsStaticAtom
* role
= nullptr;
74 for (Accessible
* acc
= aStartAcc
; acc
; acc
= acc
->Parent()) {
75 // We only want the nearest value for each attribute. If we already got a
76 // value, don't bother fetching it from further ancestors.
77 const bool wasLiveEmpty
= live
.IsEmpty();
78 acc
->LiveRegionAttributes(wasLiveEmpty
? &live
: nullptr,
79 relevant
.IsEmpty() ? &relevant
: nullptr,
80 atomic
? nullptr : &atomic
,
81 busy
.IsEmpty() ? &busy
: nullptr);
83 const nsRoleMapEntry
* roleMap
= acc
->ARIARoleMap();
85 // aria-live wasn't explicitly set. See if an aria-live value is implied
86 // by an ARIA role or markup element.
88 GetLiveAttrValue(roleMap
->liveAttRule
, live
);
89 } else if (nsStaticAtom
* value
= GetAccService()->MarkupAttribute(
90 acc
, nsGkAtoms::aria_live
)) {
91 value
->ToString(live
);
94 if (!live
.IsEmpty() && roleMap
&&
95 roleMap
->roleAtom
!= nsGkAtoms::_empty
) {
96 role
= roleMap
->roleAtom
;
103 if (!live
.IsEmpty()) {
104 aAttributes
->SetAttribute(nsGkAtoms::containerLive
, std::move(live
));
107 aAttributes
->SetAttribute(nsGkAtoms::containerLiveRole
, std::move(role
));
109 if (!relevant
.IsEmpty()) {
110 aAttributes
->SetAttribute(nsGkAtoms::containerRelevant
,
111 std::move(relevant
));
114 aAttributes
->SetAttribute(nsGkAtoms::containerAtomic
, *atomic
);
116 if (!busy
.IsEmpty()) {
117 aAttributes
->SetAttribute(nsGkAtoms::containerBusy
, std::move(busy
));
121 bool nsAccUtils::HasDefinedARIAToken(nsIContent
* aContent
, nsAtom
* aAtom
) {
122 NS_ASSERTION(aContent
, "aContent is null in call to HasDefinedARIAToken!");
124 if (!aContent
->IsElement()) return false;
126 dom::Element
* element
= aContent
->AsElement();
127 if (auto* htmlElement
= nsGenericHTMLElement::FromNode(element
);
128 htmlElement
&& !element
->HasAttr(aAtom
)) {
129 const auto* defaults
= GetARIADefaults(htmlElement
);
133 return HasDefinedARIAToken(defaults
, aAtom
);
135 return HasDefinedARIAToken(&element
->GetAttrs(), aAtom
);
138 bool nsAccUtils::HasDefinedARIAToken(const AttrArray
* aAttrs
, nsAtom
* aAtom
) {
139 return aAttrs
->HasAttr(aAtom
) &&
140 !aAttrs
->AttrValueIs(kNameSpaceID_None
, aAtom
, nsGkAtoms::_empty
,
142 !aAttrs
->AttrValueIs(kNameSpaceID_None
, aAtom
, nsGkAtoms::_undefined
,
146 nsStaticAtom
* nsAccUtils::NormalizeARIAToken(const AttrArray
* aAttrs
,
148 if (!HasDefinedARIAToken(aAttrs
, aAttr
)) {
149 return nsGkAtoms::_empty
;
152 if (aAttr
== nsGkAtoms::aria_current
) {
153 static AttrArray::AttrValuesArray tokens
[] = {
154 nsGkAtoms::page
, nsGkAtoms::step
, nsGkAtoms::location_
,
155 nsGkAtoms::date
, nsGkAtoms::time
, nsGkAtoms::_true
,
158 aAttrs
->FindAttrValueIn(kNameSpaceID_None
, aAttr
, tokens
, eCaseMatters
);
159 // If the token is present, return it, otherwise TRUE as per spec.
160 return (idx
>= 0) ? tokens
[idx
] : nsGkAtoms::_true
;
163 static AttrArray::AttrValuesArray tokens
[] = {
164 nsGkAtoms::_false
, nsGkAtoms::_true
, nsGkAtoms::mixed
, nullptr};
166 aAttrs
->FindAttrValueIn(kNameSpaceID_None
, aAttr
, tokens
, eCaseMatters
);
174 nsStaticAtom
* nsAccUtils::NormalizeARIAToken(dom::Element
* aElement
,
176 if (auto* htmlElement
= nsGenericHTMLElement::FromNode(aElement
);
177 htmlElement
&& !aElement
->HasAttr(aAttr
)) {
178 const auto* defaults
= GetARIADefaults(htmlElement
);
180 return nsGkAtoms::_empty
;
182 return NormalizeARIAToken(defaults
, aAttr
);
184 return NormalizeARIAToken(&aElement
->GetAttrs(), aAttr
);
187 Accessible
* nsAccUtils::GetSelectableContainer(const Accessible
* aAccessible
,
189 if (!aAccessible
) return nullptr;
191 if (!(aState
& states::SELECTABLE
)) return nullptr;
192 MOZ_ASSERT(!aAccessible
->IsDoc());
194 const Accessible
* parent
= aAccessible
;
195 while ((parent
= parent
->Parent()) && !parent
->IsSelect()) {
196 if (parent
->IsDoc() || parent
->Role() == roles::PANE
) {
200 return const_cast<Accessible
*>(parent
);
203 LocalAccessible
* nsAccUtils::GetSelectableContainer(
204 LocalAccessible
* aAccessible
, uint64_t aState
) {
205 Accessible
* selectable
=
206 GetSelectableContainer(static_cast<Accessible
*>(aAccessible
), aState
);
207 return selectable
? selectable
->AsLocal() : nullptr;
210 bool nsAccUtils::IsDOMAttrTrue(const LocalAccessible
* aAccessible
,
212 dom::Element
* el
= aAccessible
->Elm();
213 return el
&& ARIAAttrValueIs(el
, aAttr
, nsGkAtoms::_true
, eCaseMatters
);
216 Accessible
* nsAccUtils::TableFor(Accessible
* aAcc
) {
218 (!aAcc
->IsTable() && !aAcc
->IsTableRow() && !aAcc
->IsTableCell())) {
221 Accessible
* table
= aAcc
;
222 for (; table
&& !table
->IsTable(); table
= table
->Parent()) {
224 // We don't assert (table && table->IsTable()) here because
225 // it's possible for this tree walk to yield no table at all
226 // ex. because a table part has been moved in the tree
231 LocalAccessible
* nsAccUtils::TableFor(LocalAccessible
* aRow
) {
232 Accessible
* table
= TableFor(static_cast<Accessible
*>(aRow
));
233 return table
? table
->AsLocal() : nullptr;
236 HyperTextAccessible
* nsAccUtils::GetTextContainer(nsINode
* aNode
) {
237 // Get text accessible containing the result node.
238 DocAccessible
* doc
= GetAccService()->GetDocAccessible(aNode
->OwnerDoc());
239 LocalAccessible
* accessible
=
240 doc
? doc
->GetAccessibleOrContainer(aNode
) : nullptr;
241 if (!accessible
) return nullptr;
244 HyperTextAccessible
* textAcc
= accessible
->AsHyperText();
245 if (textAcc
) return textAcc
;
247 accessible
= accessible
->LocalParent();
248 } while (accessible
);
253 LayoutDeviceIntPoint
nsAccUtils::ConvertToScreenCoords(
254 int32_t aX
, int32_t aY
, uint32_t aCoordinateType
, Accessible
* aAccessible
) {
255 LayoutDeviceIntPoint
coords(aX
, aY
);
257 switch (aCoordinateType
) {
258 // Regardless of coordinate type, the coords returned
259 // are in dev pixels.
260 case nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE
:
263 case nsIAccessibleCoordinateType::COORDTYPE_WINDOW_RELATIVE
: {
264 coords
+= GetScreenCoordsForWindow(aAccessible
);
268 case nsIAccessibleCoordinateType::COORDTYPE_PARENT_RELATIVE
: {
269 coords
+= GetScreenCoordsForParent(aAccessible
);
274 MOZ_ASSERT_UNREACHABLE("invalid coord type!");
280 void nsAccUtils::ConvertScreenCoordsTo(int32_t* aX
, int32_t* aY
,
281 uint32_t aCoordinateType
,
282 Accessible
* aAccessible
) {
283 switch (aCoordinateType
) {
284 // Regardless of coordinate type, the values returned for
285 // aX and aY are in dev pixels.
286 case nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE
:
289 case nsIAccessibleCoordinateType::COORDTYPE_WINDOW_RELATIVE
: {
290 LayoutDeviceIntPoint coords
= GetScreenCoordsForWindow(aAccessible
);
296 case nsIAccessibleCoordinateType::COORDTYPE_PARENT_RELATIVE
: {
297 LayoutDeviceIntPoint coords
= GetScreenCoordsForParent(aAccessible
);
304 MOZ_ASSERT_UNREACHABLE("invalid coord type!");
308 LayoutDeviceIntPoint
nsAccUtils::GetScreenCoordsForParent(
309 Accessible
* aAccessible
) {
310 if (!aAccessible
) return LayoutDeviceIntPoint();
312 if (Accessible
* parent
= aAccessible
->Parent()) {
313 LayoutDeviceIntRect parentBounds
= parent
->Bounds();
314 // The rect returned from Bounds() is already in dev
315 // pixels, so we don't need to do any conversion here.
316 return parentBounds
.TopLeft();
319 return LayoutDeviceIntPoint();
322 LayoutDeviceIntPoint
nsAccUtils::GetScreenCoordsForWindow(
323 Accessible
* aAccessible
) {
324 a11y::LocalAccessible
* localAcc
= aAccessible
->AsLocal();
326 localAcc
= aAccessible
->AsRemote()->OuterDocOfRemoteBrowser();
329 LayoutDeviceIntPoint
coords(0, 0);
330 nsCOMPtr
<nsIDocShellTreeItem
> treeItem(
331 nsCoreUtils::GetDocShellFor(localAcc
->GetNode()));
332 if (!treeItem
) return coords
;
334 nsCOMPtr
<nsIDocShellTreeOwner
> treeOwner
;
335 treeItem
->GetTreeOwner(getter_AddRefs(treeOwner
));
336 if (!treeOwner
) return coords
;
338 nsCOMPtr
<nsIBaseWindow
> baseWindow
= do_QueryInterface(treeOwner
);
340 baseWindow
->GetPosition(&coords
.x
.value
,
341 &coords
.y
.value
); // in device pixels
347 bool nsAccUtils::GetLiveAttrValue(uint32_t aRule
, nsAString
& aValue
) {
352 case ePoliteLiveAttr
:
353 aValue
= u
"polite"_ns
;
355 case eAssertiveLiveAttr
:
356 aValue
= u
"assertive"_ns
;
365 bool nsAccUtils::IsTextInterfaceSupportCorrect(LocalAccessible
* aAccessible
) {
366 // Don't test for accessible docs, it makes us create accessibles too
367 // early and fire mutation events before we need to
368 if (aAccessible
->IsDoc()) return true;
370 bool foundText
= false;
371 uint32_t childCount
= aAccessible
->ChildCount();
372 for (uint32_t childIdx
= 0; childIdx
< childCount
; childIdx
++) {
373 LocalAccessible
* child
= aAccessible
->LocalChildAt(childIdx
);
374 if (child
&& child
->IsText()) {
380 return !foundText
|| aAccessible
->IsHyperText();
384 uint32_t nsAccUtils::TextLength(Accessible
* aAccessible
) {
385 if (!aAccessible
->IsText()) {
389 if (LocalAccessible
* localAcc
= aAccessible
->AsLocal()) {
390 TextLeafAccessible
* textLeaf
= localAcc
->AsTextLeaf();
392 return textLeaf
->Text().Length();
394 } else if (aAccessible
->IsText()) {
395 RemoteAccessible
* remoteAcc
= aAccessible
->AsRemote();
396 MOZ_ASSERT(remoteAcc
);
397 return remoteAcc
->GetCachedTextLength();
400 // For list bullets (or anything other accessible which would compute its own
401 // text. They don't have their own frame.
402 // XXX In the future, list bullets may have frame and anon content, so
403 // we should be able to remove this at that point
405 aAccessible
->AppendTextTo(text
); // Get all the text
406 return text
.Length();
409 bool nsAccUtils::MustPrune(Accessible
* aAccessible
) {
410 MOZ_ASSERT(aAccessible
);
411 roles::Role role
= aAccessible
->Role();
413 if (role
== roles::SLIDER
|| role
== roles::PROGRESSBAR
) {
414 // Always prune the tree for sliders and progressbars, as it doesn't make
415 // sense for either to have descendants. Per the ARIA spec, children of
416 // these elements are presentational. They also confuse NVDA.
420 if (role
!= roles::MENUITEM
&& role
!= roles::COMBOBOX_OPTION
&&
421 role
!= roles::OPTION
&& role
!= roles::ENTRY
&&
422 role
!= roles::FLAT_EQUATION
&& role
!= roles::PASSWORD_TEXT
&&
423 role
!= roles::PUSHBUTTON
&& role
!= roles::TOGGLE_BUTTON
&&
424 role
!= roles::GRAPHIC
&& role
!= roles::SEPARATOR
) {
425 // If it doesn't match any of these roles, don't prune its children.
429 if (aAccessible
->ChildCount() != 1) {
430 // If the accessible has more than one child, don't prune it.
434 roles::Role childRole
= aAccessible
->FirstChild()->Role();
435 // If the accessible's child is a text leaf, prune the accessible.
436 return childRole
== roles::TEXT_LEAF
|| childRole
== roles::STATICTEXT
;
439 bool nsAccUtils::IsARIALive(const LocalAccessible
* aAccessible
) {
440 // Get computed aria-live property based on the closest container with the
441 // attribute. Inner nodes override outer nodes within the same
443 // This should be the same as the container-live attribute, but we don't need
444 // the other container-* attributes, so we can't use the same function.
445 nsIContent
* ancestor
= aAccessible
->GetContent();
449 dom::Document
* doc
= ancestor
->GetComposedDoc();
453 dom::Element
* topEl
= doc
->GetRootElement();
455 const nsRoleMapEntry
* role
= nullptr;
456 if (ancestor
->IsElement()) {
457 role
= aria::GetRoleMap(ancestor
->AsElement());
460 if (HasDefinedARIAToken(ancestor
, nsGkAtoms::aria_live
)) {
461 GetARIAAttr(ancestor
->AsElement(), nsGkAtoms::aria_live
, live
);
463 GetLiveAttrValue(role
->liveAttRule
, live
);
464 } else if (nsStaticAtom
* value
= GetAccService()->MarkupAttribute(
465 ancestor
, nsGkAtoms::aria_live
)) {
466 value
->ToString(live
);
468 if (!live
.IsEmpty() && !live
.EqualsLiteral("off")) {
472 if (ancestor
== topEl
) {
476 ancestor
= ancestor
->GetParent();
478 ancestor
= topEl
; // Use <body>/<frameset>
485 Accessible
* nsAccUtils::DocumentFor(Accessible
* aAcc
) {
489 if (LocalAccessible
* localAcc
= aAcc
->AsLocal()) {
490 return localAcc
->Document();
492 return aAcc
->AsRemote()->Document();
495 Accessible
* nsAccUtils::GetAccessibleByID(Accessible
* aDoc
, uint64_t aID
) {
499 if (LocalAccessible
* localAcc
= aDoc
->AsLocal()) {
500 if (DocAccessible
* doc
= localAcc
->AsDoc()) {
502 // GetAccessibleByUniqueID doesn't treat 0 as the document.
505 return doc
->GetAccessibleByUniqueID(
506 reinterpret_cast<void*>(static_cast<uintptr_t>(aID
)));
508 } else if (DocAccessibleParent
* doc
= aDoc
->AsRemote()->AsDoc()) {
509 return doc
->GetAccessible(aID
);
514 void nsAccUtils::DocumentURL(Accessible
* aDoc
, nsAString
& aURL
) {
515 MOZ_ASSERT(aDoc
&& aDoc
->IsDoc());
516 if (LocalAccessible
* localAcc
= aDoc
->AsLocal()) {
517 return localAcc
->AsDoc()->URL(aURL
);
519 return aDoc
->AsRemote()->AsDoc()->URL(aURL
);
522 void nsAccUtils::DocumentMimeType(Accessible
* aDoc
, nsAString
& aMimeType
) {
523 MOZ_ASSERT(aDoc
&& aDoc
->IsDoc());
524 if (LocalAccessible
* localAcc
= aDoc
->AsLocal()) {
525 return localAcc
->AsDoc()->MimeType(aMimeType
);
527 return aDoc
->AsRemote()->AsDoc()->MimeType(aMimeType
);
530 // ARIA Accessibility Default Accessors
531 const AttrArray
* nsAccUtils::GetARIADefaults(dom::Element
* aElement
) {
532 auto* element
= nsGenericHTMLElement::FromNode(aElement
);
536 auto* internals
= element
->GetInternals();
540 return &internals
->GetAttrs();
543 bool nsAccUtils::HasARIAAttr(dom::Element
* aElement
, const nsAtom
* aName
) {
544 if (aElement
->HasAttr(aName
)) {
547 const auto* defaults
= GetARIADefaults(aElement
);
551 return defaults
->HasAttr(aName
);
554 bool nsAccUtils::GetARIAAttr(dom::Element
* aElement
, const nsAtom
* aName
,
555 nsAString
& aResult
) {
556 if (aElement
->GetAttr(aName
, aResult
)) {
559 const auto* defaults
= GetARIADefaults(aElement
);
563 return defaults
->GetAttr(aName
, aResult
);
566 const nsAttrValue
* nsAccUtils::GetARIAAttr(dom::Element
* aElement
,
567 const nsAtom
* aName
) {
568 if (const auto* val
= aElement
->GetParsedAttr(aName
, kNameSpaceID_None
)) {
571 const auto* defaults
= GetARIADefaults(aElement
);
575 return defaults
->GetAttr(aName
, kNameSpaceID_None
);
578 bool nsAccUtils::ARIAAttrValueIs(dom::Element
* aElement
, const nsAtom
* aName
,
579 const nsAString
& aValue
,
580 nsCaseTreatment aCaseSensitive
) {
581 if (aElement
->AttrValueIs(kNameSpaceID_None
, aName
, aValue
, aCaseSensitive
)) {
584 const auto* defaults
= GetARIADefaults(aElement
);
588 return defaults
->AttrValueIs(kNameSpaceID_None
, aName
, aValue
,
592 bool nsAccUtils::ARIAAttrValueIs(dom::Element
* aElement
, const nsAtom
* aName
,
593 const nsAtom
* aValue
,
594 nsCaseTreatment aCaseSensitive
) {
595 if (aElement
->AttrValueIs(kNameSpaceID_None
, aName
, aValue
, aCaseSensitive
)) {
598 const auto* defaults
= GetARIADefaults(aElement
);
602 return defaults
->AttrValueIs(kNameSpaceID_None
, aName
, aValue
,
606 int32_t nsAccUtils::FindARIAAttrValueIn(dom::Element
* aElement
,
608 AttrArray::AttrValuesArray
* aValues
,
609 nsCaseTreatment aCaseSensitive
) {
610 int32_t index
= aElement
->FindAttrValueIn(kNameSpaceID_None
, aName
, aValues
,
612 if (index
== AttrArray::ATTR_MISSING
) {
613 const auto* defaults
= GetARIADefaults(aElement
);
617 index
= defaults
->FindAttrValueIn(kNameSpaceID_None
, aName
, aValues
,