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 "nsAccUtils.h"
7 
8 #include "LocalAccessible-inl.h"
9 #include "AccAttributes.h"
10 #include "ARIAMap.h"
11 #include "nsAccessibilityService.h"
12 #include "nsCoreUtils.h"
13 #include "DocAccessible.h"
14 #include "HyperTextAccessible.h"
15 #include "nsIAccessibleTypes.h"
16 #include "Role.h"
17 #include "States.h"
18 #include "TextLeafAccessible.h"
19 
20 #include "nsIDOMXULContainerElement.h"
21 #include "nsISimpleEnumerator.h"
22 #include "mozilla/a11y/PDocAccessibleChild.h"
23 #include "mozilla/dom/Document.h"
24 #include "mozilla/dom/Element.h"
25 #include "nsAccessibilityService.h"
26 
27 using namespace mozilla;
28 using namespace mozilla::a11y;
29 
SetAccGroupAttrs(AccAttributes * aAttributes,int32_t aLevel,int32_t aSetSize,int32_t aPosInSet)30 void nsAccUtils::SetAccGroupAttrs(AccAttributes* aAttributes, int32_t aLevel,
31                                   int32_t aSetSize, int32_t aPosInSet) {
32   nsAutoString value;
33 
34   if (aLevel) {
35     aAttributes->SetAttribute(nsGkAtoms::level, aLevel);
36   }
37 
38   if (aSetSize && aPosInSet) {
39     aAttributes->SetAttribute(nsGkAtoms::posinset, aPosInSet);
40     aAttributes->SetAttribute(nsGkAtoms::setsize, aSetSize);
41   }
42 }
43 
GetDefaultLevel(const LocalAccessible * aAccessible)44 int32_t nsAccUtils::GetDefaultLevel(const LocalAccessible* aAccessible) {
45   roles::Role role = aAccessible->Role();
46 
47   if (role == roles::OUTLINEITEM) return 1;
48 
49   if (role == roles::ROW) {
50     LocalAccessible* parent = aAccessible->LocalParent();
51     // It is a row inside flatten treegrid. Group level is always 1 until it
52     // is overriden by aria-level attribute.
53     if (parent && parent->Role() == roles::TREE_TABLE) return 1;
54   }
55 
56   return 0;
57 }
58 
GetARIAOrDefaultLevel(const LocalAccessible * aAccessible)59 int32_t nsAccUtils::GetARIAOrDefaultLevel(const LocalAccessible* aAccessible) {
60   int32_t level = 0;
61   nsCoreUtils::GetUIntAttr(aAccessible->GetContent(), nsGkAtoms::aria_level,
62                            &level);
63 
64   if (level != 0) return level;
65 
66   return GetDefaultLevel(aAccessible);
67 }
68 
GetLevelForXULContainerItem(nsIContent * aContent)69 int32_t nsAccUtils::GetLevelForXULContainerItem(nsIContent* aContent) {
70   nsCOMPtr<nsIDOMXULContainerItemElement> item =
71       aContent->AsElement()->AsXULContainerItem();
72   if (!item) return 0;
73 
74   nsCOMPtr<dom::Element> containerElement;
75   item->GetParentContainer(getter_AddRefs(containerElement));
76   nsCOMPtr<nsIDOMXULContainerElement> container =
77       containerElement ? containerElement->AsXULContainer() : nullptr;
78   if (!container) return 0;
79 
80   // Get level of the item.
81   int32_t level = -1;
82   while (container) {
83     level++;
84 
85     container->GetParentContainer(getter_AddRefs(containerElement));
86     container = containerElement ? containerElement->AsXULContainer() : nullptr;
87   }
88 
89   return level;
90 }
91 
SetLiveContainerAttributes(AccAttributes * aAttributes,nsIContent * aStartContent)92 void nsAccUtils::SetLiveContainerAttributes(AccAttributes* aAttributes,
93                                             nsIContent* aStartContent) {
94   nsAutoString live, relevant, busy;
95   dom::Document* doc = aStartContent->GetComposedDoc();
96   if (!doc) {
97     return;
98   }
99   dom::Element* topEl = doc->GetRootElement();
100   nsIContent* ancestor = aStartContent;
101   while (ancestor) {
102     // container-relevant attribute
103     if (relevant.IsEmpty() &&
104         HasDefinedARIAToken(ancestor, nsGkAtoms::aria_relevant) &&
105         ancestor->AsElement()->GetAttr(kNameSpaceID_None,
106                                        nsGkAtoms::aria_relevant, relevant)) {
107       aAttributes->SetAttribute(nsGkAtoms::containerRelevant, relevant);
108     }
109 
110     // container-live, and container-live-role attributes
111     if (live.IsEmpty()) {
112       const nsRoleMapEntry* role = nullptr;
113       if (ancestor->IsElement()) {
114         role = aria::GetRoleMap(ancestor->AsElement());
115       }
116       if (HasDefinedARIAToken(ancestor, nsGkAtoms::aria_live)) {
117         ancestor->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::aria_live,
118                                        live);
119       } else if (role) {
120         GetLiveAttrValue(role->liveAttRule, live);
121       } else if (nsStaticAtom* value = GetAccService()->MarkupAttribute(
122                      ancestor, nsGkAtoms::aria_live)) {
123         value->ToString(live);
124       }
125 
126       if (!live.IsEmpty()) {
127         aAttributes->SetAttribute(nsGkAtoms::containerLive, live);
128         if (role) {
129           aAttributes->SetAttribute(nsGkAtoms::containerLiveRole,
130                                     role->ARIARoleString());
131         }
132       }
133     }
134 
135     // container-atomic attribute
136     if (ancestor->IsElement() && ancestor->AsElement()->AttrValueIs(
137                                      kNameSpaceID_None, nsGkAtoms::aria_atomic,
138                                      nsGkAtoms::_true, eCaseMatters)) {
139       aAttributes->SetAttribute(nsGkAtoms::containerAtomic, true);
140     }
141 
142     // container-busy attribute
143     if (busy.IsEmpty() && HasDefinedARIAToken(ancestor, nsGkAtoms::aria_busy) &&
144         ancestor->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::aria_busy,
145                                        busy)) {
146       aAttributes->SetAttribute(nsGkAtoms::containerBusy, busy);
147     }
148 
149     if (ancestor == topEl) {
150       break;
151     }
152 
153     ancestor = ancestor->GetParent();
154     if (!ancestor) {
155       ancestor = topEl;  // Use <body>/<frameset>
156     }
157   }
158 }
159 
HasDefinedARIAToken(nsIContent * aContent,nsAtom * aAtom)160 bool nsAccUtils::HasDefinedARIAToken(nsIContent* aContent, nsAtom* aAtom) {
161   NS_ASSERTION(aContent, "aContent is null in call to HasDefinedARIAToken!");
162 
163   if (!aContent->IsElement()) return false;
164 
165   dom::Element* element = aContent->AsElement();
166   if (!element->HasAttr(kNameSpaceID_None, aAtom) ||
167       element->AttrValueIs(kNameSpaceID_None, aAtom, nsGkAtoms::_empty,
168                            eCaseMatters) ||
169       element->AttrValueIs(kNameSpaceID_None, aAtom, nsGkAtoms::_undefined,
170                            eCaseMatters)) {
171     return false;
172   }
173   return true;
174 }
175 
GetARIAToken(dom::Element * aElement,nsAtom * aAttr)176 nsStaticAtom* nsAccUtils::GetARIAToken(dom::Element* aElement, nsAtom* aAttr) {
177   if (!HasDefinedARIAToken(aElement, aAttr)) return nsGkAtoms::_empty;
178 
179   static dom::Element::AttrValuesArray tokens[] = {
180       nsGkAtoms::_false, nsGkAtoms::_true, nsGkAtoms::mixed, nullptr};
181 
182   int32_t idx =
183       aElement->FindAttrValueIn(kNameSpaceID_None, aAttr, tokens, eCaseMatters);
184   if (idx >= 0) return tokens[idx];
185 
186   return nullptr;
187 }
188 
NormalizeARIAToken(dom::Element * aElement,nsAtom * aAttr)189 nsStaticAtom* nsAccUtils::NormalizeARIAToken(dom::Element* aElement,
190                                              nsAtom* aAttr) {
191   if (!HasDefinedARIAToken(aElement, aAttr)) {
192     return nsGkAtoms::_empty;
193   }
194 
195   if (aAttr == nsGkAtoms::aria_current) {
196     static dom::Element::AttrValuesArray tokens[] = {
197         nsGkAtoms::page, nsGkAtoms::step, nsGkAtoms::location_,
198         nsGkAtoms::date, nsGkAtoms::time, nsGkAtoms::_true,
199         nullptr};
200     int32_t idx = aElement->FindAttrValueIn(kNameSpaceID_None, aAttr, tokens,
201                                             eCaseMatters);
202     // If the token is present, return it, otherwise TRUE as per spec.
203     return (idx >= 0) ? tokens[idx] : nsGkAtoms::_true;
204   }
205 
206   return nullptr;
207 }
208 
GetSelectableContainer(LocalAccessible * aAccessible,uint64_t aState)209 LocalAccessible* nsAccUtils::GetSelectableContainer(
210     LocalAccessible* aAccessible, uint64_t aState) {
211   if (!aAccessible) return nullptr;
212 
213   if (!(aState & states::SELECTABLE)) return nullptr;
214 
215   LocalAccessible* parent = aAccessible;
216   while ((parent = parent->LocalParent()) && !parent->IsSelect()) {
217     if (parent->Role() == roles::PANE) return nullptr;
218   }
219   return parent;
220 }
221 
IsDOMAttrTrue(const LocalAccessible * aAccessible,nsAtom * aAttr)222 bool nsAccUtils::IsDOMAttrTrue(const LocalAccessible* aAccessible,
223                                nsAtom* aAttr) {
224   dom::Element* el = aAccessible->Elm();
225   return el && el->AttrValueIs(kNameSpaceID_None, aAttr, nsGkAtoms::_true,
226                                eCaseMatters);
227 }
228 
TableFor(LocalAccessible * aRow)229 LocalAccessible* nsAccUtils::TableFor(LocalAccessible* aRow) {
230   if (aRow) {
231     LocalAccessible* table = aRow->LocalParent();
232     if (table) {
233       roles::Role tableRole = table->Role();
234       const nsRoleMapEntry* roleMapEntry = table->ARIARoleMap();
235       if (tableRole == roles::GROUPING ||  // if there's a rowgroup.
236           (table->IsGenericHyperText() && !roleMapEntry &&
237            !table->IsTable())) {  // or there is a wrapping text container
238         table = table->LocalParent();
239         if (table) tableRole = table->Role();
240       }
241 
242       return (tableRole == roles::TABLE || tableRole == roles::TREE_TABLE ||
243               tableRole == roles::MATHML_TABLE)
244                  ? table
245                  : nullptr;
246     }
247   }
248 
249   return nullptr;
250 }
251 
GetTextContainer(nsINode * aNode)252 HyperTextAccessible* nsAccUtils::GetTextContainer(nsINode* aNode) {
253   // Get text accessible containing the result node.
254   DocAccessible* doc = GetAccService()->GetDocAccessible(aNode->OwnerDoc());
255   LocalAccessible* accessible =
256       doc ? doc->GetAccessibleOrContainer(aNode) : nullptr;
257   if (!accessible) return nullptr;
258 
259   do {
260     HyperTextAccessible* textAcc = accessible->AsHyperText();
261     if (textAcc) return textAcc;
262 
263     accessible = accessible->LocalParent();
264   } while (accessible);
265 
266   return nullptr;
267 }
268 
ConvertToScreenCoords(int32_t aX,int32_t aY,uint32_t aCoordinateType,LocalAccessible * aAccessible)269 nsIntPoint nsAccUtils::ConvertToScreenCoords(int32_t aX, int32_t aY,
270                                              uint32_t aCoordinateType,
271                                              LocalAccessible* aAccessible) {
272   nsIntPoint coords(aX, aY);
273 
274   switch (aCoordinateType) {
275     case nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE:
276       break;
277 
278     case nsIAccessibleCoordinateType::COORDTYPE_WINDOW_RELATIVE: {
279       coords += nsCoreUtils::GetScreenCoordsForWindow(aAccessible->GetNode());
280       break;
281     }
282 
283     case nsIAccessibleCoordinateType::COORDTYPE_PARENT_RELATIVE: {
284       coords += GetScreenCoordsForParent(aAccessible);
285       break;
286     }
287 
288     default:
289       MOZ_ASSERT_UNREACHABLE("invalid coord type!");
290   }
291 
292   return coords;
293 }
294 
ConvertScreenCoordsTo(int32_t * aX,int32_t * aY,uint32_t aCoordinateType,LocalAccessible * aAccessible)295 void nsAccUtils::ConvertScreenCoordsTo(int32_t* aX, int32_t* aY,
296                                        uint32_t aCoordinateType,
297                                        LocalAccessible* aAccessible) {
298   switch (aCoordinateType) {
299     case nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE:
300       break;
301 
302     case nsIAccessibleCoordinateType::COORDTYPE_WINDOW_RELATIVE: {
303       nsIntPoint coords =
304           nsCoreUtils::GetScreenCoordsForWindow(aAccessible->GetNode());
305       *aX -= coords.x;
306       *aY -= coords.y;
307       break;
308     }
309 
310     case nsIAccessibleCoordinateType::COORDTYPE_PARENT_RELATIVE: {
311       nsIntPoint coords = GetScreenCoordsForParent(aAccessible);
312       *aX -= coords.x;
313       *aY -= coords.y;
314       break;
315     }
316 
317     default:
318       MOZ_ASSERT_UNREACHABLE("invalid coord type!");
319   }
320 }
321 
GetScreenCoordsForParent(LocalAccessible * aAccessible)322 nsIntPoint nsAccUtils::GetScreenCoordsForParent(LocalAccessible* aAccessible) {
323   LocalAccessible* parent = aAccessible->LocalParent();
324   if (!parent) return nsIntPoint(0, 0);
325 
326   nsIFrame* parentFrame = parent->GetFrame();
327   if (!parentFrame) return nsIntPoint(0, 0);
328 
329   nsRect rect = parentFrame->GetScreenRectInAppUnits();
330   return nsPoint(rect.X(), rect.Y())
331       .ToNearestPixels(parentFrame->PresContext()->AppUnitsPerDevPixel());
332 }
333 
GetLiveAttrValue(uint32_t aRule,nsAString & aValue)334 bool nsAccUtils::GetLiveAttrValue(uint32_t aRule, nsAString& aValue) {
335   switch (aRule) {
336     case eOffLiveAttr:
337       aValue = u"off"_ns;
338       return true;
339     case ePoliteLiveAttr:
340       aValue = u"polite"_ns;
341       return true;
342     case eAssertiveLiveAttr:
343       aValue = u"assertive"_ns;
344       return true;
345   }
346 
347   return false;
348 }
349 
350 #ifdef DEBUG
351 
IsTextInterfaceSupportCorrect(LocalAccessible * aAccessible)352 bool nsAccUtils::IsTextInterfaceSupportCorrect(LocalAccessible* aAccessible) {
353   // Don't test for accessible docs, it makes us create accessibles too
354   // early and fire mutation events before we need to
355   if (aAccessible->IsDoc()) return true;
356 
357   bool foundText = false;
358   uint32_t childCount = aAccessible->ChildCount();
359   for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) {
360     LocalAccessible* child = aAccessible->LocalChildAt(childIdx);
361     if (child && child->IsText()) {
362       foundText = true;
363       break;
364     }
365   }
366 
367   return !foundText || aAccessible->IsHyperText();
368 }
369 #endif
370 
TextLength(LocalAccessible * aAccessible)371 uint32_t nsAccUtils::TextLength(LocalAccessible* aAccessible) {
372   if (!aAccessible->IsText()) {
373     return 1;
374   }
375 
376   TextLeafAccessible* textLeaf = aAccessible->AsTextLeaf();
377   if (textLeaf) return textLeaf->Text().Length();
378 
379   // For list bullets (or anything other accessible which would compute its own
380   // text. They don't have their own frame.
381   // XXX In the future, list bullets may have frame and anon content, so
382   // we should be able to remove this at that point
383   nsAutoString text;
384   aAccessible->AppendTextTo(text);  // Get all the text
385   return text.Length();
386 }
387 
MustPrune(AccessibleOrProxy aAccessible)388 bool nsAccUtils::MustPrune(AccessibleOrProxy aAccessible) {
389   MOZ_ASSERT(!aAccessible.IsNull());
390   roles::Role role = aAccessible.Role();
391 
392   if (role == roles::SLIDER) {
393     // Always prune the tree for sliders, as it doesn't make sense for a
394     // slider to have descendants and this confuses NVDA.
395     return true;
396   }
397 
398   if (role != roles::MENUITEM && role != roles::COMBOBOX_OPTION &&
399       role != roles::OPTION && role != roles::ENTRY &&
400       role != roles::FLAT_EQUATION && role != roles::PASSWORD_TEXT &&
401       role != roles::PUSHBUTTON && role != roles::TOGGLE_BUTTON &&
402       role != roles::GRAPHIC && role != roles::PROGRESSBAR &&
403       role != roles::SEPARATOR) {
404     // If it doesn't match any of these roles, don't prune its children.
405     return false;
406   }
407 
408   if (aAccessible.ChildCount() != 1) {
409     // If the accessible has more than one child, don't prune it.
410     return false;
411   }
412 
413   roles::Role childRole = aAccessible.FirstChild().Role();
414   // If the accessible's child is a text leaf, prune the accessible.
415   return childRole == roles::TEXT_LEAF || childRole == roles::STATICTEXT;
416 }
417 
IsARIALive(const LocalAccessible * aAccessible)418 bool nsAccUtils::IsARIALive(const LocalAccessible* aAccessible) {
419   // Get computed aria-live property based on the closest container with the
420   // attribute. Inner nodes override outer nodes within the same
421   // document.
422   // This should be the same as the container-live attribute, but we don't need
423   // the other container-* attributes, so we can't use the same function.
424   nsIContent* ancestor = aAccessible->GetContent();
425   if (!ancestor) {
426     return false;
427   }
428   dom::Document* doc = ancestor->GetComposedDoc();
429   if (!doc) {
430     return false;
431   }
432   dom::Element* topEl = doc->GetRootElement();
433   while (ancestor) {
434     const nsRoleMapEntry* role = nullptr;
435     if (ancestor->IsElement()) {
436       role = aria::GetRoleMap(ancestor->AsElement());
437     }
438     nsAutoString live;
439     if (HasDefinedARIAToken(ancestor, nsGkAtoms::aria_live)) {
440       ancestor->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::aria_live,
441                                      live);
442     } else if (role) {
443       GetLiveAttrValue(role->liveAttRule, live);
444     } else if (nsStaticAtom* value = GetAccService()->MarkupAttribute(
445                    ancestor, nsGkAtoms::aria_live)) {
446       value->ToString(live);
447     }
448     if (!live.IsEmpty() && !live.EqualsLiteral("off")) {
449       return true;
450     }
451 
452     if (ancestor == topEl) {
453       break;
454     }
455 
456     ancestor = ancestor->GetParent();
457     if (!ancestor) {
458       ancestor = topEl;  // Use <body>/<frameset>
459     }
460   }
461 
462   return false;
463 }
464