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